diff --git a/CHANGELOG.md b/CHANGELOG.md index 6339c3f..32efc93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,49 @@ Sponsored by [MyText.ai](https://mytext.ai) [![](./example/SponsoredByMyTextAi.png)](https://mytext.ai) +## 15.0.0-dev.1 + +* Optionally, you can now set the `supportedLocales` of your app in the `I18n` widget. + For example, if your app supports American English and Standard Spanish, you'd use: + `supportedLocales: [Locale('en', 'US'), Locale('es')]`, + or `supportedLocales: ['en-US'.asLocale, 'es'.asLocale]`. + + If you do set `I18n.supportedLocales`, you must add the line + `supportedLocales: I18n.supportedLocales` to your `MaterialApp` (or `CupertinoApp`) + widget, like this: + + ```dart + void main() { + WidgetsFlutterBinding.ensureInitialized(); + + runApp(I18n( + initialLocale: ..., + supportedLocales: ['en-US'.asLocale, 'es'.asLocale], // Here! + child: AppCore(), + )); + } + } + + class AppCore extends StatelessWidget { + Widget build(BuildContext context) { + return MaterialApp( + locale: I18n.locale, + supportedLocales: I18n.supportedLocales, // Here! + ... + ), + ``` + + If you provide `I18n.supportedLocales`, only those supported locales will be + considered when recording **missing translations**. In other words, unsupported locales + will not be recorded as missing translations. + + +* **Breaking Change**: The `Translations.missingTranslationCallback` signature has + changed. This will only affect you if you have defined your own callback, which is + unlikely. If your code does break, update it to the new signature, which is an easy fix. + Note that it now returns a boolean. Only if it returns `true` will the missing + translation be added to the `Translations.missingTranslations` map. + ## 14.1.0 Version 14 brings important improvements, like new interpolation methods, useful diff --git a/example/lib/1_translation_example/main.dart b/example/lib/1_translation_example/main.dart index 4cd29c6..ef8dcb7 100644 --- a/example/lib/1_translation_example/main.dart +++ b/example/lib/1_translation_example/main.dart @@ -36,6 +36,16 @@ void main() async { I18n( initialLocale: await I18n.loadLocale(), autoSaveLocale: true, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + const Locale('en', 'US'), // Could also be 'en-US'.asLocale, + const Locale('pt', 'BR'), // Could also be 'pt-BR'.asLocale, + const Locale('es', 'ES'), // Could also be 'es-ES'.asLocale, + ], child: AppCore(), ), ); @@ -45,19 +55,10 @@ class AppCore extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - locale: I18n.locale, debugShowCheckedModeBanner: false, - // - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: [ - const Locale('en', 'US'), - const Locale('pt', 'BR'), - const Locale('es', 'ES'), - ], + locale: I18n.locale, + localizationsDelegates: I18n.localizationsDelegates, + supportedLocales: I18n.supportedLocales, home: MyHomePage(), theme: ThemeData( elevatedButtonTheme: ElevatedButtonThemeData( diff --git a/example/lib/2_identifier_translation_example/main.dart b/example/lib/2_identifier_translation_example/main.dart index 8d5af1c..d1bad60 100644 --- a/example/lib/2_identifier_translation_example/main.dart +++ b/example/lib/2_identifier_translation_example/main.dart @@ -30,6 +30,15 @@ void main() async { I18n( initialLocale: await I18n.loadLocale(), autoSaveLocale: true, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + const Locale('en', 'US'), // Could also be 'en-US'.asLocale, + const Locale('pt', 'BR'), // Could also be 'pt-BR'.asLocale, + ], child: AppCore(), ), ); @@ -38,18 +47,10 @@ void main() async { class AppCore extends StatelessWidget { @override Widget build(BuildContext context) => MaterialApp( - locale: I18n.locale, debugShowCheckedModeBanner: false, - // - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: [ - const Locale('en', "US"), - const Locale('pt', "BR"), - ], + locale: I18n.locale, + localizationsDelegates: I18n.localizationsDelegates, + supportedLocales: I18n.supportedLocales, home: MyHomePage(), ); } diff --git a/example/lib/3_scoped_identifier_translation_example/main.dart b/example/lib/3_scoped_identifier_translation_example/main.dart index 8db751e..8aff8d0 100644 --- a/example/lib/3_scoped_identifier_translation_example/main.dart +++ b/example/lib/3_scoped_identifier_translation_example/main.dart @@ -33,6 +33,15 @@ void main() async { I18n( initialLocale: await I18n.loadLocale(), autoSaveLocale: true, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + const Locale('en', "US"), + const Locale('pt', "BR"), + ], child: AppCore(), ), ); @@ -41,18 +50,10 @@ void main() async { class AppCore extends StatelessWidget { @override Widget build(BuildContext context) => MaterialApp( - locale: I18n.locale, debugShowCheckedModeBanner: false, - // - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: [ - const Locale('en', "US"), - const Locale('pt', "BR"), - ], + locale: I18n.locale, + localizationsDelegates: I18n.localizationsDelegates, + supportedLocales: I18n.supportedLocales, home: MyHomePage(), ); } diff --git a/example/lib/4_const_translation_example/main.dart b/example/lib/4_const_translation_example/main.dart index 4b957eb..81b040c 100644 --- a/example/lib/4_const_translation_example/main.dart +++ b/example/lib/4_const_translation_example/main.dart @@ -16,6 +16,15 @@ void main() async { I18n( initialLocale: await I18n.loadLocale(), autoSaveLocale: true, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + const Locale('en', "US"), + const Locale('pt', "BR"), + ], child: AppCore(), ), ); @@ -24,18 +33,10 @@ void main() async { class AppCore extends StatelessWidget { @override Widget build(BuildContext context) => MaterialApp( - locale: I18n.locale, debugShowCheckedModeBanner: false, - // - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: [ - const Locale('en', "US"), - const Locale('pt', "BR"), - ], + locale: I18n.locale, + localizationsDelegates: I18n.localizationsDelegates, + supportedLocales: I18n.supportedLocales, home: MyHomePage(), ); } diff --git a/example/lib/5_interpolation_example/main.dart b/example/lib/5_interpolation_example/main.dart index 8d61503..796fb6f 100644 --- a/example/lib/5_interpolation_example/main.dart +++ b/example/lib/5_interpolation_example/main.dart @@ -29,6 +29,15 @@ void main() async { I18n( initialLocale: await I18n.loadLocale(), autoSaveLocale: true, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + const Locale('en', "US"), + const Locale('pt', "BR"), + ], child: AppCore(), ), ); @@ -37,18 +46,10 @@ void main() async { class AppCore extends StatelessWidget { @override Widget build(BuildContext context) => MaterialApp( - locale: I18n.locale, debugShowCheckedModeBanner: false, - // - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: [ - const Locale('en', "US"), - const Locale('pt', "BR"), - ], + locale: I18n.locale, + localizationsDelegates: I18n.localizationsDelegates, + supportedLocales: I18n.supportedLocales, home: MyHomePage(), ); } diff --git a/example/lib/6_load_by_file_example/main.dart b/example/lib/6_load_by_file_example/main.dart index 5e3d9f3..9bb8520 100644 --- a/example/lib/6_load_by_file_example/main.dart +++ b/example/lib/6_load_by_file_example/main.dart @@ -34,6 +34,16 @@ void main() async { I18n( initialLocale: await I18n.loadLocale(), autoSaveLocale: true, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + const Locale('en', 'US'), // Could also be 'en-US'.asLocale, + const Locale('pt', 'BR'), // Could also be 'pt-BR'.asLocale, + const Locale('es', 'ES'), // Could also be 'es-ES'.asLocale, + ], child: AppCore(), ), ); @@ -43,19 +53,10 @@ class AppCore extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - locale: I18n.locale, debugShowCheckedModeBanner: false, - // - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: [ - const Locale('en', 'US'), - const Locale('pt', 'BR'), - const Locale('es', 'ES'), - ], + locale: I18n.locale, + localizationsDelegates: I18n.localizationsDelegates, + supportedLocales: I18n.supportedLocales, home: MyHomePage(), theme: ThemeData( elevatedButtonTheme: ElevatedButtonThemeData( diff --git a/example/lib/7_load_by_http_example/main.dart b/example/lib/7_load_by_http_example/main.dart index 658ba8a..9a843b8 100644 --- a/example/lib/7_load_by_http_example/main.dart +++ b/example/lib/7_load_by_http_example/main.dart @@ -43,6 +43,16 @@ void main() async { I18n( initialLocale: await I18n.loadLocale(), autoSaveLocale: true, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + const Locale('en', 'US'), // Could also be 'en-US'.asLocale, + const Locale('pt', 'BR'), // Could also be 'pt-BR'.asLocale, + const Locale('es', 'ES'), // Could also be 'es-ES'.asLocale, + ], child: AppCore(), ), ); @@ -52,19 +62,10 @@ class AppCore extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - locale: I18n.locale, debugShowCheckedModeBanner: false, - // - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: [ - const Locale('en', 'US'), - const Locale('pt', 'BR'), - const Locale('es', 'ES'), - ], + locale: I18n.locale, + localizationsDelegates: I18n.localizationsDelegates, + supportedLocales: I18n.supportedLocales, home: MyHomePage(), theme: ThemeData( elevatedButtonTheme: ElevatedButtonThemeData( diff --git a/lib/src/i18n_widget.dart b/lib/src/i18n_widget.dart index 962d45c..44d439c 100644 --- a/lib/src/i18n_widget.dart +++ b/lib/src/i18n_widget.dart @@ -52,7 +52,7 @@ import 'i18n_loader.dart'; /// ), /// ``` /// -/// Note that the [I18n] widget is above the [MaterialApp] widget, but declared storeTester.dispatch(InitBasicoQuandoJaTemUmUsuarioLogado_Action()); +/// Note that the [I18n] widget is above the [MaterialApp] widget, but declared /// in a parent widget. If you declare it in the same widget as the [MaterialApp] /// widget, it will not work. This is WRONG: /// @@ -95,14 +95,122 @@ class I18n extends StatefulWidget { final Widget child; - /// If you want to force a specific locale, you can set it here. - /// If you don’t set it, the current system-locale will be used. + /// Optionally, you can set [initialLocale] to force a specific locale when the app + /// opens. If you don’t set it, the current system-locale, read from the device + /// settings, will be used. + /// + /// If you set the [initialLocale], you must add the line `locale: I18n.locale` to + /// your [MaterialApp] (or [CupertinoApp]) widget: + /// + /// ```dart + /// void main() async { + /// WidgetsFlutterBinding.ensureInitialized(); + /// + /// runApp(I18n( + /// initialLocale: Locale('en', 'US'), // Here! + /// supportedLocales: [ ... ], + /// child: AppCore(), + /// )); + /// } + /// } + /// + /// class AppCore extends StatelessWidget { + /// Widget build(BuildContext context) { + /// return MaterialApp( + /// locale: I18n.locale, // Here! + /// supportedLocales: I18n.supportedLocales, + /// ... + /// ), + /// ``` + /// final Locale? initialLocale; + /// Optionally, you can set the [supportedLocales] of your app. + /// For example, if your app supports American English and Standard Spanish, use: + /// `supportedLocales: [Locale('en', 'US'), Locale('es')]`, + /// or `supportedLocales: ['en-US'.asLocale, 'es'.asLocale]`. + /// + /// If you set the [supportedLocales], you must add the line + /// `supportedLocales: I18n.supportedLocales` to your [MaterialApp] + /// (or [CupertinoApp]) widget: + /// + /// ```dart + /// void main() { + /// WidgetsFlutterBinding.ensureInitialized(); + /// + /// runApp(I18n( + /// initialLocale: ..., + /// supportedLocales: [Locale('en', 'US'), Locale('es')], // Here! + /// child: AppCore(), + /// )); + /// } + /// } + /// + /// class AppCore extends StatelessWidget { + /// Widget build(BuildContext context) { + /// return MaterialApp( + /// locale: I18n.locale, + /// supportedLocales: I18n.supportedLocales, // Here! + /// ... + /// ), + /// ``` + final Iterable _supportedLocales; + + /// Optionally, you can set the [localizationsDelegates] of your app. + /// + /// If you do that, you must add the line + /// `localizationsDelegates: I18n.localizationsDelegates` to your [MaterialApp] + /// (or [CupertinoApp]) widget: + /// + /// ```dart + /// void main() { + /// WidgetsFlutterBinding.ensureInitialized(); + /// + /// runApp(I18n( + /// localizationsDelegates: [ // Here! + /// GlobalMaterialLocalizations.delegate, + /// GlobalWidgetsLocalizations.delegate, + /// GlobalCupertinoLocalizations.delegate, + /// ], + /// child: AppCore(), + /// )); + /// } + /// } + /// + /// class AppCore extends StatelessWidget { + /// Widget build(BuildContext context) { + /// return MaterialApp( + /// localizationsDelegates: I18n.localizationsDelegates, // Here! + /// ... + /// ), + /// ``` + final Iterable> _localizationsDelegates; + /// If [autoSaveLocale] is true, the locale will be saved and recovered between /// app restarts. This is useful if you want to remember the user's language /// preference. If your app only ever uses the current system locale, or if you - /// save the locale in another way, keep [autoSaveLocale] as false. + /// save the locale in another way, keep [autoSaveLocale] as false: + /// + /// ```dart + /// void main() async { + /// WidgetsFlutterBinding.ensureInitialized(); + /// + /// runApp(I18n( + /// initialLocale: await I18n.loadLocale(), + /// supportedLocales: ['en-US'.asLocale, 'es'.asLocale], + /// autoSaveLocale: true, // Here! + /// child: AppCore(), + /// )); + /// } + /// + /// class AppCore extends StatelessWidget { + /// Widget build(BuildContext context) { + /// return MaterialApp( + /// locale: I18n.locale, + /// supportedLocales: I18n.supportedLocales, + /// ... + /// ), + /// ``` final bool autoSaveLocale; /// # Setup @@ -141,7 +249,7 @@ class I18n extends StatefulWidget { /// ), /// ``` /// - /// Note that the [I18n] widget is above the [MaterialApp] widget, but declared storeTester.dispatch(InitBasicoQuandoJaTemUmUsuarioLogado_Action()); + /// Note that the [I18n] widget is above the [MaterialApp] widget, but declared /// in a parent widget. If you declare it in the same widget as the [MaterialApp] /// widget, it will not work. This is WRONG: /// @@ -181,8 +289,14 @@ class I18n extends StatefulWidget { I18n({ required this.child, this.initialLocale, + Iterable supportedLocales = const [], + Iterable> localizationsDelegates = const [], this.autoSaveLocale = false, - }) : super(key: _i18nKey); + }) : _supportedLocales = supportedLocales, + _localizationsDelegates = localizationsDelegates, + super(key: _i18nKey) { + Translations.supportedLocales = supportedLocales.map((e) => e.format()).toList(); + } /// Return the current locale of the app. /// @@ -274,6 +388,32 @@ class I18n extends StatefulWidget { /// locale if needed. Otherwise, leave it as is. static Locale preInitializationLocale = const Locale('es', 'US'); + /// Returns the supported locales of the app, as set in the [I18n] widget. + static Iterable get supportedLocales { + // + var currentState = _i18nKey.currentState; + + if (currentState == null) + throw TranslationsException("Can't get I18n.supportedLocales. " + "Make sure the I18n widget is in a separate parent widget, " + "above MaterialApp/CupertinoApp in the widget tree."); + + return currentState.widget._supportedLocales; + } + + /// Returns the localization delegates of the app, as set in the [I18n] widget. + static Iterable> get localizationsDelegates { + // + var currentState = _i18nKey.currentState; + + if (currentState == null) + throw TranslationsException("Can't get I18n.localizationsDelegates. " + "Make sure the I18n widget is in a separate parent widget, " + "above MaterialApp/CupertinoApp in the widget tree."); + + return currentState.widget._localizationsDelegates; + } + /// Even before we can use `View.of()` it's possible we have access to the device /// locale via the `PlatformDispatcher`. If that's `undefined` (`und`) return the /// `preInitializationLocale` instead. @@ -287,11 +427,11 @@ class I18n extends StatefulWidget { /// Note this is just the language itself, without region/country, script etc. /// For example, if the locale is Locale('en', 'US'), then it returns 'en'. static String getLanguageOnlyFromLocale(Locale locale) { - _checkLanguageCode(locale); + _assertLanguageCode(locale); return locale.languageCode.toLowerCase().trim(); } - static void _checkLanguageCode(Locale locale) { + static void _assertLanguageCode(Locale locale) { if (locale.languageCode.contains("_")) { throw TranslationsException("Language code '${locale.languageCode}' is invalid: " "Contains an underscore character."); @@ -563,7 +703,7 @@ class _I18nState extends State with WidgetsBindingObserver { I18n._forcedLocale = _locale; Locale newLocale = I18n.locale; - I18n._checkLanguageCode(newLocale); + I18n._assertLanguageCode(newLocale); if (oldLocale != newLocale) I18n.observeLocale(oldLocale: oldLocale, newLocale: newLocale); @@ -625,7 +765,7 @@ class _I18nState extends State with WidgetsBindingObserver { I18n._systemLocale = newSystemLocale; Locale newLocale = I18n.locale; - I18n._checkLanguageCode(newLocale); + I18n._assertLanguageCode(newLocale); if (oldLocale != newLocale) I18n.observeLocale(oldLocale: oldLocale, newLocale: newLocale); diff --git a/pubspec.yaml b/pubspec.yaml index 6319224..41f1f15 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: i18n_extension description: Translation and Internationalization (i18n) for Flutter. Easy to use for both large and small projects. Uses Dart extensions to reduce boilerplate. -version: 14.1.0 +version: 15.0.0-dev.1 # author: Marcelo Glasberg homepage: https://github.com/marcglasberg/i18n_extension topics: @@ -15,7 +15,7 @@ environment: dependencies: intl: '>=0.17.0 <=0.20.1' - i18n_extension_core: ^4.0.0 + i18n_extension_core: ^5.0.0 http: ^1.2.2 shared_preferences: ^2.3.3 gettext_parser: ^0.2.0 diff --git a/test/i18n_extension_test.dart b/test/i18n_extension_test.dart index 54a2191..bc96246 100644 --- a/test/i18n_extension_test.dart +++ b/test/i18n_extension_test.dart @@ -549,6 +549,16 @@ void main() { test("Record missing keys and missing translations.", () { // + Translations.supportedLocales = [ + 'en-US', + 'cs-cz', + 'en-uk', + 'pt-BR', + 'es', + 'es-ES', + 'es-US' + ]; + // --------------- // 1) Search for a key which exists, and the translation also exists. @@ -631,8 +641,35 @@ void main() { expect("Hi.".i18n, "Hi."); expect(Translations.missingKeys, isEmpty); - expect(Translations.missingTranslations.single.locale, "xx-YY"); - expect(Translations.missingTranslations.single.key, "Hi."); + expect(Translations.missingTranslations, isEmpty); + + // --------------- + + // 5) Search for a key which exists, but the translation in the locale does NOT. + + Translations.supportedLocales = [ + 'en-US', + 'cs-cz', + 'en-uk', + 'pt-BR', + 'es', + 'es-ES', + 'es-US', + 'xx-yy' + ]; + + Translations.missingKeys.clear(); + Translations.missingTranslations.clear(); + + I18n.define(const Locale("xx", "yy")); + expect("Hi.".i18n, "Hi."); + + expect(Translations.missingKeys, isEmpty); + expect(Translations.missingTranslations, isEmpty); + + // --------------- + + Translations.supportedLocales = []; }); test( @@ -643,6 +680,7 @@ void main() { // 1) You CAN provide the translations "by locale" in the default locale, if you want. + Translations.supportedLocales = ["en-US", "es-ES"]; Translations.missingKeys.clear(); Translations.missingTranslations.clear(); @@ -665,6 +703,7 @@ void main() { // 2) But you don’t NEED to to provide the translations "by locale" in the default locale. + Translations.supportedLocales = ["en-US", "es-ES"]; Translations.missingKeys.clear(); Translations.missingTranslations.clear(); @@ -709,6 +748,8 @@ void main() { throwsA(TranslationsException("No default translation for 'en-US'."))); // --------------- + + Translations.supportedLocales = []; }); test("You must provide the translation in the default language.", () {