Skip to content

Conversation

JavedNicolas
Copy link
Contributor

@JavedNicolas JavedNicolas commented Aug 19, 2025

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

    • Split a locale’s translations across linked JSON files using a :/ path syntax; supports deep nesting, composition, and detects/report cycles or missing links.
    • Supports loading linked files from bundled assets and CLI/IO sources.
  • Documentation

    • Added "Linked files" instructions explaining syntax, asset inclusion, and default JSON loader requirements.
  • Tests

    • New tests covering single/multiple links, nested/deep nesting, structure preservation, error cases, and language-code-only loads.
  • Behavior

    • Added fallback handling for empty resources and improved locale name handling in tooling.

Copy link

coderabbitai bot commented Aug 19, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
Documentation
README.md
Adds a "Linked files" section describing the :/ link syntax, locale-scoped linked assets, pubspec inclusion guidance; duplicated insertion and minor spacing edits.
Asset loader API
lib/src/asset_loader.dart
AssetLoader now requires and exposes linkedFileResolver and fileLoader; RootBundleAssetLoader gains a non-parameterless constructor and factories (fromRootBundle, fromIOFile); load flow delegates to resolver via fileLoader.
Linked-file resolver & helpers
lib/src/linked_file_resolver.dart
Adds LinkedFileResolver and JsonLinkedFileResolver implementations: resolve :/... links recursively, merge JSON maps, detect cycles, enforce max depth, build locale-specific linked paths.
File loader abstractions/impls
lib/src/file_loaders/file_loader.dart, lib/src/file_loaders/io_file_loader.dart, lib/src/file_loaders/root_bundle_file_loader.dart
Introduces FileLoader interface and two implementations: IOFileLoader (dart:io) and RootBundleFileLoader (Flutter rootBundle).
Public exports
lib/easy_localization.dart
Exports linked_file_resolver.dart and file_loaders/root_bundle_file_loader.dart.
EasyLocalization default wiring
lib/src/easy_localization_app.dart
Default assetLoader switched to a RootBundleAssetLoader wired with RootBundleFileLoader and JsonLinkedFileResolver.
Audit CLI
bin/audit/audit_command.dart
Makes audit async, uses IOFileLoader + linked-file resolution to load translations, flattens keys per full locale string, reports IO errors.
Tests — new & updated
test/asset_loader_linked_files_test.dart, test/utils/test_asset_loaders.dart, test/easy_localization_test.dart, test/easy_localization_context_test.dart, test/easy_localization_widget_test.dart, test/easy_localization_extra_asset_loaders_test.dart
Adds tests for single/multiple/nested/deep linked files, cyclic and missing-file errors; wires test asset loaders to JsonLinkedFileResolver(RootBundleFileLoader()); enables several await tester.idle() inserts and formatting changes; some constructors made non-const.
i18n fixtures — linked, cyclic, missing, plural move
i18n/en-linked.json, i18n/en-linked/..., i18n/en-cyclic.json, i18n/en-cyclic/..., i18n/en-missing.json, i18n/en.json, i18n/en/hats.json
Adds many JSON fixtures demonstrating :/ linking (single/multiple/nested/deep), introduces cyclic and missing-file fixtures, and moves hats pluralization to i18n/en/hats.json.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • [Command] Audit #763 — Related changes to AuditCommand and audit flow that also touch audit loading and linked-file resolution.

Suggested reviewers

  • aissat

Poem

I hop through keys from file to file,
Chasing :/ paths with rabbit style.
If loops appear I thump and shout—
Count the depth, then sort them out.
Translations stitched, I nibble a carrot. 🐇📚

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 2de68dc and fa0e4e6.

📒 Files selected for processing (1)
  • test/easy_localization_widget_test.dart (26 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/easy_localization_widget_test.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
✨ 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.
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: 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 but package: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 the FileLoader 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 of easy_localization.dart. If this is intentional (library is Flutter-only), all good. If you want CLI/tooling use-cases to import easy_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 parsing

Avoid repeated split('-') calls and use a clearer name than local 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 stderr

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

Add 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 of isA<T>() in package: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 clarity

An async main works, but putting environment init in setUpAll 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 type

You fail('Expected StateError…') but then assert FlutterError. 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 reference

The 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/message

You 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 comment

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

📥 Commits

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

⛔ 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 good

Using 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 semantics

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

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

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

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