Skip to content

Commit

Permalink
feat(date-textbox): localize date placeholders (#2191)
Browse files Browse the repository at this point in the history
  • Loading branch information
LuLaValva authored Jun 24, 2024
1 parent 02c16ff commit 1290638
Show file tree
Hide file tree
Showing 21 changed files with 826 additions and 294 deletions.
5 changes: 5 additions & 0 deletions .changeset/brown-points-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ebay/ebayui-core": major
---

feat(date-textbox): localize date placeholders
27 changes: 27 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"chai-dom": "^1.12.0",
"cheerio": "^1.0.0-rc.12",
"chromedriver": "^125",
"cldr-dates-full": "^45.0.0",
"coveralls": "^3.1.1",
"css-loader": "^7",
"del": "^7.1.0",
Expand Down
177 changes: 177 additions & 0 deletions scripts/generate-date-info.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import fs from "fs";
import weekData from "cldr-core/supplemental/weekData.json" with { type: "json" };
import availableLocales from "cldr-core/availableLocales.json" with { type: "json" };
import defaultContent from "cldr-core/defaultContent.json" with { type: "json" };

const output = {
_: {
y: "Y",
m: "M",
d: "D",
o: "ymd",
s: ["-", "-"],
w: 0,
},
};

const firstDays = weekData.supplemental.weekData.firstDay;
const dayNums = {
sun: 0,
mon: 1,
tue: 2,
wed: 3,
thu: 4,
fri: 5,
sat: 6,
};

const modernLocales = availableLocales.availableLocales.modern;
const defaultCountries = Object.fromEntries(
defaultContent.defaultContent.map((locale) => {
const index = locale.lastIndexOf("-");
return [locale.slice(0, index), locale.slice(index + 1)];
}),
);

const dirPath = "node_modules/cldr-dates-full/main";
const files = fs.readdirSync(dirPath);

for (const locale of files) {
if (!modernLocales.includes(locale)) {
continue;
}
const parts = locale.split("-");
let editing = output;
for (const part of parts) {
editing[part.toLowerCase()] ??= {};
editing = editing[part.toLowerCase()];
}
editing._ ??= {};
editing = editing._;

const dateFields = JSON.parse(
fs.readFileSync(`${dirPath}/${locale}/dateFields.json`),
);
const caGeneric = JSON.parse(
fs.readFileSync(`${dirPath}/${locale}/ca-generic.json`),
);

const getLetter = (time) =>
dateFields.main[locale].dates.fields[time].displayName
.charAt(0)
.toUpperCase();
editing.y = getLetter("year-narrow");
editing.m = getLetter("month-narrow");
editing.d = getLetter("day-narrow");
const { order, seps } = formatFromAvailable(
caGeneric.main[locale].dates.calendars.generic.dateTimeFormats
.availableFormats,
);
editing.o = order;
editing.s = seps;
if (parts[1] && firstDays[parts[1]]) {
editing.w = dayNums[firstDays[parts[1]]];
} else if (
defaultCountries[locale] &&
firstDays[defaultCountries[locale]]
) {
editing.w = dayNums[firstDays[defaultCountries[locale]]];
} else {
editing.w = 0;
}
}

function formatFromAvailable(availableFormats) {
let { yyyyMd } = availableFormats;
yyyyMd = yyyyMd.replace(/G/g, "").trim().toLowerCase();
let order = "";
const seps = [];
for (const char of yyyyMd) {
if (char === "y" || char === "m" || char === "d") {
if (!order.length || order[order.length - 1] !== char) {
order += char;
seps.push("");
}
} else {
seps[seps.length - 1] += char;
}
}
if (seps.at(-1) === "") {
seps.pop();
}

return { order, seps };
}

/**
* if any child `_` matches _exactly_ with its parent's `_`, prune it
*/
function pruneDuplication(obj, parent, parentKey) {
if (typeof obj === "object" && obj !== null) {
for (const key in obj) {
if (
typeof obj[key] === "object" &&
obj[key] !== null &&
key !== "_"
) {
pruneDuplication(obj[key], obj, key);
}
}
}
if (parent) {
if (JSON.stringify(obj._) === JSON.stringify(parent._)) {
delete parent[parentKey];
} else if (parent._) {
for (const field in obj._) {
if (
JSON.stringify(obj._[field]) ===
JSON.stringify(parent._[field])
) {
delete obj._[field];
}
}
}
}
}

pruneDuplication(output);

/**
* Manual Overrides
*/
output.uz._.s = [".", "."];
output.hy._.s = [".", "."];
output.bg._.s = [".", "."];
output.ar._.s = ["/", "/"];
delete output.ar._.d;
delete output.ar._.m;
delete output.ar._.y;
output.yo._.d = "ọ";

fs.writeFileSync(
"src/common/dates/locale-info.ts",
`// GENERATED FILE - DO NOT MODIFY
// Information pulled from cldr-core, see \`scripts/generate-date-info.js\` for more details
export interface LocaleInfo {
/** order of year, month, and day in date format */
o: \`\${"y" | "m" | "d"}\${"y" | "m" | "d"}\${"y" | "m" | "d"}\`;
/** separators between year, month, and day */
s: string[];
/** first day of week, 0 is Sunday */
w: number;
/** letter representing day */
d: string;
/** letter representing month */
m: string;
/** letter representing year */
y: string;
}
export interface Locales {
_: Partial<LocaleInfo>;
[country: string]: Locales | Partial<LocaleInfo>;
}
export default ${JSON.stringify(output)} as Locales;`,
);
2 changes: 1 addition & 1 deletion src/common/dates/date-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function findFirstDayOfWeek(localeName: string): number {
export function getWeekdayInfo(localeName?: string) {
localeName = localeDefault(localeName);
const locale = getLocale(localeName);
const firstDayOfWeek = locale.weekStart;
const firstDayOfWeek = locale.w;

const weekdayLabelFormatter = new Intl.DateTimeFormat(localeName, {
weekday: "short",
Expand Down
102 changes: 89 additions & 13 deletions src/common/dates/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,59 @@
import type { DayISO } from "./date-utils";
import locales from "./locales";
export { locales };
import localeInfo, { LocaleInfo, Locales } from "./locale-info";

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"]
);
const localeCache = new Map<string | undefined, LocaleInfo>();

export function getLocale(locale?: string): LocaleInfo {
if (!locale) {
locale = localeDefault();
}

if (localeCache.has(locale)) {
return localeCache.get(locale) as LocaleInfo;
}

let info = { ...localeInfo._ } as LocaleInfo;

let curr = localeInfo;
const parts = locale.split("-");
for (let part of parts) {
part = part.toLowerCase();
if (curr[part]) {
curr = curr[part] as Locales;
info = { ...info, ...curr._ };
} else {
break;
}
}

localeCache.set(locale, info);
return info;
}

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

const parts = date.split(sep.trim());
const parts: string[] = [];
const firstEnd = value.indexOf(sep[0].trim());
parts.push(value.slice(0, firstEnd).trim());
const secondEnd = value.indexOf(sep[1].trim(), firstEnd + 1);
parts.push(value.slice(firstEnd + 1, secondEnd).trim());
if (sep[2]) {
const thirdEnd = value.indexOf(sep[2].trim(), secondEnd + 1);
parts.push(
value
.slice(secondEnd + 1, thirdEnd === -1 ? undefined : thirdEnd)
.trim(),
);
} else {
parts.push(value.slice(secondEnd + 1).trim());
}

if (parts.length !== 3) {
return null;
Expand All @@ -33,19 +68,60 @@ export function parse(date: string, locale?: string): DayISO | null {
parsed[order[i] as "y" | "m" | "d"] = num;
}

return `${padStart(parsed.y, 4)}-${padStart(parsed.m, 2)}-${padStart(parsed.d, 2)}` as DayISO;
if (parsed.y < 100) {
// 2-digit year
// if year is less than 50, assume 2000s, otherwise 1900s
if (parsed.y < 50) {
parsed.y += 2000;
} else {
parsed.y += 1900;
}
}

const iso =
`${padStart(parsed.y, 4)}-${padStart(parsed.m, 2)}-${padStart(parsed.d, 2)}` as DayISO;
if (isNaN(new Date(iso).getTime())) {
return null;
}
return iso;
}

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 { o: order, s: sep } = getLocale(locale);
const [y, m, d] = date.split("-");
const parts = { y, m, d };
let result = "";
for (let i = 0; i < 3; i++) {
result += parts[order[i] as "y" | "m" | "d"];
if (sep[i]) {
result += sep[i];
}
}

return result;
}

export function placeholder(locale?: string) {
const { o: order, s: sep, y, m, d } = getLocale(locale);
const parts = {
y: `${y}${y}${y}${y}`,
m: `${m}${m}`,
d: `${d}${d}`,
};

let result = "";
for (let i = 0; i < 3; i++) {
result += parts[order[i] as "y" | "m" | "d"];
if (sep[i]) {
result += sep[i];
}
}

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

function padStart(num: number, digits: number) {
Expand Down
Loading

0 comments on commit 1290638

Please sign in to comment.