Skip to content

Commit

Permalink
i18n functions everywhere
Browse files Browse the repository at this point in the history
  • Loading branch information
schlawg committed Nov 8, 2024
1 parent 29b0acc commit 2857dcd
Show file tree
Hide file tree
Showing 149 changed files with 3,609 additions and 3,549 deletions.
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

0 comments on commit 2857dcd

Please sign in to comment.