Skip to content

[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

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
Open

Conversation

JavedNicolas
Copy link
Contributor

@JavedNicolas JavedNicolas commented Aug 19, 2025

Allow user to split the translations files to keep translations maintanable.

PS : Only for default Asset loader and JSON format. Might need some more work for other type of files and custom asset loaders

Summary by CodeRabbit

  • New Features
    • Support splitting a locale’s translations across multiple linked JSON files using a :/ path syntax, with deep nesting, composition, and clear errors for cyclic or missing links. No public API changes.
  • Documentation
    • Added “Linked files” section explaining setup, linking syntax, asset inclusion, and that this works with the default JSON asset loader.
  • Tests
    • Added comprehensive tests covering single/multiple links, nested and deeply nested structures, structure preservation, error cases (cycles/missing), and language-code-only scenarios.

Copy link

coderabbitai bot commented Aug 19, 2025

Walkthrough

Introduces linked translation file resolution in RootBundleAssetLoader with depth limit and cycle detection, updates README with a “Linked files” section (duplicated), adjusts test timing, adds comprehensive tests for linked files, and adds/updates i18n JSON assets to exercise single, multiple, nested, deep, cyclic, and missing link scenarios. en.json now links hats externally.

Changes

Cohort / File(s) Summary
Documentation
README.md
Adds a “Linked files” section describing :/ path linking, locale-scoped loading, asset inclusion requirements; section appears duplicated; minor spacing changes.
Asset loader core logic
lib/src/asset_loader.dart
Implements recursive linked-file expansion with max depth (32), cycle detection via visited set, locale-scoped path resolution, and explicit StateError messages; load() now returns expanded maps.
New linked-files tests
test/asset_loader_linked_files_test.dart
Adds tests covering single/multiple/nested/deep links, structure preservation, cyclic/missing link errors, and useOnlyLangCode behavior using RootBundleAssetLoader.
Test formatting/timing adjustments
test/easy_localization_context_test.dart, test/easy_localization_widget_test.dart
Refactors array literals to single-line; enables await tester.idle() in multiple places; no semantic changes to assertions.
Base locale change for hats
i18n/en.json, i18n/en/hats.json
Replaces inline “hat” plural map with reference “hats”: “:/hats.json”; adds hats.json with plural forms.
Linked data set (happy paths)
i18n/en-linked.json, i18n/en-linked/errors.json, i18n/en-linked/validation.json, i18n/en-linked/multi_errors.json, i18n/en-linked/multi_validation.json, i18n/en-linked/nested/messages.json, i18n/en-linked/deep/level1.json, i18n/en-linked/deep/level2.json
Adds base and linked translation files demonstrating single, multiple, nested, and deep linking; includes placeholders in validation strings.
Cyclic linking fixtures
i18n/en-cyclic.json, i18n/en-cyclic/cycle_file1.json, i18n/en-cyclic/cycle_file2.json
Adds resources forming a cyclic reference chain to validate cycle detection and error handling.
Missing-link fixture
i18n/en-missing.json
Adds a file referencing a non-existent JSON to validate missing-file error handling.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App
  participant Controller as EasyLocalizationController
  participant Loader as RootBundleAssetLoader
  participant Bundle as rootBundle

  App->>Controller: loadTranslations()
  Controller->>Loader: load(path, locale)
  Loader->>Bundle: loadString(base locale JSON)
  Bundle-->>Loader: baseJson (Map)

  rect rgb(242,248,255)
    note right of Loader: Resolve linked values recursively<br/>with depth limit and cycle detection
    loop For each entry in JSON
      alt value startsWith ":/"
        Loader->>Loader: _getLinkedLocalePath()
        Loader->>Bundle: loadString(linked file for locale)
        Bundle-->>Loader: linkedJson (Map)
        Loader->>Loader: recurse(depth+1, visited)
        Loader-->>Loader: resolved Map
        Loader-->>Loader: replace string with resolved Map
      else value is Map
        Loader->>Loader: recurse into nested Map
      else
        note right of Loader: keep primitive as-is
      end
    end
  end

  Loader-->>Controller: expanded translations (Map)
  Controller-->>App: ready

  alt Cycle detected or depth exceeded
    Loader-->>Controller: throw StateError
    Controller-->>App: error surfaced (FlutterError)
  else Missing file
    Loader-->>Controller: throw StateError
    Controller-->>App: error surfaced (FlutterError)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I hop through keys, from root to leaf,
Following paths with bunny belief.
If links loop back, I thump—beware!
Depths are counted, I take care.
Hats now live next burrow, neat—
Translations stitched, a tidy feat. 🐇📜

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 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.

📥 Commits

Reviewing files that changed from the base of the PR and between faffb64 and 255057d.

📒 Files selected for processing (15)
  • 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)
  • test/asset_loader_linked_files_test.dart (1 hunks)
✅ Files skipped from review due to trivial changes (3)
  • i18n/en-linked/validation.json
  • i18n/en-cyclic/cycle_file1.json
  • i18n/en-linked/errors.json
🚧 Files skipped from review as they are similar to previous changes (11)
  • i18n/en-cyclic.json
  • i18n/en-linked/deep/level1.json
  • i18n/en-linked/deep/level2.json
  • i18n/en-cyclic/cycle_file2.json
  • i18n/en-linked/multi_errors.json
  • i18n/en-missing.json
  • i18n/en-linked/nested/messages.json
  • i18n/en-linked/multi_validation.json
  • test/asset_loader_linked_files_test.dart
  • i18n/en-linked.json
  • i18n/en/hats.json
🧰 Additional context used
🧠 Learnings (1)
📓 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.
⏰ 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)
i18n/en.json (1)

11-11: Link target semantics are correct; no rename needed

  • The loader’s _getLinkedLocalePath(…) uses <basePath>/<locale>/hats.json, so ":/hats.json" correctly resolves to i18n/en/hats.json.
  • There is no existing top-level "hat" key (only "hat_other"), so introducing "hats" does not break any prior calls.
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a 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 block

markdownlint (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 subfolders

Readers 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 site

While 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.

📥 Commits

Reviewing files that changed from the base of the PR and between c818108 and 186b05c.

📒 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)

@JavedNicolas JavedNicolas changed the title [Feature] Linked Files [WIP][Feature] Linked Files Aug 19, 2025
Was needed for test to pass, everything is working as intended. iddle is probably need be cause de loading is slightly longer
Copy link

@coderabbitai coderabbitai bot left a 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 pumpAndSettle

After 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 expect

Using () 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 186b05c and 74e6432.

📒 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 — LGTM

Activating 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 fine

Consolidating 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 — LGTM

The 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 — LGTM

Verification after setLocale + pumpAndSettle looks correct.

Copy link

@coderabbitai coderabbitai bot left a 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 comment

Stray 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 resolved

Current 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 sanitization

The 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 74e6432 and 51b65d7.

📒 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

Copy link

@coderabbitai coderabbitai bot left a 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 reads

The 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 traversal

Raw 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 constant

The 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 rethrowing

Wrap 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 51b65d7 and 3061da1.

📒 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 helper

The helper composes the locale-scoped asset path consistently with the main getLocalePath.

Copy link

@coderabbitai coderabbitai bot left a 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 paths

Prevent 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 constant

The 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 joining

String 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 cycles

Per 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 them

Currently 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 map

Align 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.

📥 Commits

Reviewing files that changed from the base of the PR and between e31d608 and 90db15f.

📒 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 programming

The early return on excessive depth protects against runaway recursion. Looks good.


65-66: Good: typed cast for decoded JSON

Casting the decoded linked JSON to Map<String, dynamic> avoids type ambiguity at runtime.


68-76: Acknowledged: not clearing visited is intentional policy

Leaving entries in visited to disallow reuse across branches aligns with the stated design. No change requested.

@JavedNicolas JavedNicolas changed the title [WIP][Feature] Linked Files [Feature] Linked Files Aug 19, 2025
Copy link

@coderabbitai coderabbitai bot left a 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 consumers

Declaring 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 reference

The 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 positives

In 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 file

The 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 assertions

Use 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 StateError

The 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 path

A 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 90db15f and faffb64.

📒 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 linking

Keys/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 detection

This 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 structure

The 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 consistent

Placeholders {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 dataset

All 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" group

Content aligns with tests. Placeholders {min}/{max} preserved.

i18n/en-cyclic/cycle_file2.json (1)

1-4: LGTM: cycle established for error-path testing

The 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant