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); } } }); });