wayBetter for X
This commit is contained in:
commit
434f193217
720 changed files with 147465 additions and 0 deletions
129
apps/desktop/tests/i18n.test.ts
Normal file
129
apps/desktop/tests/i18n.test.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import en from '../locales/en.json' with { type: 'json' };
|
||||
import kitEn from '../../../waybetter/packages/waybetter/locales/en.json' with { type: 'json' };
|
||||
import languages from '../locales/languages.json' with { type: 'json' };
|
||||
import { direction, formatMessage } from '../../../waybetter/packages/runtime/src/i18n.ts';
|
||||
import { resolveLocale } from '../../../waybetter/packages/shell/src/frame/language.ts';
|
||||
|
||||
const desktop = fileURLToPath(new URL('..', import.meta.url));
|
||||
const root = join(desktop, '../..');
|
||||
const messages: Record<string, string> = { ...kitEn.messages, ...en.messages };
|
||||
|
||||
/**
|
||||
* The UI languages X's own web client offers (its locale chunk loader,
|
||||
* `loadLanguage.registerChunkLoader` in abs.twimg.com/responsive-web/client-web/main.*.js, read
|
||||
* 2026-09-29), as catalog tags: X's `msa`/`no`/`zh-cn`/`zh-tw` are `ms`/`nb`/`zh-Hans`/`zh-Hant`,
|
||||
* and its pseudo-locales `en-ss`/`en-xx` are left out. docs/TRANSLATING.md names the source.
|
||||
*/
|
||||
const X_LANGUAGES = [
|
||||
'ar', 'ar-x-fm', 'bg', 'bn', 'ca', 'cs', 'da', 'de', 'el', 'en', 'en-GB', 'es', 'eu', 'fa', 'fi', 'fil', 'fr', 'ga', 'gl', 'gu',
|
||||
'ha', 'he', 'hi', 'hr', 'hu', 'id', 'ig', 'it', 'ja', 'kn', 'ko', 'mr', 'ms', 'nb', 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', 'sr',
|
||||
'sv', 'ta', 'th', 'tr', 'uk', 'ur', 'vi', 'yo', 'zh-Hans', 'zh-Hant',
|
||||
];
|
||||
|
||||
function sources(dir: string, pattern: RegExp): string[] {
|
||||
const out: string[] = [];
|
||||
for (const name of readdirSync(dir)) {
|
||||
const path = join(dir, name);
|
||||
if (statSync(path).isDirectory()) out.push(...sources(path, pattern));
|
||||
else if (pattern.test(name)) out.push(path);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
test('every catalog passes scripts/i18n-check.mjs and the generated files are current', () => {
|
||||
const run = spawnSync(process.execPath, [join(root, 'scripts/i18n-check.mjs')], { encoding: 'utf8' });
|
||||
assert.equal(run.status, 0, `${run.stderr}\n${run.stdout.split('\n').slice(-3).join('\n')}`);
|
||||
});
|
||||
|
||||
test('every literal key the app looks up exists in the English source catalog', () => {
|
||||
// t('…'), tx('…'), coreText('…') in the shell, panel, popover and X runtime; tr/trf(app, "…") natively.
|
||||
const call = /\b(?:t|tx|coreText|i18n\.t|i18n\.tx)\(\s*(['"])([a-z][A-Za-z0-9_.-]*)\1/g;
|
||||
const native = /\btrf?\(\s*&?app,\s*"([a-z][A-Za-z0-9_.-]*)"/g;
|
||||
const missing: string[] = [];
|
||||
let seen = 0;
|
||||
for (const file of sources(join(desktop, 'src'), /\.(ts|tsx)$/)) {
|
||||
for (const match of readFileSync(file, 'utf8').matchAll(call)) {
|
||||
seen += 1;
|
||||
if (!(match[2]! in messages)) missing.push(`${file.slice(desktop.length)}: ${match[2]}`);
|
||||
}
|
||||
}
|
||||
for (const file of sources(join(desktop, 'src-tauri/src'), /\.rs$/)) {
|
||||
for (const match of readFileSync(file, 'utf8').matchAll(native)) {
|
||||
seen += 1;
|
||||
if (!match[1]!.startsWith('native.') || !(match[1]! in messages)) missing.push(`${file.slice(desktop.length)}: ${match[1]}`);
|
||||
}
|
||||
}
|
||||
// (The panel's own lookups are the framework's: waybetter/packages/shell/tests/panel.test.ts.)
|
||||
assert.ok(seen > 250, `${seen} lookups found (the scan pattern is stale)`);
|
||||
assert.deepEqual(missing, []);
|
||||
});
|
||||
|
||||
test('languages.json lists every catalog, and the catalogs are exactly the languages X offers', () => {
|
||||
const files = readdirSync(join(desktop, 'locales')).filter(file => file.endsWith('.json') && !['languages.json', 'en.meta.json'].includes(file));
|
||||
const codes = files.map(file => file.replace(/\.json$/, '')).sort();
|
||||
assert.deepEqual(codes, [...X_LANGUAGES].sort());
|
||||
assert.deepEqual(languages.map(language => language.code).sort(), codes);
|
||||
assert.equal(languages[0]!.code, 'en', 'English first');
|
||||
for (const language of languages) {
|
||||
const catalog = JSON.parse(readFileSync(join(desktop, 'locales', `${language.code}.json`), 'utf8'));
|
||||
assert.equal(language.name, catalog.name, language.code);
|
||||
assert.equal(language.englishName, catalog.englishName, language.code);
|
||||
assert.equal(language.status, catalog.status, language.code);
|
||||
assert.equal(language.dir, direction(language.code), language.code);
|
||||
assert.ok(language.coverage > 0.99 && language.coverage <= 1, `${language.code} coverage ${language.coverage}`);
|
||||
}
|
||||
for (const rtl of ['ar', 'ar-x-fm', 'fa', 'he', 'ur']) assert.equal(languages.find(language => language.code === rtl)?.dir, 'rtl', rtl);
|
||||
});
|
||||
|
||||
test('plural messages render in languages with one form and with many', () => {
|
||||
const load = (code: string) => JSON.parse(readFileSync(join(desktop, 'locales', `${code}.json`), 'utf8')).messages as Record<string, string>;
|
||||
const key = 'fonts.weightCount';
|
||||
assert.equal(formatMessage(messages[key]!, { count: 1 }, 'en'), '1 weight');
|
||||
assert.equal(formatMessage(messages[key]!, { count: 2 }, 'en'), '2 weights');
|
||||
for (const code of ['ja', 'vi', 'ar', 'ru', 'tr']) {
|
||||
const text = formatMessage(load(code)[key] ?? messages[key]!, { count: 5 }, code);
|
||||
assert.match(text, /5|٥/, `${code}: ${text}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('RTL styles use logical properties (no physical left/right margins or paddings)', () => {
|
||||
const physical = /(?:margin|padding)-(?:left|right)\s*:|(?:^|[\s;{])(?:border-(?:left|right))\s*:|text-align\s*:\s*(?:left|right)/m;
|
||||
const offenders: string[] = [];
|
||||
for (const file of sources(join(desktop, 'src'), /\.css$/)) {
|
||||
readFileSync(file, 'utf8').split('\n').forEach((line, index) => {
|
||||
if (physical.test(line) && !line.includes('rtl-ok')) offenders.push(`${file.slice(desktop.length)}:${index + 1}`);
|
||||
});
|
||||
}
|
||||
assert.deepEqual(offenders, []);
|
||||
});
|
||||
|
||||
test('language tiers: 17 supported languages (en-GB with English), the rest community; automatic choices never land on a community language', () => {
|
||||
const codes = languages.map(language => language.code);
|
||||
const supported = languages.filter(language => language.tier === 'supported').map(language => language.code);
|
||||
assert.deepEqual([...supported].sort(), ['ar', 'de', 'en', 'en-GB', 'es', 'fr', 'hi', 'id', 'it', 'ja', 'ko', 'nl', 'pt', 'ru', 'th', 'tr', 'vi', 'zh-Hant'].sort());
|
||||
assert.equal(languages.length - supported.length, 33);
|
||||
const config = JSON.parse(readFileSync(join(root, 'waybetter.i18n.json'), 'utf8'));
|
||||
assert.deepEqual([...config.supported].sort(), [...supported].sort(), 'languages.json is generated from waybetter.i18n.json');
|
||||
for (const code of codes) {
|
||||
for (const preference of ['system', 'site'] as const) {
|
||||
const locale = resolveLocale(preference, preference === 'site' ? code : null, [code], codes, supported);
|
||||
assert.ok(supported.includes(locale), `${preference} ${code} -> ${locale}`);
|
||||
}
|
||||
assert.equal(resolveLocale(code, null, ['de'], codes, supported), code, `explicit ${code}`);
|
||||
}
|
||||
const auto = (tag: string) => resolveLocale('system', null, [tag], codes, supported);
|
||||
assert.equal(auto('pt-BR'), 'pt');
|
||||
assert.equal(auto('pt-PT'), 'pt');
|
||||
assert.equal(auto('zh-TW'), 'zh-Hant');
|
||||
assert.equal(auto('zh-HK'), 'zh-Hant');
|
||||
assert.equal(auto('en-AU'), 'en');
|
||||
assert.equal(auto('en-GB'), 'en-GB');
|
||||
assert.equal(auto('sv-SE'), 'en', 'Swedish is a community translation');
|
||||
assert.equal(auto('zh-CN'), 'en', 'Simplified Chinese is a community translation');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue