Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add localized formatting #2160

Merged
merged 18 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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