From 9fb6fd81b98f6600e3d568a259aed9310f1d61d1 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Fri, 15 Sep 2017 07:37:08 -0700 Subject: [PATCH] Localizations overrides (#12094) --- dev/tools/gen_localizations.dart | 8 +- packages/flutter/lib/src/material/app.dart | 62 ++++++- .../lib/src/material/i18n/localizations.dart | 47 +++--- .../lib/src/material/i18n/material_zh.arb | 1 - .../src/material/material_localizations.dart | 14 +- packages/flutter/lib/src/widgets/app.dart | 7 +- .../lib/src/widgets/localizations.dart | 74 ++++++++- .../test/material/localizations_test.dart | 119 ++++++++++++++ .../test/widgets/localizations_test.dart | 153 ++++++++++++++++-- 9 files changed, 427 insertions(+), 58 deletions(-) diff --git a/dev/tools/gen_localizations.dart b/dev/tools/gen_localizations.dart index 18e8c37205..493044b221 100644 --- a/dev/tools/gen_localizations.dart +++ b/dev/tools/gen_localizations.dart @@ -75,19 +75,15 @@ String generateLocalizationsMap() { /// This variable is used by [MaterialLocalizations]. const Map> localizations = const > {'''); - final String lastLocale = localeToResources.keys.last; for (String locale in localeToResources.keys.toList()..sort()) { output.writeln(' "$locale": const {'); final Map resources = localeToResources[locale]; - final String lastName = resources.keys.last; for (String name in resources.keys) { - final String comma = name == lastName ? "" : ","; final String value = generateString(resources[name]); - output.writeln(' "$name": $value$comma'); + output.writeln(' "$name": $value,'); } - final String comma = locale == lastLocale ? "" : ","; - output.writeln(' }$comma'); + output.writeln(' },'); } output.writeln('};'); diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart index f75491f8ec..7207640f83 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.dart @@ -234,6 +234,61 @@ class MaterialApp extends StatefulWidget { /// /// The delegates collectively define all of the localized resources /// for this application's [Localizations] widget. + /// + /// Delegates that produce [WidgetsLocalizations] and [MaterialLocalizations] + /// are included automatically. Apps can provide their own versions of these + /// localizations by creating implementations of + /// [LocalizationsDelegate] or + /// [LocalizationsDelegate] whose load methods return + /// custom versions of [WidgetLocalizations] or [MaterialLocalizations]. + /// + /// For example: to add support to [MaterialLocalizations] for a + /// locale it doesn't already support, say `const Locale('foo', 'BR')`, + /// one could just extend [DefaultMaterialLocalizations]: + /// + /// ```dart + /// class FooLocalizations extends DefaultMaterialLocalizations { + /// FooLocalizations(Locale locale) : super(locale); + /// @override + /// String get okButtonLabel { + /// if (locale == const Locale('foo', 'BR')) + /// return 'foo'; + /// return super.okButtonLabel; + /// } + /// } + /// + /// ``` + /// + /// A `FooLocalizationsDelegate` is essentially just a method that constructs + /// a `FooLocalizations` object. We return a [SynchronousFuture] here because + /// no asynchronous work takes place upon "loading" the localizations object. + /// + /// ```dart + /// class FooLocalizationsDelegate extends LocalizationsDelegate { + /// const FooLocalizationsDelegate(); + /// @override + /// Future load(Locale locale) { + /// return new SynchronousFuture(new FooLocalizations(locale)); + /// } + /// @override + /// bool shouldReload(FooLocalizationsDelegate old) => false; + /// } + /// ``` + /// + /// Constructing a [MaterialApp] with a `FooLocalizationsDelegate` overrides + /// the automatically included delegate for [MaterialLocalizations] because + /// only the first delegate of each [LocalizationsDelegate.type] is used and + /// the automatically included delegates are added to the end of the app's + /// [localizationsDelegates] list. + /// + /// ```dart + /// new MaterialApp( + /// localizationsDelegates: [ + /// const FooLocalizationsDelegate(), + /// ], + /// // ... + /// ) + /// ``` final Iterable> localizationsDelegates; /// This callback is responsible for choosing the app's locale @@ -379,11 +434,14 @@ class _MaterialAppState extends State { } // Combine the Localizations for Material with the ones contributed - // by the localizationsDelegates parameter, if any. + // by the localizationsDelegates parameter, if any. Only the first delegate + // of a particular LocalizationsDelegate.type is loaded so the + // localizationsDelegate parameter can be used to override + // _MaterialLocalizationsDelegate. Iterable> get _localizationsDelegates sync* { - yield const _MaterialLocalizationsDelegate(); // TODO(ianh): make this configurable if (widget.localizationsDelegates != null) yield* widget.localizationsDelegates; + yield const _MaterialLocalizationsDelegate(); } RectTween _createRectTween(Rect begin, Rect end) { diff --git a/packages/flutter/lib/src/material/i18n/localizations.dart b/packages/flutter/lib/src/material/i18n/localizations.dart index fe4210f0cd..5ad37ede3c 100644 --- a/packages/flutter/lib/src/material/i18n/localizations.dart +++ b/packages/flutter/lib/src/material/i18n/localizations.dart @@ -4,7 +4,7 @@ // This file has been automatically generated. Please do not edit it manually. // To regenerate the file, use: -// dart dev/tools/gen_localizations.dart lib/src/material/i18n material +// dart dev/tools/gen_localizations.dart packages/flutter/lib/src/material/i18n material /// Maps from [Locale.languageCode] to a map that contains the localized strings /// for that locale. @@ -36,7 +36,7 @@ const Map> localizations = const { "timeOfDayFormat": r"HH:mm", @@ -63,7 +63,7 @@ const Map> localizations = const { "timeOfDayFormat": r"h:mm a", @@ -92,16 +92,16 @@ const Map> localizations = const { - "timeOfDayFormat": r"HH:mm" + "timeOfDayFormat": r"HH:mm", }, "en_IE": const { - "timeOfDayFormat": r"HH:mm" + "timeOfDayFormat": r"HH:mm", }, "en_ZA": const { - "timeOfDayFormat": r"HH:mm" + "timeOfDayFormat": r"HH:mm", }, "es": const { "timeOfDayFormat": r"H:mm", @@ -128,10 +128,10 @@ const Map> localizations = const { - "timeOfDayFormat": r"h:mm a" + "timeOfDayFormat": r"h:mm a", }, "fa": const { "timeOfDayFormat": r"H:mm", @@ -156,7 +156,7 @@ const Map> localizations = const { "timeOfDayFormat": r"HH:mm", @@ -183,10 +183,10 @@ const Map> localizations = const { - "timeOfDayFormat": r"HH 'h' mm" + "timeOfDayFormat": r"HH 'h' mm", }, "he": const { "timeOfDayFormat": r"H:mm", @@ -211,7 +211,7 @@ const Map> localizations = const { "timeOfDayFormat": r"HH:mm", @@ -236,7 +236,7 @@ const Map> localizations = const { "timeOfDayFormat": r"H:mm", @@ -261,7 +261,7 @@ const Map> localizations = const { "timeOfDayFormat": r"HH:mm", @@ -286,7 +286,7 @@ const Map> localizations = const { "timeOfDayFormat": r"HH:mm", @@ -311,7 +311,7 @@ const Map> localizations = const { "timeOfDayFormat": r"H:mm", @@ -336,7 +336,7 @@ const Map> localizations = const { "timeOfDayFormat": r"HH:mm", @@ -361,7 +361,7 @@ const Map> localizations = const { "timeOfDayFormat": r"h:mm a", @@ -388,12 +388,12 @@ const Map> localizations = const { "timeOfDayFormat": r"ah:mm", "openAppDrawerTooltip": r"打开导航菜单", - "backButtonTooltip": r"背部", + "backButtonTooltip": r"返回", "closeButtonTooltip": r"关", "nextMonthTooltip": r"-下月就29了。", "previousMonthTooltip": r"前一个月", @@ -415,7 +415,6 @@ const Map> localizations = const result = {}; if (localizations.containsKey(locale.languageCode)) - result.addAll(localizations[locale.languageCode]); - if (localizations.containsKey(locale.toString())) - result.addAll(localizations[locale.toString()]); - return new DefaultMaterialLocalizations._(locale, result); + _nameToValue.addAll(localizations[locale.languageCode]); + if (localizations.containsKey(_localeName)) + _nameToValue.addAll(localizations[_localeName]); } - DefaultMaterialLocalizations._(this.locale, this._nameToValue); - /// The locale for which the values of this class's localized resources /// have been translated. final Locale locale; - final Map _nameToValue; + final Map _nameToValue = {}; String get _localeName { final String localeName = locale.countryCode.isEmpty ? locale.languageCode : locale.toString(); diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index 4f01802d43..93eb0d1ee0 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -383,11 +383,14 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv } // Combine the Localizations for Widgets with the ones contributed - // by the localizationsDelegates parameter, if any. + // by the localizationsDelegates parameter, if any. Only the first delegate + // of a particular LocalizationsDelegate.type is loaded so the + // localizationsDelegate parameter can be used to override + // _WidgetsLocalizationsDelegate. Iterable> get _localizationsDelegates sync* { - yield const _WidgetsLocalizationsDelegate(); // TODO(ianh): make this configurable if (widget.localizationsDelegates != null) yield* widget.localizationsDelegates; + yield const _WidgetsLocalizationsDelegate(); } @override diff --git a/packages/flutter/lib/src/widgets/localizations.dart b/packages/flutter/lib/src/widgets/localizations.dart index 13ce6f50e2..75ab2c51c0 100644 --- a/packages/flutter/lib/src/widgets/localizations.dart +++ b/packages/flutter/lib/src/widgets/localizations.dart @@ -38,10 +38,20 @@ class _Pending { // This is more complicated than just applying Future.wait to input // because some of the input.values may be SynchronousFutures. We don't want // to Future.wait for the synchronous futures. -Future> _loadAll(Locale locale, Iterable> delegates) { +Future> _loadAll(Locale locale, Iterable> allDelegates) { final Map output = {}; List<_Pending> pendingList; + // Only load the first delegate for each delgate type. + final Set types = new Set(); + final List> delegates = >[]; + for (LocalizationsDelegate delegate in allDelegates) { + if (!types.contains(delegate.type)) { + types.add(delegate.type); + delegates.add(delegate); + } + } + for (LocalizationsDelegate delegate in delegates) { final Future inputValue = delegate.load(locale); dynamic completedValue; @@ -227,6 +237,9 @@ class _LocalizationsScope extends InheritedWidget { /// class _MyDelegate extends LocalizationsDelegate { /// @override /// Future load(Locale locale) => MyLocalizations.load(locale); +/// +/// @override +/// bool shouldReload(MyLocalizationsDelegate old) => false; ///} /// ``` /// @@ -298,8 +311,7 @@ class _LocalizationsScope extends InheritedWidget { /// One could choose another approach for loading localized resources and looking them up while /// still conforming to the structure of this example. class Localizations extends StatefulWidget { - /// Create a widget from which ambient localizations (translated strings) - /// can be obtained. + /// Create a widget from which localizations (like translated strings) can be obtained. Localizations({ Key key, @required this.locale, @@ -311,6 +323,51 @@ class Localizations extends StatefulWidget { assert(delegates.any((LocalizationsDelegate delegate) => delegate is LocalizationsDelegate)); } + /// Overrides the inherited [Locale] or [LocalizationsDelegate]s for `child`. + /// + /// This factory constructor is used for the (usually rare) situtation where part + /// of an app should be localized for a different locale than the one defined + /// for the device, or if its localizations should come from a different list + /// of [LocalizationsDelegate]s than the list defined by + /// [WidgetsApp.localizationsDelegates]. + /// + /// For example you could specify that `myWidget` was only to be localized for + /// the US English locale: + /// + /// ```dart + /// Widget build(BuildContext context) { + /// return new Localizations.override( + /// context: context, + /// locale: const Locale('en', 'US'), + /// child: myWidget, + /// ); + /// } + /// ``` + /// + /// The `locale` and `delegates` parameters default to the [Localizations.locale] + /// and [Localizations.delegates] values from the nearest [Localizations] ancestor. + /// + /// To override the [Localizations.locale] or [Localizations.delegates] for an + /// entire app, specify [WidgetsApp.locale] or [WidgetsApp.localizationsDelegates] + /// (or specify the same parameters for [MaterialApp]). + factory Localizations.override({ + Key key, + @required BuildContext context, + Locale locale, + List> delegates, + Widget child, + }) { + final List> mergedDelegates = Localizations._delegatesOf(context); + if (delegates != null) + mergedDelegates.insertAll(0, delegates); + return new Localizations( + key: key, + locale: locale ?? Localizations.localeOf(context), + delegates: mergedDelegates, + child: child, + ); + } + /// The resources returned by [Localizations.of] will be specific to this locale. final Locale locale; @@ -326,9 +383,19 @@ class Localizations extends StatefulWidget { static Locale localeOf(BuildContext context) { assert(context != null); final _LocalizationsScope scope = context.inheritFromWidgetOfExactType(_LocalizationsScope); + assert(scope != null, 'a Localizations ancestor was not found'); return scope.localizationsState.locale; } + // There doesn't appear to be a need to make this public. See the + // Localizations.override factory constructor. + static List> _delegatesOf(BuildContext context) { + assert(context != null); + final _LocalizationsScope scope = context.inheritFromWidgetOfExactType(_LocalizationsScope); + assert(scope != null, 'a Localizations ancestor was not found'); + return new List>.from(scope.localizationsState.widget.delegates); + } + /// Returns the 'type' localized resources for the widget tree that /// corresponds to [BuildContext] `context`. /// @@ -345,6 +412,7 @@ class Localizations extends StatefulWidget { assert(context != null); assert(type != null); final _LocalizationsScope scope = context.inheritFromWidgetOfExactType(_LocalizationsScope); + assert(scope != null, 'a Localizations ancestor was not found'); return scope.localizationsState.resourcesFor(type); } diff --git a/packages/flutter/test/material/localizations_test.dart b/packages/flutter/test/material/localizations_test.dart index 73e2ecb339..def6f3cc5f 100644 --- a/packages/flutter/test/material/localizations_test.dart +++ b/packages/flutter/test/material/localizations_test.dart @@ -3,11 +3,33 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; +class FooMaterialLocalizations extends DefaultMaterialLocalizations { + FooMaterialLocalizations(Locale locale) : super(locale); + + @override + String get backButtonTooltip => 'foo'; +} + +class FooMaterialLocalizationsDelegate extends LocalizationsDelegate { + const FooMaterialLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return new SynchronousFuture(new FooMaterialLocalizations(locale)); + } + + @override + bool shouldReload(FooMaterialLocalizationsDelegate old) => false; +} + Widget buildFrame({ Locale locale, + Iterable> delegates, WidgetBuilder buildContent, + LocaleResolutionCallback localeResolutionCallback, Iterable supportedLocales: const [ const Locale('en', 'US'), const Locale('es', 'es'), @@ -16,6 +38,8 @@ Widget buildFrame({ return new MaterialApp( color: const Color(0xFFFFFFFF), locale: locale, + localizationsDelegates: delegates, + localeResolutionCallback: localeResolutionCallback, supportedLocales: supportedLocales, onGenerateRoute: (RouteSettings settings) { return new MaterialPageRoute( @@ -133,6 +157,101 @@ void main() { expect(localizations.selectedRowCountTitle(123456789), '123.456.789 artículos seleccionados'); }); + testWidgets('Localizations.override widget tracks parent\'s locale', (WidgetTester tester) async { + Widget buildLocaleFrame(Locale locale) { + return buildFrame( + locale: locale, + buildContent: (BuildContext context) { + return new Localizations.override( + context: context, + child: new Builder( + builder: (BuildContext context) { + // No MaterialLocalizations are defined for the first Localizations + // ancestor, so we should get the values from the default one, i.e. + // the one created by WidgetsApp via the LocalizationsDelegate + // provided by MaterialApp. + return new Text(MaterialLocalizations.of(context).backButtonTooltip); + }, + ), + ); + } + ); + } + + await tester.pumpWidget(buildLocaleFrame(const Locale('en', 'US'))); + expect(find.text('Back'), findsOneWidget); + + await tester.pumpWidget(buildLocaleFrame(const Locale('de', 'DE'))); + expect(find.text('Zurück'), findsOneWidget); + + await tester.pumpWidget(buildLocaleFrame(const Locale('zh', 'CN'))); + expect(find.text('返回'), findsOneWidget); + }); + + testWidgets('Localizations.override widget with hardwired locale', (WidgetTester tester) async { + Widget buildLocaleFrame(Locale locale) { + return buildFrame( + locale: locale, + buildContent: (BuildContext context) { + return new Localizations.override( + context: context, + locale: const Locale('en', 'US'), + child: new Builder( + builder: (BuildContext context) { + // No MaterialLocalizations are defined for the Localizations.override + // ancestor, so we should get all values from the default one, i.e. + // the one created by WidgetsApp via the LocalizationsDelegate + // provided by MaterialApp. + return new Text(MaterialLocalizations.of(context).backButtonTooltip); + }, + ), + ); + } + ); + } + + await tester.pumpWidget(buildLocaleFrame(const Locale('en', 'US'))); + expect(find.text('Back'), findsOneWidget); + + await tester.pumpWidget(buildLocaleFrame(const Locale('de', 'DE'))); + expect(find.text('Back'), findsOneWidget); + + await tester.pumpWidget(buildLocaleFrame(const Locale('zh', 'CN'))); + expect(find.text('Back'), findsOneWidget); + }); + + testWidgets('MaterialApp overrides MaterialLocalizations', (WidgetTester tester) async { + final Key textKey = new UniqueKey(); + + await tester.pumpWidget( + buildFrame( + // Accept whatever locale we're given + localeResolutionCallback: (Locale locale, Iterable supportedLocales) => locale, + delegates: [ + const FooMaterialLocalizationsDelegate(), + ], + buildContent: (BuildContext context) { + // Should always be 'foo', no matter what the locale is + return new Text( + MaterialLocalizations.of(context).backButtonTooltip, + key: textKey, + ); + } + ) + ); + + expect(tester.widget(find.byKey(textKey)).data, 'foo'); + + await tester.binding.setLocale('zh', 'CN'); + await tester.pump(); + expect(find.text('foo'), findsOneWidget); + + await tester.binding.setLocale('de', 'DE'); + await tester.pump(); + expect(find.text('foo'), findsOneWidget); + + }); + testWidgets('deprecated Android/Java locales are modernized', (WidgetTester tester) async { final Key textKey = new UniqueKey(); diff --git a/packages/flutter/test/widgets/localizations_test.dart b/packages/flutter/test/widgets/localizations_test.dart index 33a18875ef..5fac194fa5 100644 --- a/packages/flutter/test/widgets/localizations_test.dart +++ b/packages/flutter/test/widgets/localizations_test.dart @@ -103,6 +103,36 @@ class AsyncMoreLocalizationsDelegate extends LocalizationsDelegate false; } +// Same as _WidgetsLocalizationsDelegate in widgets/app.dart +class DefaultWidgetsLocalizationsDelegate extends LocalizationsDelegate { + const DefaultWidgetsLocalizationsDelegate(); + + @override + Future load(Locale locale) => DefaultWidgetsLocalizations.load(locale); + + @override + bool shouldReload(DefaultWidgetsLocalizationsDelegate old) => false; +} + +class OnlyRTLDefaultWidgetsLocalizations extends DefaultWidgetsLocalizations { + OnlyRTLDefaultWidgetsLocalizations(Locale locale) : super(locale); + + @override + TextDirection get textDirection => TextDirection.rtl; +} + +class OnlyRTLDefaultWidgetsLocalizationsDelegate extends LocalizationsDelegate { + const OnlyRTLDefaultWidgetsLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return new SynchronousFuture(new OnlyRTLDefaultWidgetsLocalizations(locale)); + } + + @override + bool shouldReload(OnlyRTLDefaultWidgetsLocalizationsDelegate old) => false; +} + Widget buildFrame({ Locale locale, Iterable> delegates, @@ -504,15 +534,116 @@ void main() { await tester.pumpAndSettle(); expect(find.text('zh_CN'), findsOneWidget); }); -} - -// Same as _WidgetsLocalizationsDelegate in widgets/app.dart -class DefaultWidgetsLocalizationsDelegate extends LocalizationsDelegate { - const DefaultWidgetsLocalizationsDelegate(); - - @override - Future load(Locale locale) => DefaultWidgetsLocalizations.load(locale); - - @override - bool shouldReload(DefaultWidgetsLocalizationsDelegate old) => false; + + 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 supportedLocales) => locale, + buildContent: (BuildContext context) { + return new Localizations.override( + context: context, + child: new Builder( + builder: (BuildContext context) { + final Locale locale = Localizations.localeOf(context); + final TextDirection direction = WidgetsLocalizations.of(context).textDirection; + return new Text('$locale $direction'); + }, + ), + ); + } + ) + ); + + // Initial WidgetTester locale is new Locale('', '') + await tester.pumpAndSettle(); + expect(find.text('_ 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 supportedLocales) => locale, + buildContent: (BuildContext context) { + return new Localizations.override( + context: context, + delegates: [ + // Override: no matter what the locale, textDirection is always RTL. + const OnlyRTLDefaultWidgetsLocalizationsDelegate(), + ], + child: new Builder( + builder: (BuildContext context) { + final Locale locale = Localizations.localeOf(context); + final TextDirection direction = WidgetsLocalizations.of(context).textDirection; + return new Text('$locale $direction'); + }, + ), + ); + } + ) + ); + + // Initial WidgetTester locale is new Locale('', '') + await tester.pumpAndSettle(); + expect(find.text('_ 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 supportedLocales) => locale, + delegates: [ + const OnlyRTLDefaultWidgetsLocalizationsDelegate(), + ], + buildContent: (BuildContext context) { + final Locale locale = Localizations.localeOf(context); + final TextDirection direction = WidgetsLocalizations.of(context).textDirection; + return new Text('$locale $direction'); + } + ) + ); + + // Initial WidgetTester locale is new Locale('', '') + await tester.pumpAndSettle(); + expect(find.text('_ 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); + }); + }