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

i18n functions everywhere #16372

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 59 additions & 55 deletions ui/.build/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ import { transform } from 'esbuild';

type Plural = { [key in 'zero' | 'one' | 'two' | 'few' | 'many' | 'other']?: string };
type Dict = Map<string, string | Plural>;
type TranslationState = {
dicts: Map<string, Dict>;
locales: string[];
cats: string[];
siteInit: string | undefined;
};

let dicts: Map<string, Dict> = new Map();
let locales: string[], cats: string[];
let watchTimeout: NodeJS.Timeout | undefined;
const current: TranslationState = { dicts: new Map(), locales: [], cats: [], siteInit: undefined };
const i18nWatch: fs.FSWatcher[] = [];
const isFormat = /%(?:[\d]\$)?s/;

let watchTimeout: NodeJS.Timeout | undefined;

export function stopI18n(): void {
clearTimeout(watchTimeout);
Expand All @@ -26,13 +31,25 @@ export function stopI18n(): void {
export async function i18n(isBoot = true): Promise<void> {
if (!env.i18n) return;

[locales, cats] = (
[current.locales, current.cats] = (
await Promise.all([
globArray('*.xml', { cwd: path.join(env.i18nDestDir, 'site'), absolute: false }),
globArray('*.xml', { cwd: env.i18nSrcDir, absolute: false }),
])
).map(list => list.map(x => x.split('.')[0]));

current.siteInit = await min(
` window.i18n =
function(k) {
for (let v of Object.values(window.i18n)) {
if (v[k]) return v[k];
}
return k;
};` +
current.cats.map(cat => `window.i18n.${cat}`).join('=') +
`= new Proxy({}, { get: (_, k) => d(k) });`,
);

await compileTypings();
compileJavascripts(isBoot); // no await

Expand All @@ -43,7 +60,7 @@ export async function i18n(isBoot = true): Promise<void> {
watchTimeout = setTimeout(() => i18n(false), 2000);
};
i18nWatch.push(fs.watch(env.i18nSrcDir, onChange));
for (const d of cats) {
for (const d of current.cats) {
await fs.promises.mkdir(path.join(env.i18nDestDir, d)).catch(() => {});
i18nWatch.push(fs.watch(path.join(env.i18nDestDir, d), onChange));
}
Expand All @@ -55,30 +72,31 @@ async function compileTypings(): Promise<void> {
fs.promises.stat(typingsPathname).catch(() => undefined),
fs.promises.mkdir(env.i18nJsDir).catch(() => {}),
]);
const catStats = await Promise.all(cats.map(d => updated(d)));
const catStats = await Promise.all(current.cats.map(d => updated(d)));

if (!tstat || catStats.some(x => x)) {
env.log(`Building ${c.grey('i18n')}`);
dicts = new Map(
current.dicts = new Map(
zip(
cats,
current.cats,
await Promise.all(
cats.map(d => fs.promises.readFile(path.join(env.i18nSrcDir, `${d}.xml`), 'utf8').then(parseXml)),
current.cats.map(cat =>
fs.promises.readFile(path.join(env.i18nSrcDir, `${cat}.xml`), 'utf8').then(parseXml),
),
),
),
);
await fs.promises.writeFile(
typingsPathname,
tsPrelude +
[...dicts]
[...current.dicts]
.map(
([cat, dict]) =>
` ${cat}: {\n` +
[...dict.entries()]
.map(([k, v]) => {
if (!/^[A-Za-z_]\w*$/.test(k)) k = `'${k}'`;
const tpe =
typeof v !== 'string' ? 'I18nPlural' : isFormat.test(v) ? 'I18nFormat' : 'string';
const tpe = typeof v !== 'string' ? 'I18nPlural' : 'I18nString';
const comment = typeof v === 'string' ? v.split('\n')[0] : v['other']?.split('\n')[0];
return ` /** ${comment} */\n ${k}: ${tpe};`;
})
Expand All @@ -97,11 +115,11 @@ async function compileTypings(): Promise<void> {
}

async function compileJavascripts(dirty: boolean = true): Promise<void> {
for (const cat of cats) {
for (const cat of current.cats) {
const u = await updated(cat);
if (u) await writeJavascript(cat, undefined, u);
await Promise.all(
locales.map(locale =>
current.locales.map(locale =>
updated(cat, locale).then(xstat => {
if (!u && !xstat) return;
if (!dirty) env.log(`Building ${c.grey('i18n')}`);
Expand All @@ -115,44 +133,34 @@ async function compileJavascripts(dirty: boolean = true): Promise<void> {
}

async function writeJavascript(cat: string, locale?: string, xstat: fs.Stats | false = false) {
if (!dicts.has(cat))
dicts.set(
if (!current.dicts.has(cat))
current.dicts.set(
cat,
await fs.promises.readFile(path.join(env.i18nSrcDir, `${cat}.xml`), 'utf8').then(parseXml),
);

const lang = locale?.split('-')[0] ?? 'en';
const translations = new Map([
...dicts.get(cat)!,
...current.dicts.get(cat)!,
...(locale
? await fs.promises
.readFile(path.join(env.i18nDestDir, cat, `${locale}.xml`), 'utf-8')
.catch(() => '')
.then(parseXml)
: []),
]);
const lang = locale?.split('-')[0];
const jsInit =
cat !== 'site'
? ''
: siteInit +
'window.i18n.quantity=' +
(jsQuantity.find(({ l }) => l.includes(lang ?? ''))?.q ?? `o=>o==1?'one':'other'`) +
';';
const code =
jsPrelude +
jsInit +
`let i=window.i18n.${cat}={};` +
[...translations]
.map(
([k, v]) =>
`i['${k}']=` +
(typeof v !== 'string'
? `p(${JSON.stringify(v)})`
: isFormat.test(v)
? `s(${JSON.stringify(v)})`
: JSON.stringify(v)),
)
.join(';') +

let code = jsPrelude;
if (cat === 'site') {
code +=
current.siteInit +
'window.i18n.quantity=' +
(jsQuantity.find(({ l }) => l.includes(lang))?.q ?? `o=>o==1?'one':'other'`) +
';';
}
code +=
`let i=window.i18n.${cat}=new Proxy({},{get:(o,k)=>k in o?typeof o[k]=='object'?p(o[k]):s(o[k]):key(k)});` +
[...translations].map(([k, v]) => `i['${k}']=${JSON.stringify(v)}`).join(';') +
'})()';

const filename = path.join(env.i18nJsDir, `${cat}.${locale ?? 'en-GB'}.js`);
Expand Down Expand Up @@ -197,8 +205,8 @@ async function min(js: string): Promise<string> {
}

const tsPrelude = `// Generated
interface I18nFormat {
(...args: (string | number)[]): string; // formatted
interface I18nString {
(...args: (string | number)[]): string; // singular & formatted
asArray: <T>(...args: T[]) => (T | string)[]; // vdom
}
interface I18nPlural {
Expand All @@ -213,9 +221,14 @@ interface I18n {
const jsPrelude =
'"use strict";(()=>{' +
(await min(
// s(...) is the standard format function, p(...) is the plural format function.
// both have an asArray method for vdom.
`function p(t) {
// d(...) is a constructor for dummy objects that just return keys
// s(...) is the standard format constructor, p(...) is the plural format constructor.
// all have an asArray method (for vdom).
` function d(k) {
const f = () => k;
return (f.asArray = () => [k]), f;
}
function p(t) {
let r = (n, ...e) => l(o(t, n), n, ...e).join('');
return (r.asArray = (n, ...e) => l(o(t, n), ...e)), r;
}
Expand All @@ -241,15 +254,6 @@ const jsPrelude =
}`,
));

const siteInit = await min(
`window.i18n = function(k) {
for (let v of Object.values(window.i18n)) {
if (v[k]) return v[k];
return k;
}
}`,
);

const jsQuantity = [
{
l: ['fr', 'ff', 'kab', 'co', 'ak', 'am', 'bh', 'fil', 'tl', 'guw', 'hi', 'ln', 'mg', 'nso', 'ti', 'wa'],
Expand Down
Loading
Loading