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 15 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ flutter:
- assets/translations/
```
### 🔌 Loading translations from other resources
You can use JSON,CSV,HTTP,XML,Yaml files, etc.
Expand Down Expand Up @@ -407,6 +409,36 @@ Output:
print('example.emptyNameError'.tr()); //Output: Please fill in your full name
```

### 🔥 Linked files:

> ⚠ This is only available for the default asset loader (on Json Files).

You can split translations for a single locale into multiple files by using linked files. This helps keep your JSON clean and maintainable.

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`:

```json
{
"errors": ":/errors.json",
"validation": ":/validation.json",
"notifications": ":/notifications.json"
}
```

At runtime, Easy Localization will load:
```
assets
└── translations
└── en-US
├── errors.json
├── validation.json
└── notifications.json
```

Each linked file must contain a valid JSON object of translation keys.

Don't forget to add your linked files (or linked files folder, here assets/translations/en-US/), to your pubspec.yaml : [See installation](#-installation).

### 🔥 Reset locale `resetLocale()`

Reset locale to device locale
Expand Down
75 changes: 74 additions & 1 deletion lib/src/asset_loader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,89 @@ abstract class AssetLoader {
/// default used is RootBundleAssetLoader which uses flutter's assetloader
///
class RootBundleAssetLoader extends AssetLoader {
// Place inside class RootBundleAssetLoader
static const int _maxLinkedDepth = 32;

const RootBundleAssetLoader();

String getLocalePath(String basePath, Locale locale) {
return '$basePath/${locale.toStringWithSeparator(separator: "-")}.json';
}

String _getLinkedLocalePath(String basePath, String filePath, Locale locale) {
return '$basePath/${locale.toStringWithSeparator(separator: "-")}/$filePath';
}

Future<Map<String, dynamic>> _getLinkedTranslationFileDataFromBaseJson(
String basePath,
Locale locale,
Map<String, dynamic> baseJson, {
required Set<String> visited,
int depth = 0,
}) async {
if (depth > _maxLinkedDepth) {
throw StateError('Maximum linked files depth ($_maxLinkedDepth) exceeded for $locale at $basePath.');
}

final Map<String, dynamic> fullJson = Map<String, dynamic>.from(baseJson);

for (final entry in baseJson.entries) {
final key = entry.key;
var value = entry.value;

if (value is String && value.startsWith(':/')) {
final rawPath = value.substring(2).trim();
final linkedAssetPath = _getLinkedLocalePath(basePath, rawPath, locale);

if (visited.contains(linkedAssetPath)) {
throw StateError('Cyclic linked files detected at "$linkedAssetPath" (key: "$key").');
}

final Map<String, dynamic> linkedJson =
json.decode(await rootBundle.loadString(linkedAssetPath)) as Map<String, dynamic>;

visited.add(linkedAssetPath);
try {
final resolved = await _getLinkedTranslationFileDataFromBaseJson(
basePath,
locale,
linkedJson,
visited: visited,
depth: depth + 1,
);
fullJson[key] = resolved;
} catch (e) {
throw StateError(
'Error resolving linked file "$linkedAssetPath" for key "$key": $e',
);
}
}

if (value is Map<String, dynamic>) {
fullJson[key] = await _getLinkedTranslationFileDataFromBaseJson(
basePath,
locale,
value,
visited: visited,
depth: depth + 1,
);
}
}

return fullJson;
}

@override
Future<Map<String, dynamic>?> load(String path, Locale locale) async {
var localePath = getLocalePath(path, locale);
EasyLocalization.logger.debug('Load asset from $path');
return json.decode(await rootBundle.loadString(localePath));

Map<String, dynamic> baseJson = json.decode(await rootBundle.loadString(localePath));
return await _getLinkedTranslationFileDataFromBaseJson(
path,
locale,
baseJson,
visited: <String>{},
);
}
}
38 changes: 9 additions & 29 deletions test/easy_localization_context_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,7 @@ void main() async {
saveLocale: false,
useOnlyLangCode: true,
// fallbackLocale:Locale('en') ,
supportedLocales: const [
Locale('ar')
], // Locale('en', 'US'), Locale('ar','DZ')
supportedLocales: const [Locale('ar')], // Locale('en', 'US'), Locale('ar','DZ')
child: const MyApp(),
));
// await tester.idle();
Expand Down Expand Up @@ -117,10 +115,7 @@ void main() async {
await tester.pumpWidget(EasyLocalization(
path: '../../i18n',
// fallbackLocale:Locale('en') ,
supportedLocales: const [
Locale('en', 'US'),
Locale('ar', 'DZ')
], // Locale('en', 'US'), Locale('ar','DZ')
supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], // Locale('en', 'US'), Locale('ar','DZ')
child: const MyApp(),
));
// await tester.idle();
Expand All @@ -140,13 +135,10 @@ void main() async {
await tester.pumpWidget(EasyLocalization(
path: '../../i18n',
// fallbackLocale:Locale('en') ,
supportedLocales: const [
Locale('en', 'US'),
Locale('ar', 'DZ')
], // Locale('en', 'US'), Locale('ar','DZ')
supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], // Locale('en', 'US'), Locale('ar','DZ')
child: const MyApp(),
));
// await tester.idle();
await tester.idle();
// The async delegator load will require build on the next frame. Thus, pump
await tester.pump();

Expand All @@ -161,13 +153,10 @@ void main() async {
await tester.runAsync(() async {
await tester.pumpWidget(EasyLocalization(
path: '../../i18n',
supportedLocales: const [
Locale('en', 'US'),
Locale('ar', 'DZ')
], // Locale('en', 'US'), Locale('ar','DZ')
supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], // Locale('en', 'US'), Locale('ar','DZ')
child: const MyApp(),
));
// await tester.idle();
await tester.idle();
// The async delegator load will require build on the next frame. Thus, pump
await tester.pump();

Expand All @@ -182,10 +171,7 @@ void main() async {
await tester.runAsync(() async {
await tester.pumpWidget(EasyLocalization(
path: '../../i18n',
supportedLocales: const [
Locale('en', 'US'),
Locale('ar', 'DZ')
], // Locale('en', 'US'), Locale('ar','DZ')
supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], // Locale('en', 'US'), Locale('ar','DZ')
startLocale: const Locale('ar', 'DZ'),
child: const MyApp(),
));
Expand All @@ -208,10 +194,7 @@ void main() async {
await tester.runAsync(() async {
await tester.pumpWidget(EasyLocalization(
path: '../../i18n',
supportedLocales: const [
Locale('en', 'US'),
Locale('ar', 'DZ')
], // Locale('en', 'US'), Locale('ar','DZ')
supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], // Locale('en', 'US'), Locale('ar','DZ')
child: const MyApp(),
));
await tester.idle();
Expand All @@ -229,10 +212,7 @@ void main() async {
await tester.runAsync(() async {
await tester.pumpWidget(EasyLocalization(
path: '../../i18n',
supportedLocales: const [
Locale('en', 'US'),
Locale('ar', 'DZ')
], // Locale('en', 'US'), Locale('ar','DZ')
supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')], // Locale('en', 'US'), Locale('ar','DZ')
startLocale: const Locale('ar', 'DZ'),
child: const MyApp(),
));
Expand Down
Loading