Skip to content

Commit

Permalink
feat: add localized formatting (#2160)
Browse files Browse the repository at this point in the history
  • Loading branch information
LuLaValva authored May 24, 2024
1 parent 88e0f01 commit 5c71772
Show file tree
Hide file tree
Showing 15 changed files with 378 additions and 77 deletions.
5 changes: 5 additions & 0 deletions .changeset/dirty-badgers-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ebay/ebayui-core": minor
---

feat: add localized formatting using date-fns
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getLocale, localeDefault } from ".";

export type DayISO = `${number}-${number}-${number}`;

export function findFirstDayOfWeek(localeName: string): number {
Expand All @@ -13,8 +15,10 @@ export function findFirstDayOfWeek(localeName: string): number {
return 0;
}

export function getWeekdayInfo(localeName: string) {
const firstDayOfWeek = findFirstDayOfWeek(localeName);
export function getWeekdayInfo(localeName?: string) {
localeName = localeDefault(localeName);
const locale = getLocale(localeName);
const firstDayOfWeek = locale.weekStart;

const weekdayLabelFormatter = new Intl.DateTimeFormat(localeName, {
weekday: "short",
Expand Down Expand Up @@ -49,9 +53,3 @@ export function offsetISO(iso: DayISO, days: number) {
date.setUTCDate(date.getUTCDate() + days);
return toISO(date);
}

export function localeOverride(locale?: string) {
const defaultLanguage =
typeof navigator !== "undefined" ? navigator.language : "en-US";
return locale || defaultLanguage;
}
53 changes: 53 additions & 0 deletions src/common/dates/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { DayISO } from "./date-utils";
import locales from "./locales";
export { locales };

export function localeDefault(locale?: string) {
if (locale) return locale;
if (typeof navigator !== "undefined") return navigator.language;
return "en-US";
}

export function getLocale(locale?: string) {
return (
locales[localeDefault(locale).replace(/\W/g, "").toLowerCase()] ??
locales["enus"]
);
}

export function parse(date: string, locale?: string): DayISO | null {
const { order, sep } = getLocale(locale);

const parts = date.split(sep.trim());

if (parts.length !== 3) {
return null;
}

const parsed = {} as { y: number; m: number; d: number };
for (const i in parts) {
const num = parseInt(parts[i]);
if (isNaN(num)) {
return null;
}
parsed[order[i] as "y" | "m" | "d"] = num;
}

return `${padStart(parsed.y, 4)}-${padStart(parsed.m, 2)}-${padStart(parsed.d, 2)}` as DayISO;
}

export function format(date: DayISO, locale?: string) {
if (!/^\d\d\d\d-\d\d-\d\d$/g.test(date)) {
return "";
}

const { order, sep } = getLocale(locale);
const [y, m, d] = date.split("-");
const parts = { y, m, d };

return [...order].map((char) => parts[char as "y" | "m" | "d"]).join(sep);
}

function padStart(num: number, digits: number) {
return String(num).slice(-digits).padStart(digits, "0");
}
110 changes: 110 additions & 0 deletions src/common/dates/locales.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
export interface Locale {
order: `${"y" | "m" | "d"}${"y" | "m" | "d"}${"y" | "m" | "d"}`;
sep: string;
/** 0 is Sunday */
weekStart: number;
}

/**
* date-fns formats has special handling per country on whether to
* pad with `0`, included in the comments to the right of each entry.
* We unconditionally pad all dates, and since every example here has
* identical separators we can just keep track of the order and sep.
*/
export default {
af: { order: "ymd", sep: "/", weekStart: 0 }, // yyyy/MM/dd
ardz: { order: "mdy", sep: "/", weekStart: 0 }, // MM/dd/yyyy
areg: { order: "dmy", sep: "/", weekStart: 0 }, // d/MM/y
arma: { order: "mdy", sep: "/", weekStart: 1 }, // MM/dd/yyyy
arsa: { order: "mdy", sep: "/", weekStart: 0 }, // MM/dd/yyyy
artn: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/yyyy
ar: { order: "dmy", sep: "/", weekStart: 6 }, // dd/MM/yyyy
az: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.yyyy
betarask: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.y
be: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.y
bg: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/yyyy
bn: { order: "mdy", sep: "/", weekStart: 0 }, // MM/dd/yyyy
bs: { order: "dmy", sep: ". ", weekStart: 1 }, // dd. MM. yy.
ca: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/y
ckb: { order: "mdy", sep: "/", weekStart: 0 }, // MM/dd/yyyy
cs: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.yyyy
cy: { order: "dmy", sep: "/", weekStart: 0 }, // dd/MM/yyyy
da: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/y
de: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.y
el: { order: "dmy", sep: "/", weekStart: 1 }, // d/M/yy
enau: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/yyyy
enca: { order: "ymd", sep: "-", weekStart: 0 }, // yyyy-MM-dd
engb: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/yyyy
enin: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/yyyy
ennz: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/yyyy
enus: { order: "mdy", sep: "/", weekStart: 0 }, // MM/dd/yyyy
enza: { order: "ymd", sep: "/", weekStart: 0 }, // yyyy/MM/dd
eo: { order: "ymd", sep: "-", weekStart: 1 }, // yyyy-MM-dd
es: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/y
et: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.y
eu: { order: "ymd", sep: "/", weekStart: 1 }, // yy/MM/dd
fair: { order: "ymd", sep: "/", weekStart: 6 }, // yyyy/MM/dd
fi: { order: "dmy", sep: ".", weekStart: 1 }, // d.M.y
frca: { order: "ymd", sep: "-", weekStart: 0 }, // yy-MM-dd
frch: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.y
fr: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/y
fy: { order: "dmy", sep: "-", weekStart: 1 }, // dd-MM-y
gd: { order: "mdy", sep: "/", weekStart: 0 }, // MM/dd/yyyy
gl: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/y
gu: { order: "dmy", sep: "/", weekStart: 1 }, // d/M/yy
he: { order: "dmy", sep: ".", weekStart: 0 }, // d.M.y
hi: { order: "mdy", sep: "/", weekStart: 0 }, // dd/MM/yyyy
hr: { order: "dmy", sep: ". ", weekStart: 1 }, // dd. MM. y.
ht: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/y
hu: { order: "ymd", sep: ". ", weekStart: 1 }, // y. MM. dd.
hy: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.yyyy
id: { order: "dmy", sep: "/", weekStart: 1 }, // d/M/yyyy
is: { order: "dmy", sep: ".", weekStart: 1 }, // d.MM.y
itch: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.y
it: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/y
jahira: { order: "ymd", sep: "/", weekStart: 0 }, // y/MM/dd
ja: { order: "ymd", sep: "/", weekStart: 0 }, // y/MM/dd
ka: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/yyyy
kk: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.yyyy
km: { order: "dmy", sep: "/", weekStart: 0 }, // dd/MM/yyyy
kn: { order: "dmy", sep: "/", weekStart: 1 }, // d/M/yy
ko: { order: "ymd", sep: ".", weekStart: 0 }, // y.MM.dd
lb: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.yy
lt: { order: "ymd", sep: "-", weekStart: 1 }, // y-MM-dd
lv: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.y.
mk: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/yyyy
mn: { order: "ymd", sep: ".", weekStart: 1 }, // y.MM.dd
ms: { order: "dmy", sep: "/", weekStart: 1 }, // d/M/yyyy
mt: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/yyyy
nb: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.y
nlbe: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.y
nl: { order: "dmy", sep: "-", weekStart: 1 }, // dd-MM-y
nn: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.y
oc: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/y
pl: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.y
ptbr: { order: "dmy", sep: "/", weekStart: 0 }, // dd/MM/yyyy
pt: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/y
ro: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.yyyy
ru: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.y
se: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.y
sk: { order: "dmy", sep: ". ", weekStart: 1 }, // d. M. y
sl: { order: "dmy", sep: ". ", weekStart: 1 }, // d. MM. yy
sq: { order: "mdy", sep: "/", weekStart: 1 }, // MM/dd/yyyy
srlatn: { order: "dmy", sep: ". ", weekStart: 1 }, // dd. MM. yy.
sr: { order: "dmy", sep: ". ", weekStart: 1 }, // dd. MM. yy.
sv: { order: "ymd", sep: "-", weekStart: 1 }, // y-MM-dd
ta: { order: "dmy", sep: "/", weekStart: 1 }, // d/M/yy
te: { order: "dmy", sep: "-", weekStart: 0 }, // dd-MM-yy
th: { order: "dmy", sep: "/", weekStart: 0 }, // dd/MM/yyyy
tr: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.yyyy
ug: { order: "mdy", sep: "/", weekStart: 0 }, // MM/dd/yyyy
uk: { order: "dmy", sep: ".", weekStart: 1 }, // dd.MM.y
uzcyrl: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/yyyy
uz: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/yyyy
vi: { order: "dmy", sep: "/", weekStart: 1 }, // dd/MM/y
zhcn: { order: "ymd", sep: "-", weekStart: 1 }, // yy-MM-dd
zhhk: { order: "ymd", sep: "-", weekStart: 0 }, // yy-MM-dd
zhtw: { order: "ymd", sep: "-", weekStart: 1 }, // yy-MM-dd
} as {
[index: string]: Locale;
};
2 changes: 1 addition & 1 deletion src/components/ebay-calendar/calendar.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default {
description: "Locale of the date picker",
table: {
defaultValue: {
summary: "navigator.language",
summary: "navigator.language || 'en-US'",
},
},
},
Expand Down
32 changes: 16 additions & 16 deletions src/components/ebay-calendar/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import {
dateArgToISO,
fromISO,
getWeekdayInfo,
localeOverride,
offsetISO,
toISO,
type DayISO,
} from "./date-utils";
} from "../../common/dates/date-utils";
import { localeDefault } from "../../common/dates";

const DAY_UPDATE_KEYMAP = {
ArrowRight: 1,
Expand Down Expand Up @@ -62,10 +62,11 @@ interface State {
}

class Calendar extends Marko.Component<Input, State> {
declare locale?: string;

onCreate(input: Input) {
const { firstDayOfWeek, weekdayLabels } = getWeekdayInfo(
localeOverride(input.locale),
);
this.locale = input.locale;
const { firstDayOfWeek, weekdayLabels } = getWeekdayInfo(input.locale);
const todayISO = toISO(new Date());
this.state = {
focusISO: null,
Expand All @@ -84,21 +85,20 @@ class Calendar extends Marko.Component<Input, State> {
};
}

onMount() {
// recalculate on the browser in case firstDayOfWeek is not supported
const { firstDayOfWeek } = getWeekdayInfo(
localeOverride(this.input.locale),
);
this.state.firstDayOfWeek = firstDayOfWeek;
}

onInput(input: Input) {
if (input.locale !== this.locale) {
this.locale = input.locale;
const { firstDayOfWeek, weekdayLabels } = getWeekdayInfo(
input.locale,
);
this.state.firstDayOfWeek = firstDayOfWeek;
this.state.weekdayLabels = weekdayLabels;
}
if (input.todayISO) {
const newTodayISO = toISO(new Date(input.todayISO));
this.state.todayISO = newTodayISO
this.state.todayISO = newTodayISO;
this.state.baseISO = newTodayISO;
this.state.tabindexISO = newTodayISO;

}
if (input.selected) {
// If no selected times are visible, snap the view to the first one
Expand Down Expand Up @@ -289,7 +289,7 @@ class Calendar extends Marko.Component<Input, State> {

monthTitle(date: Date) {
const formatter = new Intl.DateTimeFormat(
localeOverride(this.input.locale),
localeDefault(this.input.locale),
{
month: "long",
year: "numeric",
Expand Down
2 changes: 1 addition & 1 deletion src/components/ebay-calendar/examples/linkMap.marko
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toISO } from "../date-utils";
import { toISO } from "../../../common/dates/date-utils";
static const yesterdayISO = toISO(new Date(Date.now() - 24 * 60 * 60 * 1000));
static const tomorrowISO = toISO(new Date(Date.now() + 24 * 60 * 60 * 1000));
static const linkMap = {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ebay-calendar/index.marko
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toISO } from "./date-utils"
import { toISO } from "../../common/dates/date-utils";

$ const {
numMonths = 1,
Expand Down
28 changes: 24 additions & 4 deletions src/components/ebay-date-textbox/component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import Expander from "makeup-expander";
import { type DayISO, dateArgToISO } from "../ebay-calendar/date-utils";
import { type DayISO, dateArgToISO } from "../../common/dates/date-utils";
import type { WithNormalizedProps } from "../../global";
import type { AttrString } from "marko/tags-html";
import type { Input as TextboxInput } from "../ebay-textbox/component-browser";
import { parse } from "../../common/dates";

const MIN_WIDTH_FOR_DOUBLE_PANE = 600;

Expand All @@ -10,12 +12,14 @@ interface DateTextboxInput {
rangeEnd?: Date | number | string;
locale?: string;
range?: boolean;
"todayISO"?: Date | number | string;
textbox?: Marko.RepeatableAttrTag<TextboxInput>;
todayISO?: Date | number | string;
disabled?: boolean;
"disable-before"?: Date | number | string;
"disable-after"?: Date | number | string;
"disable-weekdays"?: number[];
"disable-list"?: (Date | number | string)[];
/** @deprecated use `@textbox-input` instead */
"input-placeholder-text"?: string | [string, string];
"collapse-on-select"?: boolean;
"get-a11y-show-month-text"?: (monthName: string) => string;
Expand All @@ -25,12 +29,16 @@ interface DateTextboxInput {
"a11y-in-range-text"?: AttrString;
"a11y-range-end-text"?: AttrString;
"a11y-separator"?: string;
/** @deprecated use `@textbox-input` instead */
"floating-label"?: string | [string, string];
/** @deprecated will be default in next major */
localizeFormat?: boolean;
"on-change"?: (
event:
| { selected: DayISO | null }
| { rangeStart: DayISO | null; rangeEnd: DayISO | null },
) => void;
"on-invalid-date"?: () => void;
}

export interface Input extends WithNormalizedProps<DateTextboxInput> {}
Expand Down Expand Up @@ -91,8 +99,20 @@ class DateTextbox extends Marko.Component<Input, State> {
}

handleInputChange(index: number, { value }: { value: string }) {
const valueDate = new Date(value);
const iso = isNaN(valueDate.getTime()) ? null : dateArgToISO(valueDate);
let iso: DayISO | null;
/* next major, localizeFormat will _always_ be true */
if (this.input.localizeFormat) {
iso = parse(value, this.input.locale);
} else {
const date = new Date(value);
iso = isNaN(date.getTime()) ? null : dateArgToISO(date);
}

if (iso === null) {
this.emit("invalid-date");
return;
}

if (index === 0) {
this.state.firstSelected = iso;
} else {
Expand Down
Loading

0 comments on commit 5c71772

Please sign in to comment.