83f52937cf
- Root cause of the raw keys (web_accounts_load_failed, the settle missing state) on main: Vitest 5's file-system module cache (on by default) skips only modules whose source contains the literal `import.meta.glob(`. catalog.ts wrote `import.meta.glob<…>(`, so its transform was cached by its own content and kept the glob result from before a feature added its i18n/*.json; the main checkout's node_modules/.vitest-cache served that stale catalog until a config change invalidated it. Fresh worktrees passed. - catalog.ts: glob without a type argument (typed by annotation), with the reason. - catalog.test.ts: the loaded feature catalogs must equal the i18n files on disk. - A string two features render moves to the shared catalog: web_accounts_load_failed (accounts/PageStates, also rendered by Home) is now web_ledger_load_failed in src/i18n/web-strings.csv; sv.json/en.json regenerated. - vectors.test.ts: suggest_category is PORTED and runs against features/expenses/suggest. - fakeServer: bodies by Content-Type (JSON parsed, text/* as text, else raw bytes); ProfilePage's picture test asserts the uploaded bytes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
82 lines
3.7 KiB
TypeScript
82 lines
3.7 KiB
TypeScript
import { readdirSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { featureCatalogs, fill, LOADED_FEATURE_CATALOGS, mergeCatalogs, translate, translatePlural } from './catalog';
|
|
import en from './en.json';
|
|
import { languageOf } from './language';
|
|
import sv from './sv.json';
|
|
|
|
describe('i18n catalogs', () => {
|
|
it('have the same keys in sv and en', () => {
|
|
expect(Object.keys(sv).sort()).toEqual(Object.keys(en).sort());
|
|
});
|
|
it('hold no Android placeholders after conversion', () => {
|
|
for (const value of [...Object.values(sv), ...Object.values(en)]) {
|
|
expect(value).not.toMatch(/%\d*\$?[sd]/);
|
|
}
|
|
});
|
|
it('fills positional placeholders', () => {
|
|
expect(fill('{1} och {2}', ['Jacob', 'Anna'])).toBe('Jacob och Anna');
|
|
expect(fill('{2} {1}', [1])).toBe('{2} 1');
|
|
expect(translate('sv', 'common_and', ['Jacob', 'Anna'])).toBe('Jacob och Anna');
|
|
expect(translate('en', 'balance_partner_owes_you', ['Anna'])).toBe('Anna owes you');
|
|
expect(translate('sv', 'common_percent', ['34,8'])).toBe('34,8 %');
|
|
});
|
|
it('chooses plural forms', () => {
|
|
expect(translatePlural('sv', 'sync_pending', 1)).toBe('1 ändring väntar på synk');
|
|
expect(translatePlural('sv', 'sync_pending', 2)).toBe('2 ändringar väntar på synk');
|
|
expect(translatePlural('en', 'ledger_settlement_accounts', 3)).toBe('3 accounts');
|
|
});
|
|
it('maps browser languages', () => {
|
|
expect(languageOf('sv-SE')).toBe('sv');
|
|
expect(languageOf('sv')).toBe('sv');
|
|
expect(languageOf('en-GB')).toBe('en');
|
|
expect(languageOf('de')).toBe('en');
|
|
expect(languageOf(undefined)).toBe('en');
|
|
});
|
|
});
|
|
|
|
describe('feature catalogs', () => {
|
|
it('merge web_ keys from features without replacing shared keys', () => {
|
|
const features = featureCatalogs({
|
|
'../features/settle/i18n/sv.json': { default: { web_settle_hint: 'Tips', common_save: 'Nej' } },
|
|
'../features/settle/i18n/en.json': { default: { web_settle_hint: 'Hint' } },
|
|
'../features/settle/i18n/notes.json': { default: { web_x: 'ignored' } },
|
|
});
|
|
expect(Object.keys(features)).toEqual(['settle']);
|
|
const merged = mergeCatalogs({ common_save: 'Spara' }, [features.settle?.sv]);
|
|
expect(merged).toEqual({ common_save: 'Spara', web_settle_hint: 'Tips' });
|
|
});
|
|
|
|
it('loads every feature catalog on disk (no stale transform of catalog.ts)', () => {
|
|
const featuresDir = join(process.cwd(), 'src', 'features'); // Vitest runs from web/
|
|
const onDisk = readdirSync(featuresDir, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.flatMap((entry) => {
|
|
let files: string[] = [];
|
|
try {
|
|
files = readdirSync(join(featuresDir, entry.name, 'i18n'));
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
}
|
|
return files.filter((file) => /^(sv|en)\.json$/.test(file)).map((file) => `${entry.name}/${file}`);
|
|
})
|
|
.sort();
|
|
const loaded = Object.entries(LOADED_FEATURE_CATALOGS)
|
|
.flatMap(([feature, catalogs]) => Object.keys(catalogs).map((language) => `${feature}/${language}.json`))
|
|
.sort();
|
|
expect(loaded).toEqual(onDisk);
|
|
});
|
|
|
|
it('every loaded feature has the same web_ keys in sv and en, none shared', () => {
|
|
for (const [feature, catalogs] of Object.entries(LOADED_FEATURE_CATALOGS)) {
|
|
const svKeys = Object.keys(catalogs.sv ?? {}).sort();
|
|
expect(Object.keys(catalogs.en ?? {}).sort(), feature).toEqual(svKeys);
|
|
for (const key of svKeys) {
|
|
expect(key.startsWith('web_'), `${feature}: ${key}`).toBe(true);
|
|
expect(key in sv, `${feature}: ${key} is a shared key`).toBe(false);
|
|
}
|
|
}
|
|
});
|
|
});
|