Skip to content

Commit 697d316

Browse files
authored
Merge pull request #770 from JavedNicolas/develop
[Feature] Linked Files
2 parents c818108 + fa0e4e6 commit 697d316

32 files changed

+722
-394
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ flutter:
7373
- assets/translations/
7474
```
7575
76+
77+
7678
### 🔌 Loading translations from other resources
7779
7880
You can use JSON,CSV,HTTP,XML,Yaml files, etc.
@@ -407,6 +409,34 @@ Output:
407409
print('example.emptyNameError'.tr()); //Output: Please fill in your full name
408410
```
409411

412+
### 🔥 Linked files:
413+
414+
You can split translations for a single locale into multiple files by using linked files. This helps keep your JSON clean and maintainable.
415+
416+
To link an external file, set the key’s value to a path prefixed with `:/`, relative to your translations directory. For example, with default path `assets/translations` and locale `en-US`:
417+
418+
```json
419+
{
420+
"errors": ":/errors.json",
421+
"validation": ":/validation.json",
422+
"notifications": ":/notifications.json"
423+
}
424+
```
425+
426+
At runtime, Easy Localization will load:
427+
```
428+
assets
429+
└── translations
430+
└── en-US
431+
├── errors.json
432+
├── validation.json
433+
└── notifications.json
434+
```
435+
436+
Each linked file must contain a valid object of translation keys (of the file type you are using [Other file types](#-loading-translations-from-other-resources)).
437+
438+
Don't forget to add your linked files (or linked files folder, here assets/translations/en-US/), to your pubspec.yaml : [See installation](#-installation).
439+
410440
### 🔥 Reset locale `resetLocale()`
411441

412442
Reset locale to device locale

bin/audit/audit_command.dart

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import 'dart:convert';
22
import 'dart:io';
3-
3+
import 'package:easy_localization/src/linked_file_resolver.dart';
44
import 'package:path/path.dart';
5+
import 'package:easy_localization/src/file_loaders/io_file_loader.dart';
56

67
class AuditCommand {
7-
void run({required String transDir, required String srcDir}) {
8+
Future<void> run({required String transDir, required String srcDir}) async {
89
try {
910
final translationDir = Directory(transDir);
1011
final sourceDir = Directory(srcDir);
@@ -19,7 +20,7 @@ class AuditCommand {
1920
return;
2021
}
2122

22-
final allTranslations = _loadTranslations(translationDir);
23+
final allTranslations = await _loadTranslations(translationDir);
2324
final usedKeys = _scanSourceForKeys(sourceDir);
2425

2526
_report(allTranslations, usedKeys);
@@ -31,15 +32,30 @@ class AuditCommand {
3132
/// Walks [translationsDir], reads every `.json`, flattens nested maps
3233
/// into dot‑separated keys, and returns a map:
3334
/// { 'en': {'home.title', 'home.subtitle', …}, 'fr': { … } }
34-
Map<String, Set<String>> _loadTranslations(Directory translationsDir) {
35+
/// Also handles linked translation files (those containing ':/file.json' references)
36+
Future<Map<String, Set<String>>> _loadTranslations(Directory translationsDir) async {
3537
final result = <String, Set<String>>{};
38+
const IOFileLoader fileLoader = IOFileLoader();
39+
const LinkedFileResolver linkedFileResolver = JsonLinkedFileResolver(fileLoader: fileLoader);
40+
3641
for (var file in translationsDir.listSync().whereType<File>()) {
3742
if (!file.path.endsWith('.json')) continue;
3843

3944
try {
40-
final langCode = basenameWithoutExtension(file.path);
45+
final local = basenameWithoutExtension(file.path);
46+
final langCode = local.split('-').first;
47+
final hasCountryCode = local.split('-').length > 1;
48+
final countryCode = hasCountryCode ? local.split('-').last : null;
4149
final jsonMap = json.decode(file.readAsStringSync()) as Map<String, dynamic>;
42-
result[langCode] = _flatten(jsonMap);
50+
51+
// Process linked files if present using the shared resolver
52+
final resolvedJson = await linkedFileResolver.resolveLinkedFiles(
53+
basePath: translationsDir.path,
54+
languageCode: langCode,
55+
baseJson: jsonMap,
56+
countryCode: countryCode,
57+
);
58+
result[local] = _flatten(resolvedJson);
4359
} catch (e) {
4460
stderr.writeln('Error reading ${file.path}: $e');
4561
}

debug_linked.dart

Whitespace-only changes.

example/lib/generated/codegen_loader.g.dart

Lines changed: 16 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44

55
import 'dart:ui';
66

7-
import 'package:easy_localization/easy_localization.dart' show AssetLoader;
7+
import 'package:easy_localization/easy_localization.dart'
8+
show AssetLoader, JsonLinkedFileResolver, RootBundleFileLoader;
89

910
class CodegenLoader extends AssetLoader {
10-
const CodegenLoader();
11+
const CodegenLoader()
12+
: super(
13+
linkedFileResolver: const JsonLinkedFileResolver(fileLoader: RootBundleFileLoader()),
14+
fileLoader: const RootBundleFileLoader());
1115

1216
@override
1317
Future<Map<String, dynamic>> load(String fullPath, Locale locale) {
@@ -20,11 +24,7 @@ class CodegenLoader extends AssetLoader {
2024
"msg_named": "{} مكتوبة باللغة {lang}",
2125
"clickMe": "إضغط هنا",
2226
"profile": {
23-
"reset_password": {
24-
"label": "اعادة تعين كلمة السر",
25-
"username": "المستخدم",
26-
"password": "كلمة السر"
27-
}
27+
"reset_password": {"label": "اعادة تعين كلمة السر", "username": "المستخدم", "password": "كلمة السر"}
2828
},
2929
"clicked": {
3030
"zero": "لم تنقر بعد!",
@@ -55,11 +55,7 @@ class CodegenLoader extends AssetLoader {
5555
"msg_named": "{} مكتوبة باللغة {lang}",
5656
"clickMe": "إضغط هنا",
5757
"profile": {
58-
"reset_password": {
59-
"label": "اعادة تعين كلمة السر",
60-
"username": "المستخدم",
61-
"password": "كلمة السر"
62-
}
58+
"reset_password": {"label": "اعادة تعين كلمة السر", "username": "المستخدم", "password": "كلمة السر"}
6359
},
6460
"clicked": {
6561
"zero": "لم تنقر بعد!",
@@ -90,11 +86,7 @@ class CodegenLoader extends AssetLoader {
9086
"msg_named": "{} ist in {lang} geschrieben",
9187
"clickMe": "Click mich",
9288
"profile": {
93-
"reset_password": {
94-
"label": "Password zurücksetzten",
95-
"username": "Name",
96-
"password": "Password"
97-
}
89+
"reset_password": {"label": "Password zurücksetzten", "username": "Name", "password": "Password"}
9890
},
9991
"clicked": {
10092
"zero": "Du hast {} mal geklickt",
@@ -125,11 +117,7 @@ class CodegenLoader extends AssetLoader {
125117
"msg_named": "{} ist in {lang} geschrieben",
126118
"clickMe": "Click mich",
127119
"profile": {
128-
"reset_password": {
129-
"label": "Password zurücksetzten",
130-
"username": "Name",
131-
"password": "Password"
132-
}
120+
"reset_password": {"label": "Password zurücksetzten", "username": "Name", "password": "Password"}
133121
},
134122
"clicked": {
135123
"zero": "Du hast {} mal geklickt",
@@ -160,11 +148,7 @@ class CodegenLoader extends AssetLoader {
160148
"msg_named": "{} are written in the {lang} language",
161149
"clickMe": "Click me",
162150
"profile": {
163-
"reset_password": {
164-
"label": "Reset Password",
165-
"username": "Username",
166-
"password": "password"
167-
}
151+
"reset_password": {"label": "Reset Password", "username": "Username", "password": "password"}
168152
},
169153
"clicked": {
170154
"zero": "You clicked {} times!",
@@ -195,11 +179,7 @@ class CodegenLoader extends AssetLoader {
195179
"msg_named": "{} are written in the {lang} language",
196180
"clickMe": "Click me",
197181
"profile": {
198-
"reset_password": {
199-
"label": "Reset Password",
200-
"username": "Username",
201-
"password": "password"
202-
}
182+
"reset_password": {"label": "Reset Password", "username": "Username", "password": "password"}
203183
},
204184
"clicked": {
205185
"zero": "You clicked {} times!",
@@ -230,11 +210,7 @@ class CodegenLoader extends AssetLoader {
230210
"msg_named": "{} написан на языке {lang}",
231211
"clickMe": "Нажми на меня",
232212
"profile": {
233-
"reset_password": {
234-
"label": "Сбросить пароль",
235-
"username": "Логин",
236-
"password": "Пароль"
237-
}
213+
"reset_password": {"label": "Сбросить пароль", "username": "Логин", "password": "Пароль"}
238214
},
239215
"clicked": {
240216
"zero": "Ты кликнул {} раз!",
@@ -255,10 +231,7 @@ class CodegenLoader extends AssetLoader {
255231
"gender": {
256232
"male": "Привет мужык ;) ",
257233
"female": "Привет девчуля :)",
258-
"with_arg": {
259-
"male": "Привет мужык ;) {}",
260-
"female": "Привет девчуля :) {}"
261-
}
234+
"with_arg": {"male": "Привет мужык ;) {}", "female": "Привет девчуля :) {}"}
262235
},
263236
"reset_locale": "Сбросить язык"
264237
};
@@ -268,11 +241,7 @@ class CodegenLoader extends AssetLoader {
268241
"msg_named": "{} написан на языке {lang}",
269242
"clickMe": "Нажми на меня",
270243
"profile": {
271-
"reset_password": {
272-
"label": "Сбросить пароль",
273-
"username": "Логин",
274-
"password": "Пароль"
275-
}
244+
"reset_password": {"label": "Сбросить пароль", "username": "Логин", "password": "Пароль"}
276245
},
277246
"clicked": {
278247
"zero": "Ты кликнул {} раз!",
@@ -293,10 +262,7 @@ class CodegenLoader extends AssetLoader {
293262
"gender": {
294263
"male": "Привет мужык ;) ",
295264
"female": "Привет девчуля :)",
296-
"with_arg": {
297-
"male": "Привет мужык ;) {}",
298-
"female": "Привет девчуля :) {}"
299-
}
265+
"with_arg": {"male": "Привет мужык ;) {}", "female": "Привет девчуля :) {}"}
300266
},
301267
"reset_locale": "Сбросить язык"
302268
};

i18n/en-cyclic.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"test": "cyclic_test",
3+
"cycle_start": ":/cycle_file1.json"
4+
}

i18n/en-cyclic/cycle_file1.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"file1_value": "This is file 1",
3+
"link_to_file2": ":/cycle_file2.json"
4+
}

i18n/en-cyclic/cycle_file2.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"file2_value": "This is file 2",
3+
"link_back_to_file1": ":/cycle_file1.json"
4+
}

i18n/en-linked.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"test": "test_linked_en",
3+
"hello": "Hello",
4+
"app": {
5+
"name": "Test App",
6+
"errors": ":/errors.json"
7+
},
8+
"validation": ":/validation.json",
9+
"nested": {
10+
"module": {
11+
"messages": ":/nested/messages.json"
12+
}
13+
},
14+
"multiple": {
15+
"errors": ":/multi_errors.json",
16+
"validation": ":/multi_validation.json"
17+
},
18+
"deep_nested": ":/deep/level1.json"
19+
}

i18n/en-linked/deep/level1.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"level1_value": "This is level 1",
3+
"level2": ":/deep/level2.json"
4+
}

i18n/en-linked/deep/level2.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"level2_value": "This is level 2",
3+
"final_message": "Deep nesting works!"
4+
}

0 commit comments

Comments
 (0)