refactor(i18n): bundle catalogs under assets/i18n/<locale>/ (T-462)

Move the catalogs out of lib/kernel/src/i18n/catalog/ into assets/i18n/, and
switch the layout from a `{namespace}_{locale}.json` filename to a per-locale
DIRECTORY: assets/i18n/en_us/{namespace}.json. Adding a language is now just a
new folder (assets/i18n/nl_nl/, nl_be, en_eu, …) of the same namespace files —
no renames — which is cleaner to maintain and mirrors how an external
extension ships its own catalog. AssetCatalogLoader/FileCatalogLoader resolve
`<root>/<locale>/<namespace>.json`; pubspec bundles the locale folder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-19 15:54:14 +02:00
co-authored by Claude Opus 4.8
parent 37fa255a63
commit 1bdfd1be1c
31 changed files with 40 additions and 17 deletions
@@ -29,7 +29,7 @@ class _MapAssetBundle extends CachingAssetBundle {
void main() {
group('AssetCatalogLoader', () {
test('returns parsed JSON for a present asset', () async {
final bundle = _MapAssetBundle({'lib/kernel/src/i18n/catalog/welcome_en_us.json': '{"title":{"translation":"Hi"}}'});
final bundle = _MapAssetBundle({'assets/i18n/en_us/welcome.json': '{"title":{"translation":"Hi"}}'});
final loader = AssetCatalogLoader(bundle: bundle);
final r = await loader.load('welcome', const Locale('en', 'US'));
expect(r['title'], isA<Map>());
@@ -42,19 +42,19 @@ void main() {
});
test('returns an empty map on malformed JSON (FormatException catch)', () async {
final bundle = _MapAssetBundle({'lib/kernel/src/i18n/catalog/welcome_en_us.json': 'not json at all'});
final bundle = _MapAssetBundle({'assets/i18n/en_us/welcome.json': 'not json at all'});
final loader = AssetCatalogLoader(bundle: bundle);
expect(await loader.load('welcome', const Locale('en', 'US')), isEmpty);
});
test('returns an empty map when the asset is blank', () async {
final bundle = _MapAssetBundle({'lib/kernel/src/i18n/catalog/welcome_en_us.json': ' \n'});
final bundle = _MapAssetBundle({'assets/i18n/en_us/welcome.json': ' \n'});
final loader = AssetCatalogLoader(bundle: bundle);
expect(await loader.load('welcome', const Locale('en', 'US')), isEmpty);
});
test('returns an empty map when JSON parses to a non-object', () async {
final bundle = _MapAssetBundle({'lib/kernel/src/i18n/catalog/welcome_en_us.json': '[1, 2, 3]'});
final bundle = _MapAssetBundle({'assets/i18n/en_us/welcome.json': '[1, 2, 3]'});
final loader = AssetCatalogLoader(bundle: bundle);
expect(await loader.load('welcome', const Locale('en', 'US')), isEmpty);
});
@@ -65,6 +65,8 @@ void main() {
setUp(() async {
tmp = await Directory.systemTemp.createTemp('clide-catalog-');
// Catalogs live under a per-locale subdir (<root>/<locale>/<ns>.json).
await Directory('${tmp.path}/en_us').create();
});
tearDown(() async {
@@ -72,7 +74,7 @@ void main() {
});
test('reads and parses a present file', () async {
await File('${tmp.path}/welcome_en_us.json').writeAsString('{"k":{"translation":"v"}}');
await File('${tmp.path}/en_us/welcome.json').writeAsString('{"k":{"translation":"v"}}');
final loader = FileCatalogLoader(rootDir: tmp);
final r = await loader.load('welcome', const Locale('en', 'US'));
expect(r['k'], isA<Map>());
@@ -84,13 +86,13 @@ void main() {
});
test('returns an empty map on malformed JSON (FormatException catch)', () async {
await File('${tmp.path}/welcome_en_us.json').writeAsString('garbage');
await File('${tmp.path}/en_us/welcome.json').writeAsString('garbage');
final loader = FileCatalogLoader(rootDir: tmp);
expect(await loader.load('welcome', const Locale('en', 'US')), isEmpty);
});
test('returns an empty map when the file is blank', () async {
await File('${tmp.path}/welcome_en_us.json').writeAsString(' ');
await File('${tmp.path}/en_us/welcome.json').writeAsString(' ');
final loader = FileCatalogLoader(rootDir: tmp);
expect(await loader.load('welcome', const Locale('en', 'US')), isEmpty);
});