-
-
Notifications
You must be signed in to change notification settings - Fork 364
[Feature] Linked Files #770
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
Getting file link to all key not just first level Getting file link in data from files datas
WalkthroughAdds support for linked JSON translation files via a new LinkedFileResolver and FileLoader abstraction, updates AssetLoader/RootBundleAssetLoader to use them (with IO/rootBundle factories), updates the audit CLI, adds many i18n fixtures and tests for linked/missing/cyclic files, and documents the ":/" linking syntax in the README. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant App
participant Controller as EasyLocalizationController
participant Loader as RootBundleAssetLoader
participant Resolver as JsonLinkedFileResolver
participant FileLoader as RootBundleFileLoader
App->>Controller: loadTranslations()
Controller->>Loader: load(path, locale)
Loader->>FileLoader: loadString(base locale JSON)
FileLoader-->>Loader: baseJson (String)
Loader->>Resolver: resolveLinkedFiles(basePath, languageCode, baseJson, countryCode?)
rect rgb(242,248,255)
note right of Resolver: Recursively resolve strings starting with ":/"\nLoad linked file via FileLoader, merge maps,\ntrack visited paths, enforce max depth
loop traverse entries
alt entry is linked (":/")
Resolver->>FileLoader: loadString(resolved linked path)
FileLoader-->>Resolver: linkedJson
Resolver->>Resolver: recurse(linkedJson)
else entry is Map
Resolver->>Resolver: recurse(nested Map)
else
note right of Resolver: keep primitive value
end
end
end
Resolver-->>Loader: expanded Map
Loader-->>Controller: translations Map
Controller-->>App: ready
alt Cycle/depth exceeded or missing file
Resolver-->>Loader: throw StateError
Loader-->>Controller: error propagated as FlutterError
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. 📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
✨ Finishing Touches🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (5)
README.md (4)
412-416
: Fix heading punctuation and wording; align terminology with JSON
- Remove trailing colon from the heading (MD026)
- Prefer “JSON files” and clearer phrasing in the note.
-### 🔥 Linked files: +### 🔥 Linked files @@ -> ⚠ This is only available for the default asset loader (on Json Files). +> ⚠ This is only available for the default asset loader (JSON files only).
429-437
: Specify a language for the fenced code blockmarkdownlint (MD040) flags this block. Use a neutral language like text.
-``` +```text assets └── translations └── en-US ├── errors.json ├── validation.json └── notifications.json--- `440-440`: **Tighten grammar and fix the anchor link** - Remove the extra space before the colon. - The anchor for “Installation” should be #installation (emoji is ignored in GitHub anchors). ```diff -Don't forget to add your linked files (or linked files folder, here assets/translations/en-US/), to your pubspec.yaml : [See installation](#-installation). +Don't forget to add your linked files (or the folder assets/translations/en-US/) to your pubspec.yaml. See [Installation](#installation).
416-441
: Optional: Add a minimal pubspec example for linked subfoldersReaders may wonder whether subfolders must be declared explicitly. Consider appending a short snippet:
flutter: assets: - assets/translations/ # includes subfolders like en-US/, de-DE/, etc. # or, if you prefer being explicit: # - assets/translations/en-US/lib/src/asset_loader.dart (1)
33-35
: Nit: normalize leading slashes in filePath at the join siteWhile we sanitize in the recursive loader, it’s safer if this join also tolerates leading slashes in filePath.
- String _getLinkedLocalePath(String basePath, String filePath, Locale locale) { - return '$basePath/${locale.toStringWithSeparator(separator: "-")}/$filePath'; - } + String _getLinkedLocalePath(String basePath, String filePath, Locale locale) { + final sanitized = filePath.replaceAll(RegExp(r'^[\\/]+'), ''); + return '$basePath/${locale.toStringWithSeparator(separator: "-")}/$sanitized'; + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
README.md
(2 hunks)lib/src/asset_loader.dart
(1 hunks)
🧰 Additional context used
🪛 LanguageTool
README.md
[grammar] ~412-~412: There might be a mistake here.
Context: ...your full name ``` ### 🔥 Linked files: > ⚠ This is only available for the default ...
(QB_NEW_EN)
[grammar] ~440-~440: There might be a mistake here.
Context: ...s/translations/en-US/), to your pubspec.yaml : See installation. ##...
(QB_NEW_EN)
🪛 markdownlint-cli2 (0.17.2)
README.md
412-412: Trailing punctuation in heading
Punctuation: ':'
(MD026, no-trailing-punctuation)
429-429: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
Was needed for test to pass, everything is working as intended. iddle is probably need be cause de loading is slightly longer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
lib/src/asset_loader.dart (1)
69-72
: Improve logging clarity and seed traversal state with typed decode
- Log the actual asset path being loaded, not just base path.
- Cast json.decode to Map<String, dynamic> and pass initial visited/cache to the helper.
Apply this diff:
- EasyLocalization.logger.debug('Load asset from $path'); - - Map<String, dynamic> baseJson = json.decode(await rootBundle.loadString(localePath)); - return await _getLinkedTranslationFileDataFromBaseJson(path, locale, baseJson); + EasyLocalization.logger.debug('Load asset from $localePath'); + + final Map<String, dynamic> baseJson = + json.decode(await rootBundle.loadString(localePath)) as Map<String, dynamic>; + return await _getLinkedTranslationFileDataFromBaseJson( + path, + locale, + baseJson, + visited: <String>{}, + cache: <String, Map<String, dynamic>>{}, + );
🧹 Nitpick comments (5)
test/easy_localization_context_test.dart (1)
182-187
: Stabilize after resetLocale with pumpAndSettleAfter calling _context.resetLocale() (Line 184), a plain pump() (Line 185) may not always await all async/UI work. Prefer pumpAndSettle() here for stability.
Apply this diff:
- await _context.resetLocale(); - await tester.pump(); + await _context.resetLocale(); + await tester.pumpAndSettle();test/easy_localization_widget_test.dart (3)
91-99
: Good call enabling tester.idle(); also modernize matcher to isA()The idle() addition is appropriate. Minor nit: isInstanceOf() is deprecated; use isA() for consistency (you already use it elsewhere in this file).
Apply this diff:
- expect(Localization.of(_context), isInstanceOf<Localization>()); - expect(Localization.instance, isInstanceOf<Localization>()); - expect(Localization.instance, Localization.of(_context)); + expect(Localization.of(_context), isA<Localization>()); + expect(Localization.instance, isA<Localization>()); + expect(Localization.instance, Localization.of(_context));
183-197
: Fix test name typo: “loacle” → “locale”Typo in test description. Keeps test outputs clean.
Apply this diff:
- '[EasyLocalization] change loacle test', + '[EasyLocalization] change locale test',
262-268
: Avoid set literal in async closure passed to expectUsing () async => { await ... } creates a Set literal, which is confusing. Use a block closure or return the Future expression directly.
Apply this diff:
- l = const Locale('en', 'UK'); - expect(() async => {await EasyLocalization.of(_context)!.setLocale(l)}, throwsAssertionError); + l = const Locale('en', 'UK'); + expect(() async { + await EasyLocalization.of(_context)!.setLocale(l); + }, throwsAssertionError);lib/src/asset_loader.dart (1)
33-35
: Consider normalizing/sanitizing linked file paths_getLinkedLocalePath assumes filePath is safe. If callers supply values like '/x' or '../x', rootBundle may still resolve declared assets unexpectedly. The refactor above sanitizes before calling this function; keep that contract documented or add checks here as well.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
lib/src/asset_loader.dart
(1 hunks)test/easy_localization_context_test.dart
(7 hunks)test/easy_localization_widget_test.dart
(26 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (4)
test/easy_localization_context_test.dart (2)
141-141
: Enabling tester.idle() improves async determinism — LGTMActivating await tester.idle() after pumpWidget/pump helps settle microtasks before the next frame. This should reduce flakiness around async delegators.
Also applies to: 159-159, 171-171, 200-200, 219-219
91-91
: Inline supportedLocales arrays — stylistic change is fineConsolidating multi-line arrays to single-line is consistent and keeps tests concise. No behavioral change.
Also applies to: 118-118, 139-139, 156-156, 174-174, 197-197, 215-215
test/easy_localization_widget_test.dart (2)
667-672
: Using pumpAndSettle for device/locale flows — LGTMThe switch to pumpAndSettle makes these flows less flaky by waiting for all frames to settle.
Also applies to: 690-695
783-784
: Assertion on plural text after locale change — LGTMVerification after setLocale + pumpAndSettle looks correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
lib/src/asset_loader.dart (1)
110-110
: Explicitly cast decoded JSON to Map<String, dynamic> (avoid implicit cast at runtime)This was called out earlier; add an explicit cast for safety and clearer intent.
Apply this diff:
- Map<String, dynamic> baseJson = json.decode(await rootBundle.loadString(localePath)); + final Map<String, dynamic> baseJson = + json.decode(await rootBundle.loadString(localePath)) as Map<String, dynamic>;
🧹 Nitpick comments (4)
lib/src/asset_loader.dart (4)
27-29
: Remove leftover placeholder commentStray scaffolding comment in source; keep only the constant.
Apply this diff:
- // Place inside class RootBundleAssetLoader static const int _maxLinkedDepth = 32;
60-65
: Harden traversal check; avoid false positives on filenames containing “..”normalizedPath.contains('..') rejects legitimate names like "foo..json". Check path segments for '..' instead and guard against empty paths.
Apply this diff:
- // Normalize and reject traversal - final normalizedPath = rawPath.replaceAll(RegExp(r'^[\\/]+'), ''); - if (normalizedPath.contains('..')) { - throw FormatException('Invalid linked file path "$rawPath" for key "$key".'); - } + // Normalize leading separators and reject traversal via ".." segments + final normalizedPath = rawPath.replaceAll(RegExp(r'^[\\/]+'), ''); + if (normalizedPath.isEmpty) { + throw FormatException('Invalid linked file path "$rawPath" for key "$key".'); + } + final parts = normalizedPath.split(RegExp(r'[\\/]+')); + if (parts.any((p) => p == '..')) { + throw FormatException('Invalid linked file path "$rawPath" for key "$key".'); + }
40-103
: Arrays aren’t traversed; linked strings inside lists won’t be resolvedCurrent traversal handles maps and direct string links but skips lists. If translations may contain arrays (e.g., bullet points) with links or nested maps, they won’t be expanded.
- If arrays are out of scope, clarify in README that linking applies to object values only.
- If needed, I can add safe List traversal that mirrors the Map logic (cycle checks, caching). Want me to draft that?
105-118
: Add tests for cycles, depth, caching, and sanitizationThe implementation is solid; let’s lock behavior in with tests (happy path and failure cases).
Suggested cases:
- Link expansion across multiple files and nested folders.
- Cycle detection (A → B → A) and max-depth exceeded.
- Path sanitization rejects "../x.json" and accepts "foo..json".
- Cache effectiveness: same linked file included multiple times should load once.
I can scaffold widget/loader tests using TestAssetBundle to assert load counts and thrown errors. Want me to open a follow-up PR with these tests?
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
lib/src/asset_loader.dart
(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Codacy Static Code Analysis
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (3)
lib/src/asset_loader.dart (3)
65-67
: Add a per-load cache to dedupe asset readsThe same linked file can be referenced multiple times. Cache parsed JSON per asset path to avoid repeated IO/decoding.
- Future<Map<String, dynamic>> _getLinkedTranslationFileDataFromBaseJson( + Future<Map<String, dynamic>> _getLinkedTranslationFileDataFromBaseJson( String basePath, Locale locale, Map<String, dynamic> baseJson, { required Set<String> visited, int depth = 0, }) async { @@ - final Map<String, dynamic> linkedJson = - json.decode(await rootBundle.loadString(linkedAssetPath)) as Map<String, dynamic>; + final Map<String, dynamic> linkedJson = + (cache[linkedAssetPath] ??= (json.decode( + await rootBundle.loadString(linkedAssetPath), + ) as Map<String, dynamic>)); @@ - final resolved = await _getLinkedTranslationFileDataFromBaseJson( + final resolved = await _getLinkedTranslationFileDataFromBaseJson( basePath, locale, linkedJson, - visited: visited, + visited: visited, + cache: cache, depth: depth + 1, );And update the function signature accordingly:
- Map<String, dynamic> baseJson, { - required Set<String> visited, - int depth = 0, - }) async { + Map<String, dynamic> baseJson, { + required Set<String> visited, + required Map<String, Map<String, dynamic>> cache, + int depth = 0, + }) async {Note: See the load(...) comment below for seeding the cache at the call site.
Also applies to: 70-76
94-100
: Cast decoded JSON and seed traversal state (optionally add cache)
- Strongly type the decoded base JSON.
- If you adopt caching (suggested above), pass an empty cache here.
Minimal typing fix:
- Map<String, dynamic> baseJson = json.decode(await rootBundle.loadString(localePath)); + final Map<String, dynamic> baseJson = + json.decode(await rootBundle.loadString(localePath)) as Map<String, dynamic>;If you implement caching, also pass it:
- return await _getLinkedTranslationFileDataFromBaseJson( + return await _getLinkedTranslationFileDataFromBaseJson( path, locale, baseJson, - visited: <String>{}, + visited: <String>{}, + cache: <String, Map<String, dynamic>>{}, );
57-60
: Sanitize linked paths: strip leading slashes and reject parent traversalRaw paths like "/x.json" or "../x.json" should not be allowed. Normalize the link and reject ".." to prevent escaping the locale directory.
- final rawPath = value.substring(2).trim(); - final linkedAssetPath = _getLinkedLocalePath(basePath, rawPath, locale); + final rawPath = value.substring(2).trim(); + // Strip leading slashes/backslashes and reject parent traversal + final normalizedPath = rawPath.replaceAll(RegExp(r'^[\\/]+'), ''); + if (normalizedPath.contains('..')) { + throw FormatException('Invalid linked file path "$rawPath" for key "$key".'); + } + final linkedAssetPath = _getLinkedLocalePath(basePath, normalizedPath, locale);
🧹 Nitpick comments (2)
lib/src/asset_loader.dart (2)
27-29
: Remove leftover guidance comment; keep the constantThe inline note "Place inside class RootBundleAssetLoader" looks like a migration hint and should be removed.
- // Place inside class RootBundleAssetLoader static const int _maxLinkedDepth = 32;
78-82
: Preserve stack traces when rethrowingWrap with Error.throwWithStackTrace to retain the original stack for easier debugging.
- } catch (e) { - throw StateError( - 'Error resolving linked file "$linkedAssetPath" for key "$key": $e', - ); - } + } catch (e, st) { + Error.throwWithStackTrace( + StateError('Error resolving linked file "$linkedAssetPath" for key "$key": $e'), + st, + ); + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
lib/src/asset_loader.dart
(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (1)
lib/src/asset_loader.dart (1)
36-38
: LGTM: locale-scoped path helperThe helper composes the locale-scoped asset path consistently with the main getLocalePath.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
lib/src/asset_loader.dart (1)
57-60
: Sanitize link targets: normalize and reject traversal/absolute pathsPrevent accidental “..” traversal or absolute paths by normalizing the target before composing the asset path. This also improves error messages.
Apply this diff:
- if (value is String && value.startsWith(':/')) { - final rawPath = value.substring(2).trim(); - final linkedAssetPath = _getLinkedLocalePath(basePath, rawPath, locale); + if (value is String && value.startsWith(':/')) { + final rawPath = value.substring(2).trim(); + // Strip any leading slashes and reject parent traversal. + final normalizedPath = rawPath.replaceAll(RegExp(r'^[\\/]+'), ''); + if (normalizedPath.contains('..')) { + throw FormatException('Invalid linked file path "$rawPath" for key "$key".'); + } + final linkedAssetPath = _getLinkedLocalePath(basePath, normalizedPath, locale);
🧹 Nitpick comments (7)
lib/src/asset_loader.dart (7)
27-29
: Remove leftover prompt comment; keep or document the constantThe inline “Place inside class …” comment looks like a prompt stub and should be removed. Consider adding a brief doc comment for clarity.
Apply this diff:
- // Place inside class RootBundleAssetLoader - static const int _maxLinkedDepth = 32; + /// Maximum allowed include depth to avoid runaway recursion. + static const int _maxLinkedDepth = 32;
27-29
: Optional: make the depth ceiling configurable (without breaking const constructor)If you foresee projects needing a different ceiling, expose it as an optional parameter with a const default so existing usage and const constructor remain valid.
Example (outside selected lines):
// Inside class: final int maxLinkedDepth; const RootBundleAssetLoader({this.maxLinkedDepth = _maxLinkedDepth}); // Use `maxLinkedDepth` instead of `_maxLinkedDepth` in the guard.
36-38
: Nit: path joiningString concatenation works, but it’s brittle if callers accidentally pass leading/trailing slashes. You already normalize later; alternatively consider joining with a posix join helper or stripping leading slashes here as well.
61-63
: Clarify the error message: duplicates are disallowed by design, not just cyclesPer the feature’s contract, a second include of the same file is forbidden even if it’s not a cycle. Update the message to reflect both cases.
Apply this diff:
- if (visited.contains(linkedAssetPath)) { - throw StateError('Cyclic linked files detected at "$linkedAssetPath" (key: "$key").'); - } + if (visited.contains(linkedAssetPath)) { + throw StateError( + 'Linked file reuse or cycle detected at "$linkedAssetPath" (key: "$key"). ' + 'Reusing linked files is not allowed.', + ); + }
77-84
: Make control flow explicit and preserve stack traces on error
- Add a
continue;
after assigning the resolved map to avoid falling through to the subsequent Map-branch check.- Preserve the original stack trace when rethrowing.
Apply this diff:
); fullJson[key] = resolved; - } catch (e) { - throw StateError( - 'Error resolving linked file "$linkedAssetPath" for key "$key": $e', - ); - } + continue; + } catch (e, st) { + Error.throwWithStackTrace( + StateError('Error resolving linked file "$linkedAssetPath" for key "$key": $e'), + st, + ); + }
85-93
: Optional: recurse into lists if you decide to support themCurrently only nested Map values are traversed. If list-typed values can contain link markers or nested maps in your schema, you’ll need to handle List as well.
104-110
: Cast the decoded base JSON to a typed mapAlign with the linked-file decode and make the type explicit.
Apply this diff:
- Map<String, dynamic> baseJson = json.decode(await rootBundle.loadString(localePath)); + final Map<String, dynamic> baseJson = + json.decode(await rootBundle.loadString(localePath)) as Map<String, dynamic>;Optional: log the resolved asset path to aid debugging (outside selected lines):
EasyLocalization.logger.debug('Load asset from $localePath');
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
lib/src/asset_loader.dart
(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-19T11:49:13.323Z
Learnt from: JavedNicolas
PR: aissat/easy_localization#770
File: lib/src/asset_loader.dart:61-63
Timestamp: 2025-08-19T11:49:13.323Z
Learning: In the easy_localization linked files feature, files must not be used multiple times during translation resolution. The visited set in RootBundleAssetLoader._getLinkedTranslationFileDataFromBaseJson is intentionally never cleared to enforce this constraint - once a linked file is processed, it cannot be reused elsewhere in the same translation tree, even in different branches.
Applied to files:
lib/src/asset_loader.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (3)
lib/src/asset_loader.dart (3)
40-49
: Depth guard is good defensive programmingThe early return on excessive depth protects against runaway recursion. Looks good.
65-66
: Good: typed cast for decoded JSONCasting the decoded linked JSON to Map<String, dynamic> avoids type ambiguity at runtime.
68-76
: Acknowledged: not clearingvisited
is intentional policyLeaving entries in
visited
to disallow reuse across branches aligns with the stated design. No change requested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (8)
pubspec.yaml (1)
28-31
: Consider test-only asset loading to avoid shipping fixtures to consumersDeclaring i18n/ as package assets will bundle these test fixtures into every consuming app, increasing their bundle size unnecessarily. If these assets are only used by tests:
- Option A (preferred): Switch tests to a custom/Test AssetBundle that reads from test fixtures on disk, and remove the assets declaration.
- Option B: Move fixtures under an example app and declare them there.
If tests depend on rootBundle with these assets, keep as-is; otherwise, consider the refactor above.
Would you like help drafting a minimal TestAssetBundle helper and updating the tests to avoid including these assets in the published package?
i18n/en.json (1)
11-11
: LGTM: switched plural "hats" to a linked file referenceThe link syntax and placement look correct for locale-scoped resolution (:/hats.json → i18n/en/hats.json). Minor note: "hat_other.other" duplicates the "other hats" text present under hats.other now; keep if intentionally supporting an existing key.
test/asset_loader_linked_files_test.dart (6)
25-27
: Propagate loader errors in tests to avoid false positivesIn positive-path tests you currently log errors. If a load error happens, the test may proceed and fail later with a less-informative assertion. Consider immediately failing in the error callback.
Example change per occurrence:
- onLoadError: (FlutterError e) { - log(e.toString()); - }, + onLoadError: (FlutterError e) { + fail('Unexpected load error: $e'); + },You’re already rethrowing in the negative-path tests, which is good.
Also applies to: 49-51, 76-78, 100-101, 123-124, 154-156, 178-180, 204-205
63-66
: Clarify misleading comment: these are different files, not multiple references to the same fileThe code checks values from validation.json and multi_validation.json (and errors.json vs multi_errors.json). It’s not verifying reuse of the same file.
- // Check multiple references to same file work + // Check multiple linked files from different sources are loaded
139-141
: Prefer matchers for type assertionsUse matchers to improve failure messages and readability.
- expect(result['app']['errors'] is Map, true); - expect(result['app']['errors'] is String, false); + expect(result['app']['errors'], isA<Map>()); + expect(result['app']['errors'], isNot(isA<String>()));
160-166
: Correct the failure message: the test expects FlutterError, not StateErrorThe test asserts FlutterError, so the fail message should reflect that.
- fail('Expected StateError to be thrown'); + fail('Expected FlutterError to be thrown');
15-16
: Optional: factor controller creation to reduce duplication and centralize the asset pathA small helper will DRY up repeated setup and ensure the asset path stays consistent:
// Add near the top (after imports) const _kI18nAssetsPath = 'i18n'; EasyLocalizationController _buildController({ required Locale locale, bool useOnlyLangCode = false, void Function(FlutterError e)? onLoadError, }) { return EasyLocalizationController( forceLocale: locale, path: _kI18nAssetsPath, supportedLocales: [locale], useOnlyLangCode: useOnlyLangCode, useFallbackTranslations: false, saveLocale: false, onLoadError: onLoadError ?? (e) => fail('Unexpected load error: $e'), assetLoader: const RootBundleAssetLoader(), ); }Then replace repeated controller initializations with _buildController(locale: const Locale('en', 'linked')) etc.
144-167
: Consider adding a test for "no reuse of the same linked file across branches" (by design)Per feature design, once a linked file is processed, it must not be reused elsewhere in the same resolution tree (visited set is not cleared). Add a negative-path test to lock this behavior.
Example sketch:
test('should error when the same linked file is referenced in two branches', () async { final controller = _buildController( locale: const Locale('en', 'linked'), onLoadError: (e) => throw e, ); // Create a fixture JSON (e.g., i18n/en-dup.json) where two different branches // both point to :/shared.json to trigger the "already visited" rule. controller.forceLocale = const Locale('en', 'dup'); try { await controller.loadTranslations(); fail('Expected FlutterError to be thrown for duplicate linked file usage'); } catch (e) { expect(e, isA<FlutterError>()); expect(e.toString(), contains('already processed')); // or the specific message you emit } });If you’d like, I can draft the dup fixtures and test case in a follow-up commit.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (16)
i18n/en-cyclic.json
(1 hunks)i18n/en-cyclic/cycle_file1.json
(1 hunks)i18n/en-cyclic/cycle_file2.json
(1 hunks)i18n/en-linked.json
(1 hunks)i18n/en-linked/deep/level1.json
(1 hunks)i18n/en-linked/deep/level2.json
(1 hunks)i18n/en-linked/errors.json
(1 hunks)i18n/en-linked/multi_errors.json
(1 hunks)i18n/en-linked/multi_validation.json
(1 hunks)i18n/en-linked/nested/messages.json
(1 hunks)i18n/en-linked/validation.json
(1 hunks)i18n/en-missing.json
(1 hunks)i18n/en.json
(1 hunks)i18n/en/hats.json
(1 hunks)pubspec.yaml
(1 hunks)test/asset_loader_linked_files_test.dart
(1 hunks)
✅ Files skipped from review due to trivial changes (6)
- i18n/en-missing.json
- i18n/en-linked/deep/level2.json
- i18n/en-cyclic/cycle_file1.json
- i18n/en-linked/errors.json
- i18n/en-linked/multi_errors.json
- i18n/en/hats.json
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: JavedNicolas
PR: aissat/easy_localization#770
File: lib/src/asset_loader.dart:61-63
Timestamp: 2025-08-19T11:49:13.323Z
Learning: In the easy_localization linked files feature, files must not be used multiple times during translation resolution. The visited set in RootBundleAssetLoader._getLinkedTranslationFileDataFromBaseJson is intentionally never cleared to enforce this constraint - once a linked file is processed, it cannot be reused elsewhere in the same translation tree, even in different branches.
📚 Learning: 2025-08-19T11:49:13.323Z
Learnt from: JavedNicolas
PR: aissat/easy_localization#770
File: lib/src/asset_loader.dart:61-63
Timestamp: 2025-08-19T11:49:13.323Z
Learning: In the easy_localization linked files feature, files must not be used multiple times during translation resolution. The visited set in RootBundleAssetLoader._getLinkedTranslationFileDataFromBaseJson is intentionally never cleared to enforce this constraint - once a linked file is processed, it cannot be reused elsewhere in the same translation tree, even in different branches.
Applied to files:
test/asset_loader_linked_files_test.dart
🔇 Additional comments (7)
i18n/en-linked/nested/messages.json (1)
1-5
: LGTM: valid JSON and paths look correct for nested linkingKeys/values are well-formed. The link target implied by en-linked.json (:/nested/messages.json) matches this file’s location. No further changes needed.
i18n/en-cyclic.json (1)
1-4
: Intentionally cyclic fixture: good for exercising cycle detectionThis looks purpose-built for the cycle tests and aligns with the resolver behavior. No issues spotted with structure or paths.
i18n/en-linked/deep/level1.json (1)
1-4
: LGTM: deep link target path matches expected structureThe link “:/deep/level2.json” correctly maps relative to en-linked/, and the file format is valid.
i18n/en-linked/validation.json (1)
1-6
: LGTM: placeholders and key naming are consistentPlaceholders {min}/{max} align with easy_localization’s named replacements. One note based on the linked-files design: the visited-set constraint means the same linked file cannot be reused in multiple branches of a single resolution. Ensure this file isn’t linked from multiple branches in the same tree if that would conflict with the intended behavior.
If there’s a scenario requiring reuse of the same validation file in multiple branches, we can suggest a structure that deduplicates content or uses small split files to avoid the visited-set restriction. Want a proposal?
i18n/en-linked.json (1)
1-19
: LGTM: comprehensive linked structure for sample datasetAll referenced linked paths are coherent and align with the test coverage (single, multiple, nested, deep).
i18n/en-linked/multi_validation.json (1)
1-6
: LGTM: validation messages for "multiple" groupContent aligns with tests. Placeholders {min}/{max} preserved.
i18n/en-cyclic/cycle_file2.json (1)
1-4
: LGTM: cycle established for error-path testingThe back-link to cycle_file1.json correctly forms the cycle used by tests.
Adds tests for linked files, including error handling for cyclic dependencies and missing files. Includes a fix to consider the `useOnlyLangCode` flag.
Fixed: audit being broken after link file changes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
test/easy_localization_test.dart (1)
1-13
: Missing intl import for NumberFormat
NumberFormat.currency()
is used butpackage:intl/intl.dart
isn’t imported; tests will fail to compile.import 'package:shared_preferences/shared_preferences.dart'; +import 'package:intl/intl.dart';
Also applies to: 515-517
♻️ Duplicate comments (2)
test/asset_loader_linked_files_test.dart (1)
20-21
: Align asset path with established test convention: '../../i18n'Per the existing suite pattern, tests should use
../../i18n
for filesystem-backed loading (not bundle keys). Keeps consistency and avoids path issues across environments. This mirrors prior discussion/decision.- path: 'i18n', + path: '../../i18n',Also applies to: 44-45, 71-72, 94-95, 117-118, 148-149, 172-173, 198-199
lib/src/asset_loader.dart (1)
50-56
: Strongly type the decoded JSON; consider passing scriptCode; confirm resolver semantics
- json.decode returns dynamic; explicitly cast to Map<String, dynamic> to satisfy the analyzer and avoid runtime surprises. This was suggested earlier as well.
- Some locales use scriptCode (e.g., zh-Hant). If LinkedFileResolver can benefit from it, consider threading locale.scriptCode through as an optional named parameter.
- Per the earlier product requirement (learned from this PR), linked files must not be reused across branches within the same resolution. Please ensure LinkedFileResolver enforces that invariant. I’m calling it out here since resolution moved out of this file.
Apply this diff for typing:
- Map<String, dynamic> baseJson = json.decode(await linkedFileResolver.fileLoader.loadString(localePath)); + final Map<String, dynamic> baseJson = + json.decode(await linkedFileResolver.fileLoader.loadString(localePath)) as Map<String, dynamic>;If you choose to pass scriptCode and the resolver supports it:
return await linkedFileResolver.resolveLinkedFiles( basePath: path, languageCode: locale.languageCode, countryCode: locale.countryCode, + scriptCode: locale.scriptCode, baseJson: baseJson, );
🧹 Nitpick comments (18)
lib/easy_localization.dart (1)
7-8
: Consider exporting the FileLoader interface (and confirm top-level Flutter dependency).
- If we export
RootBundleFileLoader
, many consumers implementing custom loaders will also want to depend on theFileLoader
interface without importing internal paths. Suggest exporting it from the top-level library for convenience.- Also, re-exporting a Flutter-specific loader from the root library couples
package:flutter
into any import ofeasy_localization.dart
. If this is intentional (library is Flutter-only), all good. If you want CLI/tooling use-cases to importeasy_localization.dart
without Flutter, consider an alternative entrypoint (e.g.,easy_localization/flutter.dart
) or export guards.Proposed minimal addition:
export 'package:easy_localization/src/linked_file_resolver.dart'; export 'package:easy_localization/src/file_loaders/root_bundle_file_loader.dart'; +export 'package:easy_localization/src/file_loaders/file_loader.dart';
lib/src/file_loaders/file_loader.dart (1)
1-5
: Document path semantics and encoding contract for implementers.The interface is clear; adding brief docs on how paths are interpreted and expected encoding will prevent subtle incompatibilities among loaders.
Proposed inline docs:
/// Abstract file loader interface to allow different implementations /// for Flutter runtime (using rootBundle) and CLI (using dart:io) abstract class FileLoader { - Future<String> loadString(String path); + /// Loads file contents as UTF-8 text. + /// + /// The [path] is the logical asset path that callers use for translations. + /// Implementations should interpret it relative to their translations root + /// and return the file contents decoded as UTF-8. + Future<String> loadString(String path); }lib/src/file_loaders/root_bundle_file_loader.dart (1)
8-12
: Avoid unnecessary async; optionally document caching behavior.
- The method can return the future directly without
async
/return
.- rootBundle caches by default; consider noting this in a doc comment (or expose a way to control it) if hot-reload staleness ever comes up.
@override - Future<String> loadString(String path) async { - return rootBundle.loadString(path); - } + Future<String> loadString(String path) => rootBundle.loadString(path);lib/src/file_loaders/io_file_loader.dart (1)
11-16
: Prefer async I/O and make UTF-8 explicit.
- Use
await file.exists()
instead of the sync variant inside an async method.readAsString()
is UTF-8 by default, but being explicit helps keep parity with other loaders and avoids surprises on older SDKs.-import 'dart:io'; +import 'dart:io'; +import 'dart:convert' show utf8; @@ @override Future<String> loadString(String path) async { final file = File(path); - if (!file.existsSync()) { + if (!await file.exists()) { throw FileSystemException('File not found', path); } - return file.readAsString(); + return file.readAsString(encoding: utf8); }README.md (3)
412-412
: Fix heading style: remove trailing colon (MD026).Headings shouldn’t end with punctuation; also improves table-of-contents rendering.
-### 🔥 Linked files: +### 🔥 Linked files
427-434
: Add a language hint to the fenced code block (MD040).This block isn’t code; annotate it as text for better rendering.
-``` +```text assets └── translations └── en-US ├── errors.json ├── validation.json └── notifications.json--- `436-436`: **Document linked-file reuse and cycle handling.** Per the current resolver semantics, a linked file is processed at most once per resolution tree and cycles are detected. Helpful to call this out to users. ```diff -Each linked file must contain a valid JSON object of translation keys. +Each linked file must contain a valid JSON object of translation keys. +By design, a linked file is processed at most once per resolution to prevent duplication; reusing the same file in multiple branches of the same tree is not allowed, and cyclic links will result in an error.
bin/audit/audit_command.dart (3)
45-49
: Reduce splits and clarify variable naming for locale parsingAvoid repeated
split('-')
calls and use a clearer name thanlocal
to prevent confusion with “locale”. Minor alloc/clarity win.- final local = basenameWithoutExtension(file.path); - final langCode = local.split('-').first; - final hasCountryCode = local.split('-').length > 1; - final countryCode = hasCountryCode ? local.split('-').last : null; + final localeId = basenameWithoutExtension(file.path); + final parts = localeId.split('-'); + final langCode = parts.first; + final countryCode = parts.length > 1 ? parts.last : null; @@ - result[local] = _flatten(resolvedJson); + result[localeId] = _flatten(resolvedJson);Also applies to: 58-58
49-50
: Avoid sync file IO inside async method
readAsStringSync()
blocks the event loop; use the async variant since you’re already in an async flow.- final jsonMap = json.decode(file.readAsStringSync()) as Map<String, dynamic>; + final content = await file.readAsString(); + final jsonMap = json.decode(content) as Map<String, dynamic>;
137-167
: Consider writing reports to stdout instead of stderrThe audit output is informational. Streaming all lines to stderr can confuse CI pipelines that treat stderr as failures. Consider using stdout for normal results and reserve stderr for actual errors.
test/easy_localization_test.dart (3)
202-202
: Typo in test name: “lenguage” → “language”Keeps test names professional and searchable.
- test('select best lenguage match if no perfect match exists', () { + test('select best language match if no perfect match exists', () {
363-365
: Unimplemented TODO leaves a gap in argument-count validation coverageAdd an assertion that mismatched
{}
count throws, to protect against regressions.Proposed test body:
expect( () => Localization.instance.tr('test_replace_two', args: ['only_one']), throwsA(isA<AssertionError>()), );I can wire this with a dedicated fixture key if you prefer a non-assertion error type (e.g., FlutterError) — say the word and I’ll push a ready diff.
49-50
: Optional: use modern matcher isA()
isInstanceOf<T>()
is deprecated in favor ofisA<T>()
inpackage:test
. Not required, but future-proof.- expect(Localization.instance, isInstanceOf<Localization>()); + expect(Localization.instance, isA<Localization>());Also applies to: 55-57
test/asset_loader_linked_files_test.dart (2)
9-14
: Move async initialization into setUpAll for test harness clarityAn
async
main works, but putting environment init insetUpAll
is the typical pattern and avoids potential ordering surprises.-void main() async { - // Initialize the test environment - TestWidgetsFlutterBinding.ensureInitialized(); - SharedPreferences.setMockInitialValues({}); - await EasyLocalization.ensureInitialized(); +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(() async { + SharedPreferences.setMockInitialValues({}); + await EasyLocalization.ensureInitialized(); + });
160-166
: Incorrect failure message vs expected error typeYou
fail('Expected StateError…')
but then assertFlutterError
. Update the message to match the expected type to avoid confusing failures.- fail('Expected StateError to be thrown'); + fail('Expected FlutterError to be thrown');lib/src/linked_file_resolver.dart (2)
60-63
: Error message could clarify duplicate vs cyclic referenceThe visited-set also blocks duplicate reuse (by design). Consider expanding the message to mention “duplicate or cyclic” for clearer debugging. If you opt in, update the dependent test expectation accordingly.
- throw StateError('Cyclic linked files detected at "$linkedAssetPath" (key: "$key").'); + throw StateError('Duplicate or cyclic linked files detected at "$linkedAssetPath" (key: "$key").');
64-83
: Wrap-and-rethrow retains detail; consider including the original type/messageYou already include the path/key which is great. If you decide to further enrich diagnostics, you could embed the original
e.runtimeType
to simplify triage. Optional.lib/src/asset_loader.dart (1)
19-19
: Remove stray commentThe “Place inside class RootBundleAssetLoader” note is stale and confusing in the AssetLoader base class.
- // Place inside class RootBundleAssetLoader final LinkedFileResolver linkedFileResolver;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
example/lib/generated/codegen_loader.g.dart
is excluded by!**/generated/**
📒 Files selected for processing (12)
README.md
(2 hunks)bin/audit/audit_command.dart
(3 hunks)lib/easy_localization.dart
(1 hunks)lib/src/asset_loader.dart
(3 hunks)lib/src/easy_localization_app.dart
(5 hunks)lib/src/file_loaders/file_loader.dart
(1 hunks)lib/src/file_loaders/io_file_loader.dart
(1 hunks)lib/src/file_loaders/root_bundle_file_loader.dart
(1 hunks)lib/src/linked_file_resolver.dart
(1 hunks)test/asset_loader_linked_files_test.dart
(1 hunks)test/easy_localization_test.dart
(21 hunks)test/utils/test_asset_loaders.dart
(5 hunks)
✅ Files skipped from review due to trivial changes (1)
- lib/src/easy_localization_app.dart
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: JavedNicolas
PR: aissat/easy_localization#770
File: lib/src/asset_loader.dart:61-63
Timestamp: 2025-08-19T11:49:13.347Z
Learning: In the easy_localization linked files feature, files must not be used multiple times during translation resolution. The visited set in RootBundleAssetLoader._getLinkedTranslationFileDataFromBaseJson is intentionally never cleared to enforce this constraint - once a linked file is processed, it cannot be reused elsewhere in the same translation tree, even in different branches.
📚 Learning: 2025-08-19T11:49:13.347Z
Learnt from: JavedNicolas
PR: aissat/easy_localization#770
File: lib/src/asset_loader.dart:61-63
Timestamp: 2025-08-19T11:49:13.347Z
Learning: In the easy_localization linked files feature, files must not be used multiple times during translation resolution. The visited set in RootBundleAssetLoader._getLinkedTranslationFileDataFromBaseJson is intentionally never cleared to enforce this constraint - once a linked file is processed, it cannot be reused elsewhere in the same translation tree, even in different branches.
Applied to files:
lib/src/linked_file_resolver.dart
test/asset_loader_linked_files_test.dart
lib/src/asset_loader.dart
📚 Learning: 2025-08-20T09:05:04.294Z
Learnt from: JavedNicolas
PR: aissat/easy_localization#770
File: test/asset_loader_linked_files_test.dart:20-21
Timestamp: 2025-08-20T09:05:04.294Z
Learning: The easy_localization test suite consistently uses the path '../../i18n' for asset loading across all test files, not Flutter asset bundle keys. This is the established pattern that should be followed for consistency.
Applied to files:
lib/src/file_loaders/root_bundle_file_loader.dart
lib/easy_localization.dart
test/asset_loader_linked_files_test.dart
test/utils/test_asset_loaders.dart
lib/src/asset_loader.dart
test/easy_localization_test.dart
🪛 GitHub Actions: PR Check
lib/src/asset_loader.dart
[error] 5-5: Flutter analyzer: Unused import 'package:flutter/services.dart' (unused_import) at lib/src/asset_loader.dart:5:8.
🪛 LanguageTool
README.md
[grammar] ~412-~412: There might be a mistake here.
Context: ...your full name ``` ### 🔥 Linked files: You can split translations for a single l...
(QB_NEW_EN)
[grammar] ~438-~438: There might be a mistake here.
Context: ...s/translations/en-US/), to your pubspec.yaml : See installation. ##...
(QB_NEW_EN)
🪛 markdownlint-cli2 (0.17.2)
README.md
412-412: Trailing punctuation in heading
Punctuation: ':'
(MD026, no-trailing-punctuation)
427-427: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (4)
test/utils/test_asset_loaders.dart (2)
7-9
: Consistent default wiring of LinkedFileResolver looks goodUsing
const JsonLinkedFileResolver(fileLoader: RootBundleFileLoader())
across loaders aligns tests with the new loader/resolver stack and keeps dependencies explicit and immutable. Nice.Also applies to: 19-20, 141-143, 153-155
30-31
: Minor: map literal formatting changes preserve semanticsThe refactors to inline some literals (e.g.,
gender_and_replace
, plural maps) keep behavior intact. No action needed.Also applies to: 83-83, 87-87, 119-136
test/asset_loader_linked_files_test.dart (1)
41-66
: Note: duplicate linked-file reuse policyThis test comments “multiple references to same file work”. The linked-files design intentionally prevents reusing the same file multiple times within a single resolution tree (visited set is never cleared). Please verify the fixture doesn’t actually reuse the identical file path across branches; if it does, the resolver should throw. If the intent is to support reuse, we need to revisit the resolver’s policy.
I can add a minimal negative test that references the same
:/file.json
twice from different branches and asserts a failure, if desired.lib/src/asset_loader.dart (1)
31-39
: Const constructor/factory may be over-constrained; ensure nested types are const or drop const hereRootBundleAssetLoader is const, and fromIOFile returns a const instance, but this is only valid if JsonLinkedFileResolver and IOFileLoader (and RootBundleFileLoader) all have const constructors and are invoked as const. To avoid brittle constraints across modules, simplest is to make this constructor/factory non-const and mark nested values const instead.
Apply this diff:
-class RootBundleAssetLoader extends AssetLoader { - const RootBundleAssetLoader({LinkedFileResolver? linkedFileResolver}) +class RootBundleAssetLoader extends AssetLoader { + RootBundleAssetLoader({LinkedFileResolver? linkedFileResolver}) : super( - linkedFileResolver: linkedFileResolver ?? const JsonLinkedFileResolver(fileLoader: RootBundleFileLoader())); + linkedFileResolver: + linkedFileResolver ?? const JsonLinkedFileResolver(fileLoader: const RootBundleFileLoader())); factory RootBundleAssetLoader.fromIOFile() { - return const RootBundleAssetLoader( - linkedFileResolver: JsonLinkedFileResolver(fileLoader: IOFileLoader()), + return RootBundleAssetLoader( + linkedFileResolver: const JsonLinkedFileResolver(fileLoader: const IOFileLoader()), ); } }If you want to keep the outer constructor const, ensure all nested constructors are const and actually support const, and add tests/builds for all platforms to catch regressions.
Allow user to split the translations files to keep translations maintanable.
An update will be needed on easy localization loader one this is pushed to pub.
Breaking changes : The asset loader now use a linked file loader and a file loader, so all class extending them will need to add the super parameter.
Summary by CodeRabbit
New Features
Documentation
Tests
Behavior