W0 web foundation: Vite + React SPA under /app/, sync-pull ledger, auth, tokens, shared components
Build Kvitt images / build (push) Failing after 1h14m5s
Build Kvitt images / build (push) Failing after 1h14m5s
web/ is the browser client's scaffold: Vite 8, React 19, TypeScript 5.9 strict, React Router 7 (basename /app, every fixed route with a lazy placeholder page), TanStack Query 5 (refetch on focus and every 60 s), openapi-typescript types generated from contracts/openapi.yaml with openapi-fetch. Reads go through GET /sync (no REST reads exist for the ledger, SPEC §7): useLedger() pulls from cursor 0 and then incrementally, restarting on a new epoch or cursor_ahead. Login pairs the browser as "Web · <browser>" with an invite code (E-3); every call sends the SPEC §14.3.1 headers, a 401 signs out. kvitt-domain's split, balances, allocation and settlement rules are ported to web/src/lib and pass every case of contracts/test-vectors. Design tokens from design-spec §2 in tokens.css (light-dark()), Material Symbols Rounded (Apache-2.0) as generated inline SVG, sv/en catalogs generated from docs/ux/strings.csv plus web-strings.csv. Gate: cd web && npm run check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
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('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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user