Skip to content

Localizations no longer works after upgrade to the latest version of flutter i18n plugin #26

Closed
@LucioD

Description

@LucioD

Hi to all,
on a my Flutter app I use the flutter i18n plugin for texts localizations, with two supported languages: Italian and English (the English is also the fallback language). This localizations until a few days ago worked properly, but after the upgrade of the i18n plugin to the latest version 0.1.1 it stopped working, and all the app's texts are now always displayed in English (the fallback language ) even if on my Android device the language is set to Italian ('it'), and before this upgrade the texts were correctly localized in Italian.

I did not change anything in the code, but I realized that now, if I call (for test) Localizations.localeOf (context), it always returns the 'en_' Locale, even if on the device I set the French language or Italian language, or any other language other than English.

This is clearly a mistake. I'm not sure, but I think this error came out after upgrading to version 0.1.1 of the flutter i18n plugin in my Android Studio (version 3.2.1) development environment.

Steps to Reproduce

This is the interestings code:

@override
  Widget build(BuildContext context) {
    Locale currentLocale = Localizations.localeOf(context);
    String localizationLanguageCode = S.delegate.isSupported(currentLocale) ? currentLocale.languageCode : Config.defaultLanguageCode;
    print('currentLocale is: ${currentLocale.toString()}, currentLocale.languageCode is: ${currentLocale.languageCode}, localizationLanguageCode is: $localizationLanguageCode');
    return MaterialApp(
      localizationsDelegates: [
        S.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: S.delegate.supportedLocales,
      localeResolutionCallback: S.delegate.resolution(fallback: new Locale('${Config.defaultLanguageCode}', '')),
      home: Scaffold(
        appBar: _makeAppBar(context),
        body: FutureBuilder<List<Lesson>>(
          future: fetchLessons(http.Client(), selectedCourse, localizationLanguageCode),
          //future: listLessons,
          builder: (context, snapshot) {
            this.numBuild++;
            print('number of build of LessonsRoute: ${this.numBuild}...');
            if (snapshot.hasError)  {
              print('Snaphot has the following error: ${snapshot.error}');
              return Center(
                  child: Text('Snaphot has the following error: ${snapshot.error}')
              );
            } else {
              if (snapshot.hasData) {
                if (snapshot.data != null && courses != null) {
                  return ListViewLessons(lessons: snapshot.data, courses: courses);
                } else {
                  return Center(child: Text('snapshot.data is null or/and courses is null!'));
                }
              } else {
                return Center(child: CircularProgressIndicator());
              }
            }
          },
        ),
      ),
    );
  }

In the above code, the following test instructions:

 Locale currentLocale = Localizations.localeOf(context);
 String localizationLanguageCode = S.delegate.isSupported(currentLocale) ? currentLocale.languageCode : Config.defaultLanguageCode;
 print('currentLocale is: ${currentLocale.toString()}, currentLocale.languageCode is: ${currentLocale.languageCode}, localizationLanguageCode is: $localizationLanguageCode');

Produces the following result:
`I/flutter ( 4432): currentLocale is: en_, currentLocale.languageCode is: en, localizationLanguageCode is: en

But, on my device (Samsung Galaxy S5 with Android 6.0.1) the language is set to Italian, not to English! See attached screenshots. Even with the ADVs emulators, I now have the same problem.

So, it seems that some internal i18n call to Localizations.localeOf (context) or similar, returns now a wrong result (always English Locale), which prevents localizations in other languages.

What happened? How can I make the localizations of texts on my app work again?

Also, referring to (flutter/flutter#24747) might help which probably explains what broke flutter_i18n?

Thanks in advance for the help you want to give me.

Flutter Doctor

[v] Flutter (Channel beta, v0.11.9, on Microsoft Windows [Versione 10.0.17134.407], locale it-IT)
    • Flutter version 0.11.9 at c:\src\flutter
    • Framework revision d48e6e433c (2 days ago), 2018-11-20 22:05:23 -0500
    • Engine revision 5c8147450d
    • Dart version 2.1.0 (build 2.1.0-dev.9.4 f9ebf21297)

[v] Android toolchain - develop for Android devices (Android SDK 28.0.3)
    • Android SDK at C:\Users\lucio\AppData\Local\Android\sdk
    • Android NDK location not configured (optional; useful for native profiling support)
    • Platform android-28, build-tools 28.0.3
    • Java binary at: C:\Program Files\Android\Android Studio1\jre\bin\java
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1136-b06)
    • All Android licenses accepted.

[v] Android Studio (version 3.2)
    • Android Studio at C:\Program Files\Android\Android Studio1
    • Flutter plugin version 30.0.1
    • Dart plugin version 181.5656
    • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1136-b06)

[v] Connected device (1 available)
    • SM G900F • 5fe4af7d • android-arm • Android 6.0.1 (API 23)

• No issues found!

flutter_02

Activity

sandrokl

sandrokl commented on Nov 29, 2018

@sandrokl

Following... I have this problem too.

noordawod

noordawod commented on Dec 2, 2018

@noordawod
Contributor

Check: flutter/flutter#24481

Did you upgrade to the latest Flutter and are you checking for null value in your locale resolve callback?

It's also important to note that Flutter may call your callback a couple of times, one time when it starts and one time when the system locale has been resolved.

LucioD

LucioD commented on Dec 4, 2018

@LucioD
Author

No, I again analyzed everything and done a bit 'of debugging.
In my case, the problem lies in the isSupported() method of the GeneratedLocalizationsDelegate class, which is automatically generated in the Android Studio IDE (and made unmodifiable in that environment) by the Flutter i18n plugin (https://plugins.jetbrains.com/plugin/10128-flutter-i18n).
This isSupported() method, which is called by the localResolutionCallback method, now returns false if the device locale contains both the languageCode and the countryCode (example 'it_IT') and the supportedLocales locale contains only the languageCode (example 'it'), whereas before the last upgrade, it returned true!
I believe that this is the reason why now the localization does not work anymore!
This is the GeneratedLocalizationsDelegate class, automatically generated in the Android Studio IDE by the Flutter i18n plugin (I have removed the methods that are not important for this explanation):

class GeneratedLocalizationsDelegate extends LocalizationsDelegate<S> {
  const GeneratedLocalizationsDelegate();

  List<Locale> get supportedLocales {
    return const <Locale>[
      Locale("en", ""),
      Locale("it", ""),
    ];
  }

  @override
  bool isSupported(Locale locale) =>
      locale != null && supportedLocales.contains(locale);

  LocaleListResolutionCallback listResolution({Locale fallback}) {
    return (List<Locale> locales, Iterable<Locale> supported) {
      if (locales == null || locales.isEmpty) {
        return fallback ?? supported.first;
      } else {
        return _resolve(locales.first, fallback, supported);
      }
    };
  }

  LocaleResolutionCallback resolution({Locale fallback}) {
    return (Locale locale, Iterable<Locale> supported) {
      return _resolve(locale, fallback, supported);
    };
  }

  Locale _resolve(Locale locale, Locale fallback, Iterable<Locale> supported) {
    if (locale == null || !isSupported(locale)) {
      return fallback ?? supported.first;
    }

    final Locale languageLocale = Locale(locale.languageCode, "");
    if (supported.contains(locale)) {
      return locale;
    } else if (supported.contains(languageLocale)) {
      return languageLocale;
    } else {
      final Locale fallbackLocale = fallback ?? supported.first;
      return fallbackLocale;
    }
  }
}

I think that in this class the isSupported() method should be changed like this:

@override
 bool isSupported(Locale locale) {
   bool isSupported = false;
   if (locale != null) {
     if (supportedLocales.contains(locale)) {
       isSupported = true;
     } else {
       for (Locale supportedLocale in supportedLocales) {
         if (supportedLocale.countryCode == null || supportedLocale.countryCode.isEmpty) {
           if (supportedLocale.languageCode == locale.languageCode) {
             isSupported = true;
             break;
           }
         }
       }
     }
   }
   return isSupported;
 }

So that it returns true not only when the supportedLocale matches both language and country code, but also when it matches only the language code. In fact, before the last upgrade, the GeneratedLocalizationsDelegate class worked like this!

But I CAN NOT make this change, because the GeneratedLocalizationsDelegate class, is automatically generated in the Android Studio IDE (and unchangeable in that environment) by the Flutter i18n plugin.

So, I believe that a new version of the Flutter i18N plugin, that includes this change, is needed.

What do you think of this?

noordawod

noordawod commented on Dec 5, 2018

@noordawod
Contributor

@LucioD

I suppose you have your source ARB files named as strings_it.arb, strings_en.arb, right?

Here's a quick way how to fix the problem you're facing: name your ARB files to include the country as well, for example: strings_it_IT.arb

What I would suggest to do instead is to allow the user to provide a comparator function that returns whether a locale is supported or not. It should be an easy pr.

LucioD

LucioD commented on Dec 6, 2018

@LucioD
Author

@noordawod

  1. yes, I have the ARB files named strings_it.arb and strings_en.arb.
  2. I had already thought about the solution to include the country code in the name of the ARB files, but I had discarded it because, if it was good for the Italian language (associated with the only IT country code), the German, French and Spanish languages (that I will have to implement as soon) are associated with multiple country codes (3 country codes for the German language, 2 country codes for the French language and 2 country codes for the Spanish language). So in total I would have to manage the maintenance of 8 additional ARB files, instead of 4 (in addition to the ARB file of English). It does not seem a very reasonable solution, especially since the previous version of the i18n flutter plugin worked properly with ARB files named only with the language code.
  3. I agree with you on the need to allow the user to provide a comparison function that returns if a locale is supported or not, but the actual default isSupported () function of the plugin should be changed to handle not just language + country, but also for language only.

That is, the current isSupported () function of the plugin should be changed, with a function like this:

  @override
  bool isSupported(Locale locale) {
    bool isSupported = false;
    if (locale != null) {
      if (supportedLocales.contains(locale)) {
        isSupported = true;
      } else {
        for (Locale supportedLocale in supportedLocales) {
          if (supportedLocale.countryCode == null || supportedLocale.countryCode.isEmpty) {
            if (supportedLocale.languageCode == locale.languageCode) {
              isSupported = true;
              break;
            }
          }
        }
      }
    }
    return isSupported;
  }

In order to restore the previous functionality that has failed with the last upgrade.

Then, giving the user the possibility to override this method with its own function, everything would be perfect!
It's right?

noordawod

noordawod commented on Dec 7, 2018

@noordawod
Contributor

Yeah, I mostly agree with you. My proposition was for a quick fix, not a long term one.

I would reformat the suggested method like this:

  @override
  bool isSupported(Locale locale) {
    if (locale != null) {
      final localeHasCountry = locale.countryCode != null && locale.countryCode.isNotEmpty;
      for (Locale supportedLocale in supportedLocales) {
        // Language must always match both locales.
        if (supportedLocale.languageCode != locale.languageCode) {
          continue;
        }
        if (localeHasCountry) {
          // Check that supported locale has the same country as provided by user.
          if (supportedLocale.countryCode == locale.countryCode) {
            return true;
          }
        } else if (supportedLocale.countryCode == null || supportedLocale.countryCode.isEmpty) {
          // Supported locale has no country, just like the user's locale.
          return true;
        }
      }
    }
    return false;
  }

The reason for dropping the first check (contains()) is because we're already looping through the locales so your implementation is actually doing double the work in worst-case scenario.

If you send a pr, maybe the maintainer @long1eu can merge it in?

LucioD

LucioD commented on Dec 7, 2018

@LucioD
Author

@noordawod
No please! The reformatted method you suggested causes exactly the same problem I've reported!

  1. The 'locale' parameter always contains both the language and the country (example fr_CH, it_IT, etc.).
  2. It is the user'supportedLocales array that can contain locales in the 'only language' format (example 'fr') or in the 'language + country' format (example 'fr_CH'), depending on the name of the .ARB files.

So, the new method according to your reformatting, must be like this:

@override
bool isSupported(Locale locale) {
  if (locale != null) {
    for (Locale supportedLocale in supportedLocales) {
      // Language must always match both locales.
      if (supportedLocale.languageCode != locale.languageCode) {
        continue;
      }
      final supportedLocaleHasCountry = supportedLocale.countryCode != null && supportedLocale.countryCode.isNotEmpty;
      if (supportedLocaleHasCountry) {
        // Check also that supported locale has the same country of device locale
        if (supportedLocale.countryCode == locale.countryCode) {
          return true;
        }
      } else {
        // Supported locale has no country, so we do not have to test the country
        return true;
      }
    }
  }
  return false;
}

Or like this, if you want to use my previous method without the first check (contains()...:

  @override
  bool isSupported(Locale locale) {
    bool isSupported = false;
    if (locale != null) {
      for (Locale supportedLocale in supportedLocales) {
        if (supportedLocale.languageCode == locale.languageCode) {
          if (supportedLocale.countryCode == null || supportedLocale.countryCode.isEmpty) {
            isSupported = true;
            break;
          } else if (supportedLocale.countryCode == locale.countryCode) {
            isSupported = true;
            break;
          }
        }
      }
    }
    return isSupported;
  }
  1. Finally, I did not understand your sentence "If you send a pr, maybe the maintainer @ long1eu can merge it in?". What is the pr that I should send you?
noordawod

noordawod commented on Dec 7, 2018

@noordawod
Contributor

@LucioD

Ahh, I think you're right about which variable holds the country code. So in this case, the method becomes much simpler:

@override
bool isSupported(Locale locale) {
  if (locale != null) {
    for (Locale supportedLocale in supportedLocales) {
      if (
        // Language must always match both locales.
        supportedLocale.languageCode == locale.languageCode && (
          // Country code can be missing, or
          supportedLocale.countryCode == null ||
          // Country code can be empty, or
          supportedLocale.countryCode.isEmpty ||
          // Country code is equal to user's locale
          supportedLocale.countryCode == locale.countryCode
        )
      ) {
        return true;
      }
    }
  }
  return false;
}

A PR is shortcut for Pull Request. Google it :)

LucioD

LucioD commented on Dec 10, 2018

@LucioD
Author

Ok, your last method is correct and works perfectly!
Can you make it the default isSupported() method in the GeneratedLocalizationsDelegate class of the Flutter i18n plugin, and /or allow the user to override it by an appropriate new parameter in the MaterialApp() constructor?
Thanks in advance.

noordawod

noordawod commented on Dec 11, 2018

@noordawod
Contributor

I can provide a pr to the maintainer, but I cannot merge the changes. Based on the maintainer's last engagement, I would say that it'll take few weeks for these changes to find their way into the code.

@long1eu feel free to give me merge privilege so I can help out with maintaining this plugin.

LucioD

LucioD commented on Dec 11, 2018

@LucioD
Author

Ok, I will wait for these changes.
Thanks for now.

long1eu

long1eu commented on Dec 14, 2018

@long1eu
Owner

@noordawod thanks. Let me know if the latest release solves this. https://github.com/long1eu/flutter_i18n/releases

19 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

    Development

    No branches or pull requests

      Participants

      @smiLLe@cbenhagen@noordawod@lascapi@LucioD

      Issue actions

        Localizations no longer works after upgrade to the latest version of flutter i18n plugin · Issue #26 · long1eu/flutter_i18n