// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; class TestLocalizations { TestLocalizations(this.locale, this.prefix); final Locale locale; final String? prefix; static Future<TestLocalizations> loadSync(Locale locale, String? prefix) { return SynchronousFuture<TestLocalizations>(TestLocalizations(locale, prefix)); } static Future<TestLocalizations> loadAsync(Locale locale, String? prefix) { return Future<TestLocalizations>.delayed( const Duration(milliseconds: 100), () => TestLocalizations(locale, prefix) ); } static TestLocalizations of(BuildContext context) { return Localizations.of<TestLocalizations>(context, TestLocalizations)!; } String get message => '${prefix ?? ""}$locale'; } class SyncTestLocalizationsDelegate extends LocalizationsDelegate<TestLocalizations> { SyncTestLocalizationsDelegate([this.prefix]); final String? prefix; // Changing this value triggers a rebuild final List<bool> shouldReloadValues = <bool>[]; @override bool isSupported(Locale locale) => true; @override Future<TestLocalizations> load(Locale locale) => TestLocalizations.loadSync(locale, prefix); @override bool shouldReload(SyncTestLocalizationsDelegate old) { shouldReloadValues.add(prefix != old.prefix); return prefix != old.prefix; } @override String toString() => '${objectRuntimeType(this, 'SyncTestLocalizationsDelegate')}($prefix)'; } class AsyncTestLocalizationsDelegate extends LocalizationsDelegate<TestLocalizations> { AsyncTestLocalizationsDelegate([this.prefix]); final String? prefix; // Changing this value triggers a rebuild final List<bool> shouldReloadValues = <bool>[]; @override bool isSupported(Locale locale) => true; @override Future<TestLocalizations> load(Locale locale) => TestLocalizations.loadAsync(locale, prefix); @override bool shouldReload(AsyncTestLocalizationsDelegate old) { shouldReloadValues.add(prefix != old.prefix); return prefix != old.prefix; } @override String toString() => '${objectRuntimeType(this, 'AsyncTestLocalizationsDelegate')}($prefix)'; } class MoreLocalizations { MoreLocalizations(this.locale); final Locale locale; static Future<MoreLocalizations> loadSync(Locale locale) { return SynchronousFuture<MoreLocalizations>(MoreLocalizations(locale)); } static Future<MoreLocalizations> loadAsync(Locale locale) { return Future<MoreLocalizations>.delayed( const Duration(milliseconds: 100), () => MoreLocalizations(locale) ); } static MoreLocalizations of(BuildContext context) { return Localizations.of<MoreLocalizations>(context, MoreLocalizations)!; } String get message => '$locale'; } class SyncMoreLocalizationsDelegate extends LocalizationsDelegate<MoreLocalizations> { @override Future<MoreLocalizations> load(Locale locale) => MoreLocalizations.loadSync(locale); @override bool isSupported(Locale locale) => true; @override bool shouldReload(SyncMoreLocalizationsDelegate old) => false; } class AsyncMoreLocalizationsDelegate extends LocalizationsDelegate<MoreLocalizations> { @override Future<MoreLocalizations> load(Locale locale) => MoreLocalizations.loadAsync(locale); @override bool isSupported(Locale locale) => true; @override bool shouldReload(AsyncMoreLocalizationsDelegate old) => false; } class OnlyRTLDefaultWidgetsLocalizations extends DefaultWidgetsLocalizations { @override TextDirection get textDirection => TextDirection.rtl; } class OnlyRTLDefaultWidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> { const OnlyRTLDefaultWidgetsLocalizationsDelegate(); @override bool isSupported(Locale locale) => true; @override Future<WidgetsLocalizations> load(Locale locale) { return SynchronousFuture<WidgetsLocalizations>(OnlyRTLDefaultWidgetsLocalizations()); } @override bool shouldReload(OnlyRTLDefaultWidgetsLocalizationsDelegate old) => false; } Widget buildFrame({ Locale? locale, Iterable<LocalizationsDelegate<dynamic>>? delegates, required WidgetBuilder buildContent, LocaleResolutionCallback? localeResolutionCallback, List<Locale> supportedLocales = const <Locale>[ Locale('en', 'US'), Locale('en', 'GB'), ], }) { return WidgetsApp( color: const Color(0xFFFFFFFF), locale: locale, localizationsDelegates: delegates, localeResolutionCallback: localeResolutionCallback, supportedLocales: supportedLocales, onGenerateRoute: (RouteSettings settings) { return PageRouteBuilder<void>( pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) { return buildContent(context); } ); }, ); } class SyncLoadTest extends StatefulWidget { const SyncLoadTest({super.key}); @override SyncLoadTestState createState() => SyncLoadTestState(); } class SyncLoadTestState extends State<SyncLoadTest> { @override Widget build(BuildContext context) { return Text( TestLocalizations.of(context).message, textDirection: TextDirection.rtl, ); } } void main() { testWidgets('Localizations.localeFor in a WidgetsApp with system locale', (WidgetTester tester) async { late BuildContext pageContext; await tester.pumpWidget( buildFrame( buildContent: (BuildContext context) { pageContext = context; return const Text('Hello World', textDirection: TextDirection.ltr); } ) ); await tester.binding.setLocale('en', 'GB'); await tester.pump(); expect(Localizations.localeOf(pageContext), const Locale('en', 'GB')); await tester.binding.setLocale('en', 'US'); await tester.pump(); expect(Localizations.localeOf(pageContext), const Locale('en', 'US')); }); testWidgets('Localizations.localeFor in a WidgetsApp with an explicit locale', (WidgetTester tester) async { const Locale locale = Locale('en', 'US'); late BuildContext pageContext; await tester.pumpWidget( buildFrame( locale: locale, buildContent: (BuildContext context) { pageContext = context; return const Text('Hello World'); }, ) ); expect(Localizations.localeOf(pageContext), locale); await tester.binding.setLocale('en', 'GB'); await tester.pump(); // The WidgetApp's explicit locale overrides the system's locale. expect(Localizations.localeOf(pageContext), locale); }); testWidgets('Synchronously loaded localizations in a WidgetsApp', (WidgetTester tester) async { final List<LocalizationsDelegate<dynamic>> delegates = <LocalizationsDelegate<dynamic>>[ SyncTestLocalizationsDelegate(), DefaultWidgetsLocalizations.delegate, ]; Future<void> pumpTest(Locale locale) async { await tester.pumpWidget(Localizations( locale: locale, delegates: delegates, child: const SyncLoadTest(), )); } await pumpTest(const Locale('en', 'US')); expect(find.text('en_US'), findsOneWidget); await pumpTest(const Locale('en', 'GB')); await tester.pump(); expect(find.text('en_GB'), findsOneWidget); await pumpTest(const Locale('en', 'US')); await tester.pump(); expect(find.text('en_US'), findsOneWidget); }); testWidgets('Asynchronously loaded localizations in a WidgetsApp', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( delegates: <LocalizationsDelegate<dynamic>>[ AsyncTestLocalizationsDelegate(), ], buildContent: (BuildContext context) { return Text(TestLocalizations.of(context).message); }, ) ); await tester.pump(const Duration(milliseconds: 50)); // TestLocalizations.loadAsync() takes 100ms expect(find.text('en_US'), findsNothing); // TestLocalizations hasn't been loaded yet await tester.pump(const Duration(milliseconds: 50)); // TestLocalizations.loadAsync() completes await tester.pumpAndSettle(); expect(find.text('en_US'), findsOneWidget); // default test locale is US english await tester.binding.setLocale('en', 'GB'); await tester.pump(const Duration(milliseconds: 100)); await tester.pumpAndSettle(); expect(find.text('en_GB'), findsOneWidget); await tester.binding.setLocale('en', 'US'); await tester.pump(const Duration(milliseconds: 50)); // TestLocalizations.loadAsync() hasn't completed yet so the old text // localization is still displayed expect(find.text('en_GB'), findsOneWidget); await tester.pump(const Duration(milliseconds: 50)); // finish the async load await tester.pumpAndSettle(); expect(find.text('en_US'), findsOneWidget); }); testWidgets('Localizations with multiple sync delegates', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( delegates: <LocalizationsDelegate<dynamic>>[ SyncTestLocalizationsDelegate(), SyncMoreLocalizationsDelegate(), ], locale: const Locale('en', 'US'), buildContent: (BuildContext context) { return Column( children: <Widget>[ Text('A: ${TestLocalizations.of(context).message}'), Text('B: ${MoreLocalizations.of(context).message}'), ], ); }, ) ); // All localizations were loaded synchronously expect(find.text('A: en_US'), findsOneWidget); expect(find.text('B: en_US'), findsOneWidget); }); testWidgets('Localizations with multiple delegates', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( delegates: <LocalizationsDelegate<dynamic>>[ SyncTestLocalizationsDelegate(), AsyncMoreLocalizationsDelegate(), // No resources until this completes ], locale: const Locale('en', 'US'), buildContent: (BuildContext context) { return Column( children: <Widget>[ Text('A: ${TestLocalizations.of(context).message}'), Text('B: ${MoreLocalizations.of(context).message}'), ], ); }, ) ); await tester.pump(const Duration(milliseconds: 50)); expect(find.text('A: en_US'), findsNothing); // MoreLocalizations.load() hasn't completed yet expect(find.text('B: en_US'), findsNothing); await tester.pump(const Duration(milliseconds: 50)); await tester.pumpAndSettle(); expect(find.text('A: en_US'), findsOneWidget); expect(find.text('B: en_US'), findsOneWidget); }); testWidgets('Multiple Localizations', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( delegates: <LocalizationsDelegate<dynamic>>[ SyncTestLocalizationsDelegate(), ], locale: const Locale('en', 'US'), buildContent: (BuildContext context) { return Column( children: <Widget>[ Text('A: ${TestLocalizations.of(context).message}'), Localizations( locale: const Locale('en', 'GB'), delegates: <LocalizationsDelegate<dynamic>>[ SyncTestLocalizationsDelegate(), DefaultWidgetsLocalizations.delegate, ], // Create a new context within the en_GB Localization child: Builder( builder: (BuildContext context) { return Text('B: ${TestLocalizations.of(context).message}'); }, ), ), ], ); }, ) ); expect(find.text('A: en_US'), findsOneWidget); expect(find.text('B: en_GB'), findsOneWidget); }); // If both the locale and the length and type of a Localizations delegate list // stays the same BUT one of its delegate.shouldReload() methods returns true, // then the dependent widgets should rebuild. testWidgets('Localizations sync delegate shouldReload returns true', (WidgetTester tester) async { final SyncTestLocalizationsDelegate originalDelegate = SyncTestLocalizationsDelegate(); await tester.pumpWidget( buildFrame( delegates: <LocalizationsDelegate<dynamic>>[ originalDelegate, SyncMoreLocalizationsDelegate(), ], locale: const Locale('en', 'US'), buildContent: (BuildContext context) { return Column( children: <Widget>[ Text('A: ${TestLocalizations.of(context).message}'), Text('B: ${MoreLocalizations.of(context).message}'), ], ); }, ) ); await tester.pumpAndSettle(); expect(find.text('A: en_US'), findsOneWidget); expect(find.text('B: en_US'), findsOneWidget); expect(originalDelegate.shouldReloadValues, <bool>[]); final SyncTestLocalizationsDelegate modifiedDelegate = SyncTestLocalizationsDelegate('---'); await tester.pumpWidget( buildFrame( delegates: <LocalizationsDelegate<dynamic>>[ modifiedDelegate, SyncMoreLocalizationsDelegate(), ], locale: const Locale('en', 'US'), buildContent: (BuildContext context) { return Column( children: <Widget>[ Text('A: ${TestLocalizations.of(context).message}'), Text('B: ${MoreLocalizations.of(context).message}'), ], ); }, ) ); await tester.pumpAndSettle(); expect(find.text('A: ---en_US'), findsOneWidget); expect(find.text('B: en_US'), findsOneWidget); expect(modifiedDelegate.shouldReloadValues, <bool>[true]); expect(originalDelegate.shouldReloadValues, <bool>[]); }); testWidgets('Localizations async delegate shouldReload returns true', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( delegates: <LocalizationsDelegate<dynamic>>[ AsyncTestLocalizationsDelegate(), AsyncMoreLocalizationsDelegate(), ], locale: const Locale('en', 'US'), buildContent: (BuildContext context) { return Column( children: <Widget>[ Text('A: ${TestLocalizations.of(context).message}'), Text('B: ${MoreLocalizations.of(context).message}'), ], ); }, ) ); await tester.pumpAndSettle(); expect(find.text('A: en_US'), findsOneWidget); expect(find.text('B: en_US'), findsOneWidget); final AsyncTestLocalizationsDelegate modifiedDelegate = AsyncTestLocalizationsDelegate('---'); await tester.pumpWidget( buildFrame( delegates: <LocalizationsDelegate<dynamic>>[ modifiedDelegate, AsyncMoreLocalizationsDelegate(), ], locale: const Locale('en', 'US'), buildContent: (BuildContext context) { return Column( children: <Widget>[ Text('A: ${TestLocalizations.of(context).message}'), Text('B: ${MoreLocalizations.of(context).message}'), ], ); }, ) ); await tester.pumpAndSettle(); expect(find.text('A: ---en_US'), findsOneWidget); expect(find.text('B: en_US'), findsOneWidget); expect(modifiedDelegate.shouldReloadValues, <bool>[true]); }); testWidgets('Directionality tracks system locale', (WidgetTester tester) async { late BuildContext pageContext; await tester.pumpWidget( buildFrame( delegates: const <LocalizationsDelegate<dynamic>>[ GlobalWidgetsLocalizations.delegate, ], supportedLocales: const <Locale>[ Locale('en', 'GB'), Locale('ar', 'EG'), ], buildContent: (BuildContext context) { pageContext = context; return const Text('Hello World'); }, ) ); await tester.binding.setLocale('en', 'GB'); await tester.pump(); expect(Directionality.of(pageContext), TextDirection.ltr); await tester.binding.setLocale('ar', 'EG'); await tester.pump(); expect(Directionality.of(pageContext), TextDirection.rtl); }); testWidgets('localeResolutionCallback override', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( localeResolutionCallback: (Locale? newLocale, Iterable<Locale> supportedLocales) { return const Locale('foo', 'BAR'); }, buildContent: (BuildContext context) { return Text(Localizations.localeOf(context).toString()); }, ) ); await tester.pumpAndSettle(); expect(find.text('foo_BAR'), findsOneWidget); await tester.binding.setLocale('en', 'GB'); await tester.pumpAndSettle(); expect(find.text('foo_BAR'), findsOneWidget); }); testWidgets('supportedLocales and defaultLocaleChangeHandler', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( supportedLocales: const <Locale>[ Locale('zh', 'CN'), Locale('en', 'GB'), Locale('en', 'CA'), ], buildContent: (BuildContext context) { return Text(Localizations.localeOf(context).toString()); }, ) ); await tester.pumpAndSettle(); expect(find.text('en_GB'), findsOneWidget); // defaultLocaleChangedHandler prefers exact supported locale match await tester.binding.setLocale('en', 'CA'); await tester.pumpAndSettle(); expect(find.text('en_CA'), findsOneWidget); // defaultLocaleChangedHandler chooses 1st matching supported locale.languageCode await tester.binding.setLocale('en', 'US'); await tester.pumpAndSettle(); expect(find.text('en_GB'), findsOneWidget); // defaultLocaleChangedHandler: no matching supported locale, so use the 1st one await tester.binding.setLocale('da', 'DA'); await tester.pumpAndSettle(); expect(find.text('zh_CN'), findsOneWidget); }); testWidgets("Localizations.override widget tracks parent's locale and delegates", (WidgetTester tester) async { await tester.pumpWidget( buildFrame( // Accept whatever locale we're given localeResolutionCallback: (Locale? locale, Iterable<Locale> supportedLocales) => locale, delegates: const <LocalizationsDelegate<dynamic>>[ GlobalWidgetsLocalizations.delegate, ], buildContent: (BuildContext context) { return Localizations.override( context: context, child: Builder( builder: (BuildContext context) { final Locale locale = Localizations.localeOf(context); final TextDirection direction = WidgetsLocalizations.of(context).textDirection; return Text('$locale $direction'); }, ), ); }, ) ); // Initial WidgetTester locale is `en_US`. await tester.pumpAndSettle(); expect(find.text('en_US TextDirection.ltr'), findsOneWidget); await tester.binding.setLocale('en', 'CA'); await tester.pumpAndSettle(); expect(find.text('en_CA TextDirection.ltr'), findsOneWidget); await tester.binding.setLocale('ar', 'EG'); await tester.pumpAndSettle(); expect(find.text('ar_EG TextDirection.rtl'), findsOneWidget); await tester.binding.setLocale('da', 'DA'); await tester.pumpAndSettle(); expect(find.text('da_DA TextDirection.ltr'), findsOneWidget); }); testWidgets("Localizations.override widget overrides parent's DefaultWidgetLocalizations", (WidgetTester tester) async { await tester.pumpWidget( buildFrame( // Accept whatever locale we're given localeResolutionCallback: (Locale? locale, Iterable<Locale> supportedLocales) => locale, buildContent: (BuildContext context) { return Localizations.override( context: context, delegates: const <OnlyRTLDefaultWidgetsLocalizationsDelegate>[ // Override: no matter what the locale, textDirection is always RTL. OnlyRTLDefaultWidgetsLocalizationsDelegate(), ], child: Builder( builder: (BuildContext context) { final Locale locale = Localizations.localeOf(context); final TextDirection direction = WidgetsLocalizations.of(context).textDirection; return Text('$locale $direction'); }, ), ); }, ) ); // Initial WidgetTester locale is `en_US`. await tester.pumpAndSettle(); expect(find.text('en_US TextDirection.rtl'), findsOneWidget); await tester.binding.setLocale('en', 'CA'); await tester.pumpAndSettle(); expect(find.text('en_CA TextDirection.rtl'), findsOneWidget); await tester.binding.setLocale('ar', 'EG'); await tester.pumpAndSettle(); expect(find.text('ar_EG TextDirection.rtl'), findsOneWidget); await tester.binding.setLocale('da', 'DA'); await tester.pumpAndSettle(); expect(find.text('da_DA TextDirection.rtl'), findsOneWidget); }); testWidgets('WidgetsApp overrides DefaultWidgetLocalizations', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( // Accept whatever locale we're given localeResolutionCallback: (Locale? locale, Iterable<Locale> supportedLocales) => locale, delegates: <OnlyRTLDefaultWidgetsLocalizationsDelegate>[ const OnlyRTLDefaultWidgetsLocalizationsDelegate(), ], buildContent: (BuildContext context) { final Locale locale = Localizations.localeOf(context); final TextDirection direction = WidgetsLocalizations.of(context).textDirection; return Text('$locale $direction'); }, ) ); // Initial WidgetTester locale is `en_US`. await tester.pumpAndSettle(); expect(find.text('en_US TextDirection.rtl'), findsOneWidget); await tester.binding.setLocale('en', 'CA'); await tester.pumpAndSettle(); expect(find.text('en_CA TextDirection.rtl'), findsOneWidget); await tester.binding.setLocale('ar', 'EG'); await tester.pumpAndSettle(); expect(find.text('ar_EG TextDirection.rtl'), findsOneWidget); await tester.binding.setLocale('da', 'DA'); await tester.pumpAndSettle(); expect(find.text('da_DA TextDirection.rtl'), findsOneWidget); }); // We provide <Locale>[Locale('en', 'US'), Locale('zh', 'CN')] as ui.window.locales // for flutter tester so that the behavior of tests match that of production // environments. Here, we test the default locales. testWidgets('WidgetsApp DefaultWidgetLocalizations', (WidgetTester tester) async { await tester.pumpAndSettle(); await tester.pumpWidget( buildFrame( // Accept whatever locale we're given localeResolutionCallback: (Locale? locale, Iterable<Locale> supportedLocales) => locale, delegates: <OnlyRTLDefaultWidgetsLocalizationsDelegate>[ const OnlyRTLDefaultWidgetsLocalizationsDelegate(), ], buildContent: (BuildContext context) { final Locale locale1 = WidgetsBinding.instance.platformDispatcher.locales.first; final Locale locale2 = WidgetsBinding.instance.platformDispatcher.locales[1]; return Text('$locale1 $locale2'); }, ) ); // Initial WidgetTester default locales is `en_US` and `zh_CN`. await tester.pumpAndSettle(); expect(find.text('en_US zh_CN'), findsOneWidget); }); testWidgets('WidgetsApp.locale is resolved against supportedLocales', (WidgetTester tester) async { // app locale matches a supportedLocale await tester.pumpWidget( buildFrame( supportedLocales: const <Locale>[ Locale('zh', 'CN'), Locale('en', 'US'), ], locale: const Locale('en', 'US'), buildContent: (BuildContext context) { return Text(Localizations.localeOf(context).toString()); }, ) ); await tester.pumpAndSettle(); expect(find.text('en_US'), findsOneWidget); // app locale matches a supportedLocale's language await tester.pumpWidget( buildFrame( supportedLocales: const <Locale>[ Locale('zh', 'CN'), Locale('en', 'GB'), ], locale: const Locale('en', 'US'), buildContent: (BuildContext context) { return Text(Localizations.localeOf(context).toString()); }, ) ); await tester.pumpAndSettle(); expect(find.text('en_GB'), findsOneWidget); // app locale matches no supportedLocale await tester.pumpWidget( buildFrame( supportedLocales: const <Locale>[ Locale('zh', 'CN'), Locale('en', 'US'), ], locale: const Locale('ab', 'CD'), buildContent: (BuildContext context) { return Text(Localizations.localeOf(context).toString()); }, ) ); await tester.pumpAndSettle(); expect(find.text('zh_CN'), findsOneWidget); }); // Example from http://unicode.org/reports/tr35/#LanguageMatching testWidgets('WidgetsApp Unicode tr35 1', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( supportedLocales: const <Locale>[ Locale('de'), Locale('fr'), Locale('ja'), ], buildContent: (BuildContext context) { final Locale locale = Localizations.localeOf(context); return Text('$locale'); }, ) ); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'de', countryCode: 'AT'), Locale.fromSubtags(languageCode: 'fr'),] ); await tester.pumpAndSettle(); expect(find.text('de'), findsOneWidget); }); // Examples from http://unicode.org/reports/tr35/#LanguageMatching testWidgets('WidgetsApp Unicode tr35 2', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( supportedLocales: const <Locale>[ Locale('ja', 'JP'), Locale('de'), Locale('zh', 'TW'), ], buildContent: (BuildContext context) { final Locale locale = Localizations.localeOf(context); return Text('$locale'); }, ) ); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh'),] ); await tester.pumpAndSettle(); expect(find.text('zh_TW'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'US'), Locale.fromSubtags(languageCode: 'de'), Locale.fromSubtags(languageCode: 'fr'), Locale.fromSubtags(languageCode: 'de', countryCode: 'SW'), Locale.fromSubtags(languageCode: 'it'),] ); await tester.pumpAndSettle(); expect(find.text('de'), findsOneWidget); }); testWidgets('WidgetsApp EdgeCase Chinese', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( supportedLocales: const <Locale>[ Locale.fromSubtags(languageCode: 'zh'), Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'), Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'TW'), Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'HK'), ], buildContent: (BuildContext context) { final Locale locale = Localizations.localeOf(context); return Text('$locale'); }, ) ); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'US'), Locale.fromSubtags(languageCode: 'de'), Locale.fromSubtags(languageCode: 'fr'), Locale.fromSubtags(languageCode: 'de', countryCode: 'SW'), Locale.fromSubtags(languageCode: 'zh'),] ); await tester.pumpAndSettle(); expect(find.text('zh'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'US'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_TW'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_TW'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'TW'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_TW'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', countryCode: 'TW'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_TW'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', countryCode: 'HK'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_HK'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'HK'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hans_CN'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', countryCode: 'CN'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hans_CN'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'US'),] ); await tester.pumpAndSettle(); expect(find.text('zh'), findsOneWidget); // This behavior is up to the implementer to decide if a perfect scriptCode match // is better than a countryCode match. await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'CN'), Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'), Locale.fromSubtags(languageCode: 'zh', countryCode: 'CN'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_TW'), findsOneWidget); // languageCode only match is not enough to prevent resolving a perfect match // further down the preferredLocales list. await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'US'), Locale.fromSubtags(languageCode: 'zh', countryCode: 'US'), Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hans_CN'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'US'), Locale.fromSubtags(languageCode: 'zh', countryCode: 'US'), Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'JP'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hans_CN'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', countryCode: 'US'), Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hans_CN'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'US'), Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hans_CN'), findsOneWidget); // When no language match, we try for country only, since it is likely users are // at least familiar with their country's language. This is a possible case only // on iOS, where countryCode can be selected independently from language and script. await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', scriptCode: 'Hans', countryCode: 'TW'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_TW'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'TW'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_TW'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'HK'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_HK'), findsOneWidget); }); // Same as 'WidgetsApp EdgeCase Chinese' test except the supportedLocales order is // reversed. testWidgets('WidgetsApp EdgeCase ReverseChinese', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( supportedLocales: const <Locale>[ Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'HK'), Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'TW'), Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'), Locale.fromSubtags(languageCode: 'zh'), ], buildContent: (BuildContext context) { final Locale locale = Localizations.localeOf(context); return Text('$locale'); }, ) ); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'US'), Locale.fromSubtags(languageCode: 'de'), Locale.fromSubtags(languageCode: 'fr'), Locale.fromSubtags(languageCode: 'de', countryCode: 'SW'), Locale.fromSubtags(languageCode: 'zh'),] ); await tester.pumpAndSettle(); expect(find.text('zh'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'US'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_HK'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_HK'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'TW'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_TW'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', countryCode: 'TW'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_TW'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', countryCode: 'HK'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_HK'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'HK'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hans_CN'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', countryCode: 'CN'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hans_CN'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'US'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_HK'), findsOneWidget); // This behavior is up to the implementer to decide if a perfect scriptCode match // is better than a countryCode match. await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'CN'), Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'), Locale.fromSubtags(languageCode: 'zh', countryCode: 'CN'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_HK'), findsOneWidget); // languageCode only match is not enough to prevent resolving a perfect match // further down the preferredLocales list. await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'US'), Locale.fromSubtags(languageCode: 'zh', countryCode: 'US'), Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hans_CN'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'US'), Locale.fromSubtags(languageCode: 'zh', countryCode: 'US'), Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'JP'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hans_CN'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'zh', countryCode: 'US'), Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hans_CN'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'US'), Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hans_CN'), findsOneWidget); // When no language match, we try for country only, since it is likely users are // at least familiar with their country's language. This is a possible case only // on iOS, where countryCode can be selected independently from language and script. await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', scriptCode: 'Hans', countryCode: 'TW'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_TW'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'TW'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_TW'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'HK'),] ); await tester.pumpAndSettle(); expect(find.text('zh_Hant_HK'), findsOneWidget); }); // Examples from https://developer.android.com/guide/topics/resources/multilingual-support testWidgets('WidgetsApp Android', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( supportedLocales: const <Locale>[ Locale('en'), Locale('de', 'DE'), Locale('es', 'ES'), Locale('fr', 'FR'), Locale('it', 'IT'), ], buildContent: (BuildContext context) { final Locale locale = Localizations.localeOf(context); return Text('$locale'); }, ) ); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'fr', countryCode: 'CH'),] ); await tester.pumpAndSettle(); expect(find.text('fr_FR'), findsOneWidget); }); // Examples from https://developer.android.com/guide/topics/resources/multilingual-support testWidgets('WidgetsApp Android', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( supportedLocales: const <Locale>[ Locale('en'), Locale('de', 'DE'), Locale('es', 'ES'), Locale('it', 'IT'), ], buildContent: (BuildContext context) { final Locale locale = Localizations.localeOf(context); return Text('$locale'); }, ) ); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'fr', countryCode: 'CH'), Locale.fromSubtags(languageCode: 'it', countryCode: 'CH'),] ); await tester.pumpAndSettle(); expect(find.text('it_IT'), findsOneWidget); }); testWidgets('WidgetsApp Country-only fallback', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( supportedLocales: const <Locale>[ Locale('en', 'US'), Locale('de', 'DE'), Locale('de', 'AU'), Locale('de', 'LU'), Locale('de', 'CH'), Locale('es', 'ES'), Locale('es', 'US'), Locale('it', 'IT'), Locale('zh', 'CN'), Locale('zh', 'TW'), Locale('fr', 'FR'), Locale('br', 'FR'), Locale('pt', 'BR'), Locale('pt', 'PT'), ], buildContent: (BuildContext context) { final Locale locale = Localizations.localeOf(context); return Text('$locale'); }, ) ); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'ar', countryCode: 'CH'),] ); await tester.pumpAndSettle(); expect(find.text('de_CH'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'ar', countryCode: 'FR'),] ); await tester.pumpAndSettle(); expect(find.text('fr_FR'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'ar', countryCode: 'US'),] ); await tester.pumpAndSettle(); expect(find.text('en_US'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'es', countryCode: 'US'),] ); await tester.pumpAndSettle(); expect(find.text('es_US'), findsOneWidget); // Strongly prefer matching first locale even if next one is perfect. await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'pt'), Locale.fromSubtags(languageCode: 'pt', countryCode: 'PT'),] ); await tester.pumpAndSettle(); expect(find.text('pt_PT'), findsOneWidget); // Don't country match with any other available match. This behavior is // up for reconsideration. await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'ar', countryCode: 'BR'), Locale.fromSubtags(languageCode: 'pt'),] ); await tester.pumpAndSettle(); expect(find.text('pt_BR'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'ar', countryCode: 'BR'), Locale.fromSubtags(languageCode: 'pt', countryCode: 'PT'),] ); await tester.pumpAndSettle(); expect(find.text('pt_PT'), findsOneWidget); }); // Simulates a Chinese-default app that supports english in Canada but not // French. French-Canadian users should get 'en_CA' instead of Chinese. testWidgets('WidgetsApp Multilingual country', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( supportedLocales: const <Locale>[ Locale('zh', 'CN'), Locale('en', 'CA'), Locale('en', 'US'), Locale('en', 'AU'), Locale('de', 'DE'), ], buildContent: (BuildContext context) { final Locale locale = Localizations.localeOf(context); return Text('$locale'); }, ) ); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'fr', countryCode: 'CA'), Locale.fromSubtags(languageCode: 'fr'),] ); await tester.pumpAndSettle(); expect(find.text('en_CA'), findsOneWidget); }); testWidgets('WidgetsApp Common cases', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( // Decently well localized app. supportedLocales: const <Locale>[ Locale('en', 'US'), Locale('en', 'GB'), Locale('en', 'AU'), Locale('en', 'CA'), Locale('zh', 'CN'), Locale('zh', 'TW'), Locale('de', 'DE'), Locale('de', 'CH'), Locale('es', 'MX'), Locale('es', 'ES'), Locale('es', 'AR'), Locale('es', 'CO'), Locale('ru', 'RU'), Locale('fr', 'FR'), Locale('fr', 'CA'), Locale('ar', 'SA'), Locale('ar', 'EG'), Locale('ar', 'IQ'), Locale('ar', 'MA'), Locale('af'), Locale('bg'), Locale('nl', 'NL'), Locale('pl'), Locale('cs'), Locale('fa'), Locale('el'), Locale('he'), Locale('hi'), Locale('pa'), Locale('ta'), Locale('id'), Locale('it', 'IT'), Locale('ja'), Locale('ko'), Locale('ms'), Locale('mn'), Locale('pt', 'BR'), Locale('pt', 'PT'), Locale('sv', 'SE'), Locale('th'), Locale('tr'), Locale('vi'), ], buildContent: (BuildContext context) { final Locale locale = Localizations.localeOf(context); return Text('$locale'); }, ) ); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'US'),] ); await tester.pumpAndSettle(); expect(find.text('en_US'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en'),] ); await tester.pumpAndSettle(); expect(find.text('en_US'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'CA'),] ); await tester.pumpAndSettle(); expect(find.text('en_CA'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'AU'),] ); await tester.pumpAndSettle(); expect(find.text('en_AU'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'ar', countryCode: 'CH'),] ); await tester.pumpAndSettle(); expect(find.text('ar_SA'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'ar'),] ); await tester.pumpAndSettle(); expect(find.text('ar_SA'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'ar', countryCode: 'IQ'),] ); await tester.pumpAndSettle(); expect(find.text('ar_IQ'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'es', countryCode: 'ES'),] ); await tester.pumpAndSettle(); expect(find.text('es_ES'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'es'),] ); await tester.pumpAndSettle(); expect(find.text('es_MX'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'pa', countryCode: 'US'),] ); await tester.pumpAndSettle(); expect(find.text('pa'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'hi', countryCode: 'IN'),] ); await tester.pumpAndSettle(); expect(find.text('hi'), findsOneWidget); // Multiple preferred locales: await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'NZ'), Locale.fromSubtags(languageCode: 'en', countryCode: 'AU'), Locale.fromSubtags(languageCode: 'en', countryCode: 'GB'), Locale.fromSubtags(languageCode: 'en'),] ); await tester.pumpAndSettle(); expect(find.text('en_AU'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'ab'), Locale.fromSubtags(languageCode: 'en', countryCode: 'NZ'), Locale.fromSubtags(languageCode: 'en', countryCode: 'AU'), Locale.fromSubtags(languageCode: 'en', countryCode: 'GB'), Locale.fromSubtags(languageCode: 'en'),] ); await tester.pumpAndSettle(); expect(find.text('en_AU'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'NZ'), Locale.fromSubtags(languageCode: 'en', countryCode: 'PH'), Locale.fromSubtags(languageCode: 'en', countryCode: 'ZA'), Locale.fromSubtags(languageCode: 'en', countryCode: 'CB'),] ); await tester.pumpAndSettle(); expect(find.text('en_US'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'CA'), Locale.fromSubtags(languageCode: 'en', countryCode: 'AU'), Locale.fromSubtags(languageCode: 'en', countryCode: 'GB'), Locale.fromSubtags(languageCode: 'en', countryCode: 'US'),] ); await tester.pumpAndSettle(); expect(find.text('en_CA'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'da'), Locale.fromSubtags(languageCode: 'en'), Locale.fromSubtags(languageCode: 'en', countryCode: 'CA'),] ); await tester.pumpAndSettle(); expect(find.text('en_CA'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'da'), Locale.fromSubtags(languageCode: 'fo'), Locale.fromSubtags(languageCode: 'hr'),] ); await tester.pumpAndSettle(); expect(find.text('en_US'), findsOneWidget); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'da'), Locale.fromSubtags(languageCode: 'fo'), Locale.fromSubtags(languageCode: 'hr', countryCode: 'CA'),] ); await tester.pumpAndSettle(); expect(find.text('en_CA'), findsOneWidget); }); testWidgets('WidgetsApp invalid preferredLocales', (WidgetTester tester) async { await tester.pumpWidget( buildFrame( supportedLocales: const <Locale>[ Locale('zh', 'CN'), Locale('en', 'CA'), Locale('en', 'US'), Locale('en', 'AU'), Locale('de', 'DE'), ], localeResolutionCallback: (Locale? locale, Iterable<Locale> supportedLocales) { if (locale == null) { return const Locale('und', 'US'); } return const Locale('en', 'US'); }, buildContent: (BuildContext context) { final Locale locale = Localizations.localeOf(context); return Text('$locale'); }, ) ); await tester.binding.setLocales(const <Locale>[ Locale.fromSubtags(languageCode: 'en', countryCode: 'US'),] ); await tester.pumpAndSettle(); expect(find.text('en_US'), findsOneWidget); await tester.binding.setLocales(const <Locale>[]); await tester.pumpAndSettle(); expect(find.text('und_US'), findsOneWidget); }); }