Repository: aissat/easy_localization

Issue:
Using plural(0) count as other
I have this json:
"test": {
        "zero": "0Test",
        "one": "1Test",
        "many": "many{digit} Tests",
        "other": "other{digit} Tests"
    }

when i use LocaleKeys.test.plural(0) it outputs as other


Discussion: Same here, it's a regression in version 3.0.4 (3.0.3 works correctly).
@aissat any update on this issue? Looks like you made a new release but this fix is not included... it's a major regression so I think it should have priority over new feature, right?
Same here, I have to downgrade it to 3.0.3
Same thing, still present in 3.0.5.

But this is not a bug, this is a feature 🤓

This is present since this PR: https://github.com/aissat/easy_localization/pull/620, which now takes into account pluralization according to language.
https://github.com/aissat/easy_localization/blob/63a39744ca18e38e51b5d30dbb63b95925b27ef5/lib/src/localization.dart#L140-L141

Pluralization is now based on the rules in `plural_rules.dart`. These rules are written from The [Unicode CLDR (Common Locale Data Repository)](https://www.unicode.org/cldr/charts/44/supplemental/language_plural_rules.html), and **only a few languages have a specific rule for zero**.

If your selected language does not have a specific rule for zero, it will use the "one" or "other" translation. Example for English:

```
PluralCase _en_rule() {
  if (_i == 0 || _i == 1) {
    return ONE;
  }
  return OTHER;
}
```

I used to do something like this, which does not work anymore:
```
{
    "zero": "No card",
    "one": "1 card",
    "other": "{} cards"
}
```

To fix this, we have to use it like this:
```
{
    "zero": "No card",
    "one": "{} card",
    "other": "{} cards"
}
```

As you can see, the bad news here is we can't no longer use "zero", unless the language has a specific rule.

To me, it will be nice to include "zero" to every language, to be able to display a custom translation in this case.

Fix patch:
diff --git a/.gitignore b/.gitignore
index ca455ab4..d5d4fa75 100644
--- a/.gitignore
+++ b/.gitignore
@@ -148,6 +148,7 @@ output.json
 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
 
 ### VisualStudioCode ###
+.vscode/
 .vscode/*
 !.vscode/settings.json
 !.vscode/tasks.json
@@ -203,6 +204,7 @@ obj/
 /out/
 
 # User-specific configurations
+.idea/
 .idea/caches/
 .idea/libraries/
 .idea/shelf/
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 53bc748f..402c0611 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -23,6 +23,7 @@ void main() async {
     // startLocale: Locale('de', 'DE'),
     // saveLocale: false,
     // useOnlyLangCode: true,
+    // ignorePluralRules: false,
 
     // optional assetLoader default used is RootBundleAssetLoader which uses flutter's assetloader
     // install easy_localization_loader for enable custom loaders
diff --git a/example/resources/langs/en-US.json b/example/resources/langs/en-US.json
index 7cee30b5..5a717468 100644
--- a/example/resources/langs/en-US.json
+++ b/example/resources/langs/en-US.json
@@ -11,12 +11,10 @@
         }
     },
     "clicked": {
-        "zero": "You clicked {} times!",
-        "one": "You clicked {} time!",
-        "two": "You clicked {} times!",
-        "few": "You clicked {} times!",
-        "many": "You clicked {} times!",
-        "other": "You clicked {} times!"
+        "zero": "You didn't click yet!",
+        "few": "You clicked a few times ({})!",
+        "many": "You clicked many times ({})!",
+        "other": "You clicked {} time(s)!"
     },
     "amount": {
         "zero": "Your amount : {} ",
diff --git a/lib/src/easy_localization_app.dart b/lib/src/easy_localization_app.dart
index 21629f49..e6acd2c1 100644
--- a/lib/src/easy_localization_app.dart
+++ b/lib/src/easy_localization_app.dart
@@ -63,6 +63,26 @@ class EasyLocalization extends StatefulWidget {
   /// ```
   final bool useFallbackTranslationsForEmptyResources;
 
+  /// Ignore usage of plural strings for languages that do not use plural rules.
+  /// @Default value false
+  /// Example:
+  /// ```
+  /// // Default behavior, use "zero" rule for 0 even if the language doesn't
+  /// // use it by default (e.g. "en"). If "zero" localization for that string
+  /// // doesn't exist, "other" is still used as fallback.
+  /// // "nTimes": "{count, plural, =0{never} =1{once} other{{count} times}}"
+  /// // Text(AppLocalizations.of(context)!.nTimes(_counter)),
+  /// // will print "never, once, 2 times" for ALL languages.
+  /// ignorePluralRules: true
+  /// // Use "zero" rule for 0 only if the language is set to do so (e.g. for
+  /// "lt" but not for "en").
+  /// // "nTimes": "{count, plural, =0{never} =1{once} other{{count} times}}"
+  /// // Text(AppLocalizations.of(context)!.nTimes(_counter)),
+  /// // will print "never, once, 2 times" ONLY for languages with plural rules.
+  /// ignorePluralRules: false
+  /// ```
+  final bool ignorePluralRules;
+
   /// Path to your folder with localization files.
   /// Example:
   /// ```dart
@@ -117,6 +137,7 @@ class EasyLocalization extends StatefulWidget {
     this.useOnlyLangCode = false,
     this.useFallbackTranslations = false,
     this.useFallbackTranslationsForEmptyResources = false,
+    this.ignorePluralRules = true,
     this.assetLoader = const RootBundleAssetLoader(),
     this.extraAssetLoaders,
     this.saveLocale = true,
@@ -198,6 +219,7 @@ class _EasyLocalizationState extends State<EasyLocalization> {
         supportedLocales: widget.supportedLocales,
         useFallbackTranslationsForEmptyResources:
             widget.useFallbackTranslationsForEmptyResources,
+        ignorePluralRules: widget.ignorePluralRules,
       ),
     );
   }
@@ -243,6 +265,7 @@ class _EasyLocalizationProvider extends InheritedWidget {
 
   /// Get fallback locale
   Locale? get fallbackLocale => parent.fallbackLocale;
+
   // Locale get startLocale => parent.startLocale;
 
   /// Change app locale
@@ -279,12 +302,14 @@ class _EasyLocalizationDelegate extends LocalizationsDelegate<Localization> {
   final List<Locale>? supportedLocales;
   final EasyLocalizationController? localizationController;
   final bool useFallbackTranslationsForEmptyResources;
+  final bool ignorePluralRules;
 
   ///  * use only the lang code to generate i18n file path like en.json or ar.json
   // final bool useOnlyLangCode;
 
   _EasyLocalizationDelegate({
     required this.useFallbackTranslationsForEmptyResources,
+    this.ignorePluralRules = true,
     this.localizationController,
     this.supportedLocales,
   }) {
@@ -307,6 +332,7 @@ class _EasyLocalizationDelegate extends LocalizationsDelegate<Localization> {
       fallbackTranslations: localizationController!.fallbackTranslations,
       useFallbackTranslationsForEmptyResources:
           useFallbackTranslationsForEmptyResources,
+      ignorePluralRules: ignorePluralRules,
     );
     return Future.value(Localization.instance);
   }
diff --git a/lib/src/localization.dart b/lib/src/localization.dart
index b153e5b1..55ef2f3c 100644
--- a/lib/src/localization.dart
+++ b/lib/src/localization.dart
@@ -20,11 +20,14 @@ class Localization {
   };
 
   bool _useFallbackTranslationsForEmptyResources = false;
+  bool _ignorePluralRules = false;
 
   Localization();
 
   static Localization? _instance;
+
   static Localization get instance => _instance ?? (_instance = Localization());
+
   static Localization? of(BuildContext context) =>
       Localizations.of<Localization>(context, Localization);
 
@@ -33,12 +36,14 @@ class Localization {
     Translations? translations,
     Translations? fallbackTranslations,
     bool useFallbackTranslationsForEmptyResources = false,
+    bool ignorePluralRules = true,
   }) {
     instance._locale = locale;
     instance._translations = translations;
     instance._fallbackTranslations = fallbackTranslations;
     instance._useFallbackTranslationsForEmptyResources =
         useFallbackTranslationsForEmptyResources;
+    instance._ignorePluralRules = ignorePluralRules;
     return translations == null ? false : true;
   }
 
@@ -114,6 +119,9 @@ class Localization {
   }
 
   static PluralRule? _pluralRule(String? locale, num howMany) {
+    if (instance._ignorePluralRules) {
+      return () => _pluralCaseFallback(howMany);
+    }
     startRuleEvaluation(howMany);
     return pluralRules[locale];
   }
@@ -139,7 +147,6 @@ class Localization {
     String? name,
     NumberFormat? format,
   }) {
-
     late String res;
 
     final pluralRule = _pluralRule(_locale.languageCode, value);


Test patch:
diff --git a/test/easy_localization_language_specific_test.dart b/test/easy_localization_language_specific_test.dart
index 2dce5467..27e3f6e3 100644
--- a/test/easy_localization_language_specific_test.dart
+++ b/test/easy_localization_language_specific_test.dart
@@ -8,58 +8,86 @@ import 'package:flutter_test/flutter_test.dart';
 import 'utils/test_asset_loaders.dart';
 
 void main() {
-    group('language-specific-plurals', () {
-      var r = EasyLocalizationController(
-          forceLocale: const Locale('fb'),
-          supportedLocales: [const Locale('en'), const Locale('ru'), const Locale('fb')],
-          fallbackLocale: const Locale('fb'),
-          path: 'path',
-          useOnlyLangCode: true,
-          useFallbackTranslations: true,
-          onLoadError: (FlutterError e) {
-            log(e.toString());
-          },
-          saveLocale: false,
-          assetLoader: const JsonAssetLoader());
+  group('language-specific-plurals', () {
+    var r = EasyLocalizationController(
+        forceLocale: const Locale('fb'),
+        supportedLocales: [
+          const Locale('en'),
+          const Locale('ru'),
+          const Locale('fb')
+        ],
+        fallbackLocale: const Locale('fb'),
+        path: 'path',
+        useOnlyLangCode: true,
+        useFallbackTranslations: true,
+        onLoadError: (FlutterError e) {
+          log(e.toString());
+        },
+        saveLocale: false,
+        assetLoader: const JsonAssetLoader());
 
-      setUpAll(() async {
-        await r.loadTranslations();
-        
-      });
+    setUpAll(() async {
+      await r.loadTranslations();
+    });
 
-      test('english one', () async {
-        Localization.load(const Locale('en'),
-            translations: r.translations,
-            fallbackTranslations: r.fallbackTranslations);
-        expect(Localization.instance.plural('hat', 1), 'one hat');
-      }); 
-      test('english other', () async {
-        Localization.load(const Locale('en'),
-            translations: r.translations,
-            fallbackTranslations: r.fallbackTranslations);
-        expect(Localization.instance.plural('hat', 2), 'other hats');
-        expect(Localization.instance.plural('hat', 0), 'other hats');
-        expect(Localization.instance.plural('hat', 3), 'other hats');
-      }); 
-      test('russian one', () async {
-        Localization.load(const Locale('ru'),
-            translations: r.translations,
-            fallbackTranslations: r.fallbackTranslations);
-        expect(Localization.instance.plural('hat', 1), 'one hat');
-      }); 
-      test('russian few', () async {
-        Localization.load(const Locale('ru'),
-            translations: r.translations,
-            fallbackTranslations: r.fallbackTranslations);
-        expect(Localization.instance.plural('hat', 2), 'few hats');
-        expect(Localization.instance.plural('hat', 3), 'few hats');
-      });
-      test('russian many', () async {
-        Localization.load(const Locale('ru'),
-            translations: r.translations,
-            fallbackTranslations: r.fallbackTranslations);
-        expect(Localization.instance.plural('hat', 0), 'many hats');
-        expect(Localization.instance.plural('hat', 5), 'many hats');
-      });
+    test('english one', () async {
+      Localization.load(const Locale('en'),
+          translations: r.translations,
+          fallbackTranslations: r.fallbackTranslations);
+      expect(Localization.instance.plural('hat', 1), 'one hat');
+    });
+    test('english other (default)', () async {
+      Localization.load(const Locale('en'),
+          translations: r.translations,
+          fallbackTranslations: r.fallbackTranslations);
+      expect(Localization.instance.plural('hat', 2), 'two hats');
+      expect(Localization.instance.plural('hat', 0), 'no hats');
+      expect(Localization.instance.plural('hat', 3), 'other hats');
+    });
+    test('english other (with ignorePluralRules)', () async {
+      Localization.load(const Locale('en'),
+          translations: r.translations,
+          fallbackTranslations: r.fallbackTranslations,
+          ignorePluralRules: false);
+      expect(Localization.instance.plural('hat', 2), 'other hats');
+      expect(Localization.instance.plural('hat', 0), 'other hats');
+      expect(Localization.instance.plural('hat', 3), 'other hats');
+    });
+    test('russian one', () async {
+      Localization.load(const Locale('ru'),
+          translations: r.translations,
+          fallbackTranslations: r.fallbackTranslations);
+      expect(Localization.instance.plural('hat', 1), 'one hat');
+    });
+    test('russian few (default)', () async {
+      Localization.load(const Locale('ru'),
+          translations: r.translations,
+          fallbackTranslations: r.fallbackTranslations);
+      expect(Localization.instance.plural('hat', 2), 'two hats');
+      expect(Localization.instance.plural('hat', 3), 'other hats');
+    });
+    test('russian few (with ignorePluralRules)', () async {
+      Localization.load(const Locale('ru'),
+          translations: r.translations,
+          fallbackTranslations: r.fallbackTranslations,
+          ignorePluralRules: false);
+      expect(Localization.instance.plural('hat', 2), 'few hats');
+      expect(Localization.instance.plural('hat', 3), 'few hats');
+    });
+    test('russian many (default)', () async {
+      Localization.load(const Locale('ru'),
+          translations: r.translations,
+          fallbackTranslations: r.fallbackTranslations);
+      expect(Localization.instance.plural('hat', 0), 'no hats');
+      expect(Localization.instance.plural('hat', 5), 'other hats');
+    });
+    test('russian many (with ignorePluralRules)', () async {
+      Localization.load(const Locale('ru'),
+          translations: r.translations,
+          fallbackTranslations: r.fallbackTranslations,
+          ignorePluralRules: false);
+      expect(Localization.instance.plural('hat', 0), 'many hats');
+      expect(Localization.instance.plural('hat', 5), 'many hats');
     });
-}
\ No newline at end of file
+  });
+}
diff --git a/test/easy_localization_widget_test.dart b/test/easy_localization_widget_test.dart
index 3444837d..895ecbfa 100644
--- a/test/easy_localization_widget_test.dart
+++ b/test/easy_localization_widget_test.dart
@@ -5,9 +5,9 @@ import 'package:easy_localization/src/exceptions.dart';
 import 'package:easy_localization/src/localization.dart';
 import 'package:easy_logger/easy_logger.dart';
 import 'package:flutter/material.dart';
-
 import 'package:flutter_test/flutter_test.dart';
 import 'package:shared_preferences/shared_preferences.dart';
+
 import 'utils/test_asset_loaders.dart';
 
 late BuildContext _context;
@@ -285,6 +285,7 @@ void main() async {
         await tester.pumpWidget(EasyLocalization(
           path: '../../i18n',
           supportedLocales: const [Locale('en', 'US'), Locale('ar', 'DZ')],
+          ignorePluralRules: false,
           child: const MyApp(),
         ));
 
@@ -297,8 +298,10 @@ void main() async {
 
         await tester.pump();
 
-        expect(EasyLocalization.of(_context)!.supportedLocales,
-            [const Locale('en', 'US'), const Locale('ar', 'DZ')]);
+        expect(EasyLocalization.of(_context)!.supportedLocales, [
+          const Locale('en', 'US'),
+          const Locale('ar', 'DZ'),
+        ]);
         expect(EasyLocalization.of(_context)!.locale, const Locale('ar', 'DZ'));
 
         var trFinder = find.text('اختبار');