From 1c38185052ed058554e3e221f7487cb2ce80ce64 Mon Sep 17 00:00:00 2001 From: Pavel Sulimau Date: Tue, 2 Mar 2021 20:50:21 +0300 Subject: [PATCH] Add 'supportedLocales' property --- CHANGELOG.md | 4 ++ README.md | 2 +- example/README.md | 15 ++++++- lib/fast_i18n.dart | 59 +++++++++++++++++++------- lib/src/generator.dart | 90 ++++++++++++++++++++++++---------------- pubspec.yaml | 2 +- test/fast_i18n_test.dart | 60 +++++++++++++++++++++++++++ 7 files changed, 178 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52fe0a4f..0f03e8b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.0 + +- Add `supportedLocales` property that can be used to fill `MaterialApp`'s `supportedLocales` argument. + ## 2.2.1 - Fix compilation error occurring when non-standard name (not 'strings.i18n.json') is used for json files. diff --git a/README.md b/README.md index 1d89275e..75caf465 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Lightweight i18n solution. Use JSON files to create typesafe translations. ```yaml dependencies: - fast_i18n: ^2.2.1 + fast_i18n: ^2.3.0 dev_dependencies: build_runner: any diff --git a/example/README.md b/example/README.md index ab22676c..8ff9c4f4 100644 --- a/example/README.md +++ b/example/README.md @@ -4,7 +4,7 @@ ```yaml dependencies: - fast_i18n: ^2.2.1 + fast_i18n: ^2.3.0 dev_dependencies: build_runner: any @@ -67,6 +67,19 @@ void initState() { } ``` +## Step 4a: Override 'supportedLocales' + +```dart +MaterialApp( + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: LocaleSettings.supportedLocales, // <--- +) +``` + ## Step 4b: iOS-only ``` diff --git a/lib/fast_i18n.dart b/lib/fast_i18n.dart index 8b3b063f..d47df1a3 100644 --- a/lib/fast_i18n.dart +++ b/lib/fast_i18n.dart @@ -1,41 +1,70 @@ library fast_i18n; import 'dart:io'; +import 'dart:ui'; import 'package:fast_i18n/utils.dart'; class FastI18n { + static const _localePartsDelimiter = '-'; - /// returns the locale string used by the device - static String getDeviceLocale() { - return Platform.localeName; - } + /// Returns the locale string used by the device. + static String getDeviceLocale() => Platform.localeName; - /// returns the candidate (or part of it) if it is supported - /// fallback to base locale + /// Returns the candidate (or part of it) if it is supported. + /// Fallbacks to base locale. static String selectLocale(String candidate, List supported, String baseLocale) { // normalize candidate = Utils.normalize(candidate); // 1st try: match exactly - String selected = supported.firstWhere((element) => element == candidate, - orElse: () => null); + String selected = supported.firstWhere((element) => element == candidate, orElse: () => null); if (selected != null) return selected; // 2nd try: match the first part (language) - List deviceLocaleParts = candidate.split('-'); - selected = supported.firstWhere( - (element) => element == deviceLocaleParts.first, - orElse: () => null); + List deviceLocaleParts = candidate.split(_localePartsDelimiter); + selected = + supported.firstWhere((element) => element == deviceLocaleParts.first, orElse: () => null); if (selected != null) return selected; // 3rd try: match the second part (region) - selected = supported.firstWhere( - (element) => element == deviceLocaleParts.last, - orElse: () => null); + selected = + supported.firstWhere((element) => element == deviceLocaleParts.last, orElse: () => null); if (selected != null) return selected; // fallback: default locale return baseLocale; } + + /// Converts the passed locales from [String] to [Locale]. + /// Puts the [baseLocale] into the the beginning of the list. + static List convertToLocales(List locales, String baseLocale) { + final rawSupportedLocales = [ + baseLocale, + ...locales.where((locale) => locale != baseLocale), + ]; + + final supportedLocales = rawSupportedLocales.map((rawLocale) { + if (rawLocale.contains(_localePartsDelimiter)) { + final localeParts = + rawLocale.split(_localePartsDelimiter).where((part) => part.isNotEmpty).toList(); + if (localeParts.length == 2) { + return Locale.fromSubtags(languageCode: localeParts[0], countryCode: localeParts[1]); + } else if (localeParts.length == 3) { + return Locale.fromSubtags( + languageCode: localeParts[0], + scriptCode: localeParts[1], + countryCode: localeParts[2], + ); + } else { + throw Exception( + "The locale '$rawLocale' is not in a supported format. Examples of the supported formats: 'en', 'en-US', 'zh-Hans-CN'."); + } + } else { + return Locale.fromSubtags(languageCode: rawLocale); + } + }).toList(); + + return supportedLocales; + } } diff --git a/lib/src/generator.dart b/lib/src/generator.dart index 5a7f228c..c2716129 100644 --- a/lib/src/generator.dart +++ b/lib/src/generator.dart @@ -77,7 +77,8 @@ void _generateHeader(StringBuffer buffer, I18nConfig config, List allL buffer.writeln(); buffer.writeln('/// Method A: Simple'); buffer.writeln('///'); - buffer.writeln('/// Widgets using this method will not be updated when locale changes during runtime.'); + buffer.writeln( + '/// Widgets using this method will not be updated when locale changes during runtime.'); buffer.writeln('/// Translation happens during initialization of the widget (call of t).'); buffer.writeln('///'); buffer.writeln('/// Usage:'); @@ -89,7 +90,8 @@ void _generateHeader(StringBuffer buffer, I18nConfig config, List allL buffer.writeln('/// Method B: Advanced'); buffer.writeln('///'); buffer.writeln('/// All widgets using this method will trigger a rebuild when locale changes.'); - buffer.writeln('/// Use this if you have e.g. a settings page where the user can select the locale during runtime.'); + buffer.writeln( + '/// Use this if you have e.g. a settings page where the user can select the locale during runtime.'); buffer.writeln('///'); buffer.writeln('/// Step 1:'); buffer.writeln('/// wrap your App with'); @@ -104,7 +106,8 @@ void _generateHeader(StringBuffer buffer, I18nConfig config, List allL buffer.writeln('\t$translationsClass._(); // no constructor'); buffer.writeln(); buffer.writeln('\tstatic $baseClassName of(BuildContext context) {'); - buffer.writeln('\t\treturn context.dependOnInheritedWidgetOfExactType<$inheritedClass>().translations;'); + buffer.writeln( + '\t\treturn context.dependOnInheritedWidgetOfExactType<$inheritedClass>().translations;'); buffer.writeln('\t}'); buffer.writeln('}'); @@ -114,7 +117,7 @@ void _generateHeader(StringBuffer buffer, I18nConfig config, List allL buffer.writeln('\t$settingsClass._(); // no constructor'); buffer.writeln(); - buffer.writeln('\t/// Use locale of the device, fallbacks to base locale.'); + buffer.writeln('\t/// Uses locale of the device, fallbacks to base locale.'); buffer.writeln('\t/// Returns the locale which has been set.'); buffer.writeln('\tstatic String useDeviceLocale() {'); buffer.writeln('\t\tString deviceLocale = FastI18n.getDeviceLocale();'); @@ -122,10 +125,11 @@ void _generateHeader(StringBuffer buffer, I18nConfig config, List allL buffer.writeln('\t}'); buffer.writeln(); - buffer.writeln('\t/// Set locale, fallbacks to base locale.'); + buffer.writeln('\t/// Sets locale, fallbacks to base locale.'); buffer.writeln('\t/// Returns the locale which has been set.'); buffer.writeln('\tstatic String setLocale(String locale) {'); - buffer.writeln('\t\t$localeVar = FastI18n.selectLocale(locale, $mapVar.keys.toList(), $baseLocaleVar);'); + buffer.writeln( + '\t\t$localeVar = FastI18n.selectLocale(locale, $mapVar.keys.toList(), $baseLocaleVar);'); buffer.writeln('\t\t$translateVar = $mapVar[$localeVar];'); buffer.writeln(); buffer.writeln('\t\tif ($translationProviderKey.currentState != null) {'); @@ -136,36 +140,45 @@ void _generateHeader(StringBuffer buffer, I18nConfig config, List allL buffer.writeln('\t}'); buffer.writeln(); - buffer.writeln('\t/// Get current locale.'); + buffer.writeln('\t/// Gets current locale.'); buffer.writeln('\tstatic String get currentLocale {'); buffer.writeln('\t\treturn $localeVar;'); buffer.writeln('\t}'); buffer.writeln(); - buffer.writeln('\t/// Get base locale.'); + buffer.writeln('\t/// Gets base locale.'); buffer.writeln('\tstatic String get baseLocale {'); buffer.writeln('\t\treturn $baseLocaleVar;'); buffer.writeln('\t}'); buffer.writeln(); - buffer.writeln('\t/// Get supported locales.'); + buffer.writeln('\t/// Gets supported locales.'); buffer.writeln('\tstatic List get locales {'); buffer.writeln('\t\treturn $mapVar.keys.toList();'); buffer.writeln('\t}'); + buffer.writeln(); + buffer.writeln('\t/// Get supported locales with base locale sorted first.'); + buffer.writeln('\tstatic List get supportedLocales {'); + buffer.writeln('\t\treturn FastI18n.convertToLocales($mapVar.keys.toList(), $baseLocaleVar);'); + buffer.writeln('\t}'); + buffer.writeln('}'); // TranslationProvider buffer.writeln(); - buffer.writeln('GlobalKey<$translationProviderStateClass> $translationProviderKey = new GlobalKey<$translationProviderStateClass>();'); + buffer.writeln( + 'GlobalKey<$translationProviderStateClass> $translationProviderKey = new GlobalKey<$translationProviderStateClass>();'); buffer.writeln(); buffer.writeln('class $translationProviderClass extends StatefulWidget {'); - buffer.writeln('\t$translationProviderClass({@required this.child}) : super(key: $translationProviderKey);'); + buffer.writeln( + '\t$translationProviderClass({@required this.child}) : super(key: $translationProviderKey);'); buffer.writeln(); buffer.writeln('\tfinal Widget child;'); buffer.writeln(); buffer.writeln('\t@override'); - buffer.writeln('\t$translationProviderStateClass createState() => $translationProviderStateClass();'); + buffer.writeln( + '\t$translationProviderStateClass createState() => $translationProviderStateClass();'); buffer.writeln('}'); buffer.writeln(); @@ -221,7 +234,7 @@ void _generateLocale(StringBuffer buffer, I18nConfig config, I18nData localeData buffer, queue, task.className, - task.members + task.members, ); } while (queue.isNotEmpty); } @@ -235,11 +248,9 @@ void _generateClass( StringBuffer buffer, Queue queue, String className, - Map currMembers + Map currMembers, ) { - String finalClassName = base - ? className - : className + locale.capitalize().replaceAll('-', ''); + String finalClassName = base ? className : className + locale.capitalize().replaceAll('-', ''); buffer.writeln(); @@ -277,15 +288,13 @@ void _generateClass( // inline map String type = value.plainStrings ? 'String' : 'dynamic'; buffer.write('Map get $key => '); - _generateMap( - base, locale, buffer, queue, childClassName, value.entries, 0); + _generateMap(base, locale, buffer, queue, childClassName, value.entries, 0); } else { // generate a class later on queue.add(ClassTask(childClassName, value.entries)); - String finalChildClassName = base - ? childClassName - : childClassName + locale.capitalize().replaceAll('-', ''); + String finalChildClassName = + base ? childClassName : childClassName + locale.capitalize().replaceAll('-', ''); buffer.writeln('$finalChildClassName get $key => $finalChildClassName._instance;'); } @@ -298,13 +307,14 @@ void _generateClass( /// generates a map of ONE locale /// similar to _generateClass but anonymous and accessible via key void _generateMap( - bool base, - String locale, - StringBuffer buffer, - Queue queue, - String className, - Map currMembers, - int depth) { + bool base, + String locale, + StringBuffer buffer, + Queue queue, + String className, + Map currMembers, + int depth, +) { buffer.writeln('{'); currMembers.forEach((key, value) { @@ -328,9 +338,8 @@ void _generateMap( // generate a class later on queue.add(ClassTask(childClassName, value.entries)); - String finalChildClassName = base - ? childClassName - : childClassName + locale.capitalize().replaceAll('-', ''); + String finalChildClassName = + base ? childClassName : childClassName + locale.capitalize().replaceAll('-', ''); buffer.writeln('\'$key\': $finalChildClassName._instance,'); } @@ -349,8 +358,15 @@ void _generateMap( } /// generates a list -void _generateList(bool base, String locale, StringBuffer buffer, - Queue queue, String className, List currList, int depth) { +void _generateList( + bool base, + String locale, + StringBuffer buffer, + Queue queue, + String className, + List currList, + int depth, +) { buffer.writeln('['); for (int i = 0; i < currList.length; i++) { @@ -421,8 +437,10 @@ extension on String { String toCase(String caseName) { switch (caseName) { - case 'snake': return snakeCase; - case 'camel': return camelCase; + case 'snake': + return snakeCase; + case 'camel': + return camelCase; default: return this; } diff --git a/pubspec.yaml b/pubspec.yaml index 6436b1b0..49ce2fd7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: fast_i18n description: Lightweight i18n solution. Use JSON files to create typesafe translations. -version: 2.2.1 +version: 2.3.0 homepage: https://github.com/Tienisto/flutter-fast-i18n environment: diff --git a/test/fast_i18n_test.dart b/test/fast_i18n_test.dart index 20a8bbe4..5bbed8d4 100644 --- a/test/fast_i18n_test.dart +++ b/test/fast_i18n_test.dart @@ -35,4 +35,64 @@ void testSelectLocale() { expect(FastI18n.selectLocale('fr', ['en', 'de'], 'cz'), 'cz'); }); }); + + group('convertToLocales', () { + test('puts the base locale first', () { + final localesAsStrings = ['en-us', 'ru-RU', 'de-de', 'zh-Hans-CN']; + final baseLocaleString = 'ru-RU'; + + final locales = FastI18n.convertToLocales(localesAsStrings, baseLocaleString); + + expect(locales.length, 4); + expect(locales[0].toLanguageTag(), 'ru-RU'); + expect(locales[1].toLanguageTag(), 'en-us'); + expect(locales[2].toLanguageTag(), 'de-de'); + expect(locales[3].toLanguageTag(), 'zh-Hans-CN'); + }); + + test('when there is no country code present', () { + final localesAsStrings = ['en', 'ru-RU', 'de']; + final baseLocaleString = 'de'; + + final locales = FastI18n.convertToLocales(localesAsStrings, baseLocaleString); + + expect(locales.length, 3); + expect(locales[0].toLanguageTag(), 'de'); + expect(locales[1].toLanguageTag(), 'en'); + expect(locales[2].toLanguageTag(), 'ru-RU'); + }); + + test("throws Exception if a locale with '-' delimiter doesn't have 2+ non-empty parts", () { + final localesAsStrings = ['ru-', 'de']; + final baseLocaleString = 'de'; + + expect( + () => FastI18n.convertToLocales(localesAsStrings, baseLocaleString), + throwsA(isA()), + ); + }); + + test( + 'throws AssertionError if primary language subtag is not present in the provided base locale', + () { + final localesAsStrings = ['en-us']; + final baseLocaleString = ''; + + expect( + () => FastI18n.convertToLocales(localesAsStrings, baseLocaleString), + throwsA(isA()), + ); + }); + + test('throws AssertionError if primary language subtag is not present in the provided locales', + () { + final localesAsStrings = ['en-us', '']; + final baseLocaleString = 'en-US'; + + expect( + () => FastI18n.convertToLocales(localesAsStrings, baseLocaleString), + throwsA(isA()), + ); + }); + }); }