Skip to content

Commit 856fbac

Browse files
authored
Merge branch 'aissat:develop' into develop
2 parents 6574a97 + c818108 commit 856fbac

16 files changed

+609
-25
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
### [3.0.8]
4+
5+
- code audit and maintenance updates
6+
- improved project structure and CI/CD workflows
7+
38
### [3.0.7]
49

510
- add _supportedLocales in EasyLocalizationController; log and check the deviceLocale when resetLocale;

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,29 @@ var money = plural('money_named_args', 10.23, namedArgs: {'name': 'Jane', 'money
317317
var money = plural('money_named_args', 10.23, namedArgs: {'name': 'Jane'}, name: 'money') // output: Jane has 10.23 dollars
318318
```
319319

320+
### ⚙️ Configuring Plural Rules with `ignorePluralRules`
321+
322+
In some languages, pluralization is simple and only involves using zero, one, two, and other forms, without needing to handle the `few` or `many` categories.
323+
By default, `easy_localization` ignores the `few` and `many` plural forms and uses just the zero, one, two, and other forms.
324+
325+
If you want to enable the handling of the `few` and `many` plural categories for specific languages, you can configure the `ignorePluralRules` flag to `false` in the `EasyLocalization` initialization.
326+
327+
Here’s how to configure it:
328+
329+
```dart
330+
EasyLocalization(
331+
ignorePluralRules: false, // Set this line to false to enable 'few' and 'many' plural categories
332+
supportedLocales: [Locale('en', 'US'), Locale('de', 'DE')],
333+
path: 'assets/translations',
334+
fallbackLocale: Locale('en', 'US'),
335+
child: MyApp()
336+
)
337+
```
338+
339+
Setting `ignorePluralRules: false` will enable the `few` and `many` plural categories, allowing your translations to handle all plural forms, including `few` and `many`, for supported languages.
340+
341+
342+
320343
### 🔥 Linked translations:
321344

322345
If there's a translation key that will always have the same concrete text as another one you can just link to it. To link to another translation key, all you have to do is to prefix its contents with an `@:` sign followed by the full name of the translation key including the namespace you want to link to.
@@ -519,6 +542,21 @@ print(LocaleKeys.title.tr()); //String
519542
Text(LocaleKeys.title).tr(); //Widget
520543
```
521544

545+
### ✅ Audit missing keys
546+
547+
If you prefer to not generate keys you can see an audit of your translation keys to see the one present in your app code but not in your translations file by running the audit command.
548+
549+
```
550+
flutter pub run easy_localization:audit
551+
```
552+
553+
If you are not using the default translations folder path (assets/translations) or the lib folder for your code you can specify your custom paths :
554+
555+
| Arguments | Short | Default | Description |
556+
| ---------------------------- | ----- | --------------------- | --------------------------------------------------------------------------- |
557+
| --translations-dir | -t | assets/translations | Folder containing localization files |
558+
| --source-dir | -s | lib | Folder containing the app code files |
559+
522560
## 🖨️ Logger
523561
524562
[Easy Localization] logger based on [Easy Logger]

bin/audit.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import 'dart:io';
2+
3+
import 'package:args/args.dart';
4+
import 'audit/audit_command.dart';
5+
6+
void main(List<String> args) {
7+
final actual = args.isEmpty ? ['audit'] : args;
8+
var parser = ArgParser();
9+
10+
parser.addOption('translations-dir', abbr: 't', defaultsTo: 'assets/translations');
11+
parser.addOption('source-dir', abbr: 's', defaultsTo: 'lib');
12+
13+
try {
14+
var argResults = parser.parse(actual);
15+
final transDir = argResults['translations-dir'] as String;
16+
final srcDir = argResults['source-dir'] as String;
17+
18+
if (!Directory(transDir).existsSync()) {
19+
stderr.writeln('Error: Translation directory "$transDir" does not exist.');
20+
exit(1);
21+
}
22+
23+
if (!Directory(srcDir).existsSync()) {
24+
stderr.writeln('Error: Source directory "$srcDir" does not exist.');
25+
exit(1);
26+
}
27+
28+
AuditCommand().run(transDir: transDir, srcDir: srcDir);
29+
} catch (e) {
30+
stderr.writeln('Error: $e');
31+
exit(1);
32+
}
33+
}

bin/audit/audit_command.dart

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
4+
import 'package:path/path.dart';
5+
6+
class AuditCommand {
7+
void run({required String transDir, required String srcDir}) {
8+
try {
9+
final translationDir = Directory(transDir);
10+
final sourceDir = Directory(srcDir);
11+
12+
if (!translationDir.existsSync()) {
13+
stderr.writeln('Error: Translation directory "$transDir" does not exist.');
14+
return;
15+
}
16+
17+
if (!sourceDir.existsSync()) {
18+
stderr.writeln('Error: Source directory "$srcDir" does not exist.');
19+
return;
20+
}
21+
22+
final allTranslations = _loadTranslations(translationDir);
23+
final usedKeys = _scanSourceForKeys(sourceDir);
24+
25+
_report(allTranslations, usedKeys);
26+
} catch (e) {
27+
stderr.writeln('Error during audit: $e');
28+
}
29+
}
30+
31+
/// Walks [translationsDir], reads every `.json`, flattens nested maps
32+
/// into dot‑separated keys, and returns a map:
33+
/// { 'en': {'home.title', 'home.subtitle', …}, 'fr': { … } }
34+
Map<String, Set<String>> _loadTranslations(Directory translationsDir) {
35+
final result = <String, Set<String>>{};
36+
for (var file in translationsDir.listSync().whereType<File>()) {
37+
if (!file.path.endsWith('.json')) continue;
38+
39+
try {
40+
final langCode = basenameWithoutExtension(file.path);
41+
final jsonMap = json.decode(file.readAsStringSync()) as Map<String, dynamic>;
42+
result[langCode] = _flatten(jsonMap);
43+
} catch (e) {
44+
stderr.writeln('Error reading ${file.path}: $e');
45+
}
46+
}
47+
return result;
48+
}
49+
50+
Set<String> _flatten(Map<String, dynamic> json, [String parentKey = '']) {
51+
final keys = <String>{};
52+
for (var entry in json.entries) {
53+
final key = entry.key;
54+
final value = entry.value;
55+
56+
final newKey = parentKey.isEmpty ? key : '$parentKey.$key';
57+
if (value is String) {
58+
keys.add(newKey);
59+
continue;
60+
}
61+
62+
if (value is Map<String, dynamic>) {
63+
keys.addAll(_flatten(value, newKey));
64+
continue;
65+
}
66+
67+
if (value is List || value is num || value is bool) {
68+
keys.add(newKey);
69+
}
70+
}
71+
return keys;
72+
}
73+
74+
Set<String> _scanSourceForKeys(Directory srcDir) {
75+
List<RegExp> keyPatterns = [
76+
// 1) tr('foo.bar') or tr("foo.bar"), with optional args/comma before the )
77+
RegExp(r"""\btr\s*\(\s*['"]([^'"]+)['"](?:\s*,[^)]*)?\)"""),
78+
79+
// 2) context.tr('foo.bar') same as above but with the context qualifier
80+
RegExp(r"""context\s*\.\s*tr\s*\(\s*['"]([^'"]+)['"](?:\s*,[^)]*)?\)"""),
81+
82+
// 3) 'foo.bar'.tr() or "foo.bar".tr(), allowing whitespace/newlines
83+
RegExp(r"""['"]([^'"]+)['"]\s*\.\s*tr\s*\(\s*[^)]*\)"""),
84+
85+
// 4) generated keys: LocaleKeys.foo_bar (whitespace around the dot ok)
86+
RegExp(r"""LocaleKeys\s*\.\s*([A-Za-z0-9_]+)"""),
87+
88+
// 5) plural() calls
89+
RegExp(r"""\bplural\s*\(\s*['"]([^'"]+)['"](?:\s*,[^)]*)?\)"""),
90+
91+
// 6) context.plural() calls
92+
RegExp(r"""context\s*\.\s*plural\s*\(\s*['"]([^'"]+)['"](?:\s*,[^)]*)?\)"""),
93+
];
94+
95+
final used = <String>{};
96+
97+
for (var file in srcDir.listSync(recursive: true).whereType<File>().where((f) => f.path.endsWith('.dart'))) {
98+
try {
99+
final content = file.readAsStringSync();
100+
for (var pattern in keyPatterns) {
101+
final matches = pattern.allMatches(content);
102+
for (var match in matches) {
103+
if (match.groupCount > 0) {
104+
String key = match.group(1)!;
105+
if (pattern.pattern.contains('LocaleKeys')) {
106+
key = key.replaceAll('_', '.');
107+
}
108+
used.add(key);
109+
}
110+
}
111+
}
112+
} catch (e) {
113+
stderr.writeln('Error reading ${file.path}: $e');
114+
}
115+
}
116+
117+
return used;
118+
}
119+
120+
void _report(Map<String, Set<String>> allTranslations, Set<String> usedKeys) {
121+
stderr.writeln('=== Keys Audit ===');
122+
123+
for (var lang in allTranslations.keys) {
124+
final keysInFile = allTranslations[lang]!;
125+
final missing = usedKeys.difference(keysInFile);
126+
final missingWithVariables = missing.where((key) => key.contains('\$')).toList();
127+
final missingWithoutVariables = missing.where((key) => !key.contains('\$')).toList();
128+
129+
stderr.writeln('\nLanguage: $lang');
130+
if (missingWithVariables.isEmpty && missingWithoutVariables.isEmpty) {
131+
stderr.writeln(' ✅ all good!');
132+
}
133+
134+
if (missingWithoutVariables.isNotEmpty) {
135+
stderr.writeln(' 🔴 Missing (${missingWithoutVariables.length}):');
136+
for (var key in missingWithoutVariables) {
137+
stderr.writeln(' – $key');
138+
}
139+
140+
stderr.writeln('\n');
141+
}
142+
143+
if (missingWithVariables.isNotEmpty) {
144+
stderr.writeln(' 🟡 Missing with variables (${missingWithVariables.length}):');
145+
stderr.writeln(' These keys may not be missing as they contain variables that cannot be verified.');
146+
for (var key in missingWithVariables) {
147+
stderr.writeln(' – $key');
148+
}
149+
}
150+
}
151+
}
152+
}

bin/generate.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ Future _writeKeys(StringBuffer classBuilder, List<FileSystemEntity> files,
175175
var file = '''
176176
// DO NOT EDIT. This is code generated via package:easy_localization/generate.dart
177177
178+
// ignore_for_file: constant_identifier_names
179+
178180
abstract class LocaleKeys {
179181
''';
180182

@@ -234,7 +236,7 @@ Future _writeJson(
234236
var gFile = '''
235237
// DO NOT EDIT. This is code generated via package:easy_localization/generate.dart
236238
237-
// ignore_for_file: prefer_single_quotes, avoid_renaming_method_parameters
239+
// ignore_for_file: prefer_single_quotes, avoid_renaming_method_parameters, constant_identifier_names
238240
239241
import 'dart:ui';
240242
@@ -255,13 +257,13 @@ class CodegenLoader extends AssetLoader{
255257
for (var file in files) {
256258
final localeName =
257259
path.basename(file.path).replaceFirst('.json', '').replaceAll('-', '_');
258-
listLocales.add('"$localeName": $localeName');
260+
listLocales.add('"$localeName": _$localeName');
259261
final fileData = File(file.path);
260262

261263
Map<String, dynamic>? data = json.decode(await fileData.readAsString());
262264

263265
final mapString = const JsonEncoder.withIndent(' ').convert(data);
264-
gFile += 'static const Map<String,dynamic> $localeName = $mapString;\n';
266+
gFile += 'static const Map<String,dynamic> _$localeName = $mapString;\n';
265267
}
266268

267269
gFile +=

i18n/en-US.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"test": "test",
2+
"test": "test_en-US",
33
"day": {
44
"zero": "{} days",
55
"one": "{} day",

i18n/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"test": "test",
2+
"test": "test_en",
33
"day": {
44
"zero": "{} days",
55
"one": "{} day",

lib/src/easy_localization_app.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import 'package:easy_logger/easy_logger.dart';
66
import 'package:flutter/material.dart';
77
import 'package:flutter_localizations/flutter_localizations.dart';
88

9-
import 'asset_loader.dart';
109
import 'localization.dart';
1110

1211
part 'utils.dart';
@@ -230,6 +229,7 @@ class _EasyLocalizationProvider extends InheritedWidget {
230229
final EasyLocalizationController _localeState;
231230
final Locale? currentLocale;
232231
final _EasyLocalizationDelegate delegate;
232+
final bool _translationsLoaded;
233233

234234
/// {@macro flutter.widgets.widgetsApp.localizationsDelegates}
235235
///
@@ -256,6 +256,7 @@ class _EasyLocalizationProvider extends InheritedWidget {
256256
_EasyLocalizationProvider(this.parent, this._localeState,
257257
{Key? key, required this.delegate})
258258
: currentLocale = _localeState.locale,
259+
_translationsLoaded = _localeState.translations != null,
259260
super(key: key, child: parent.child) {
260261
EasyLocalization.logger.debug('Init provider');
261262
}
@@ -291,7 +292,8 @@ class _EasyLocalizationProvider extends InheritedWidget {
291292

292293
@override
293294
bool updateShouldNotify(_EasyLocalizationProvider oldWidget) {
294-
return oldWidget.currentLocale != locale;
295+
return oldWidget.currentLocale != locale
296+
|| oldWidget._translationsLoaded != _translationsLoaded;
295297
}
296298

297299
static _EasyLocalizationProvider? of(BuildContext context) =>

lib/src/easy_localization_controller.dart

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,29 @@ class EasyLocalizationController extends ChangeNotifier {
7272
}) {
7373
final selectedLocale = supportedLocales.firstWhere(
7474
(locale) => locale.supports(deviceLocale),
75-
orElse: () => _getFallbackLocale(supportedLocales, fallbackLocale),
75+
orElse: () => _getFallbackLocale(
76+
supportedLocales,
77+
fallbackLocale,
78+
deviceLocale: deviceLocale,
79+
),
7680
);
7781
return selectedLocale;
7882
}
7983

8084
//Get fallback Locale
8185
static Locale _getFallbackLocale(
82-
List<Locale> supportedLocales, Locale? fallbackLocale) {
86+
List<Locale> supportedLocales, Locale? fallbackLocale,
87+
{final Locale? deviceLocale}) {
88+
if (deviceLocale != null) {
89+
// a locale that matches the language code of the device locale is
90+
// preferred over the fallback locale
91+
final deviceLanguage = deviceLocale.languageCode;
92+
for (Locale locale in supportedLocales) {
93+
if (locale.languageCode == deviceLanguage) {
94+
return locale;
95+
}
96+
}
97+
}
8398
//If fallbackLocale not set then return first from supportedLocales
8499
if (fallbackLocale != null) {
85100
return fallbackLocale;
@@ -109,6 +124,7 @@ class EasyLocalizationController extends ChangeNotifier {
109124
}
110125
_fallbackTranslations = Translations(data);
111126
}
127+
notifyListeners();
112128
} on FlutterError catch (e) {
113129
onLoadError(e);
114130
} catch (e) {

lib/src/public.dart

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,12 @@ String tr(
4545
.tr(key, args: args, namedArgs: namedArgs, gender: gender);
4646
}
4747

48-
bool trExists(String key) {
49-
return Localization.instance
50-
.exists(key);
48+
bool trExists(String key, {BuildContext? context}) {
49+
return context != null
50+
? Localization.of(context)!
51+
.exists(key)
52+
: Localization.instance
53+
.exists(key);
5154
}
5255

5356
/// {@template plural}

0 commit comments

Comments
 (0)