Unverified Commit dfd02295 authored by Gary Qian's avatar Gary Qian Committed by GitHub

New locale resolution algorithm to use full preferred locale list, include...

New locale resolution algorithm to use full preferred locale list, include scriptCode in Locale. (#23583)
parent 5071657e
......@@ -90,6 +90,7 @@ class CupertinoApp extends StatefulWidget {
this.color,
this.locale,
this.localizationsDelegates,
this.localeListResolutionCallback,
this.localeResolutionCallback,
this.supportedLocales = const <Locale>[Locale('en', 'US')],
this.showPerformanceOverlay = false,
......@@ -157,6 +158,11 @@ class CupertinoApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.localizationsDelegates}
final Iterable<LocalizationsDelegate<dynamic>> localizationsDelegates;
/// {@macro flutter.widgets.widgetsApp.localeListResolutionCallback}
///
/// This callback is passed along to the [WidgetsApp] built by this widget.
final LocaleListResolutionCallback localeListResolutionCallback;
/// {@macro flutter.widgets.widgetsApp.localeResolutionCallback}
///
/// This callback is passed along to the [WidgetsApp] built by this widget.
......@@ -283,6 +289,7 @@ class _CupertinoAppState extends State<CupertinoApp> {
locale: widget.locale,
localizationsDelegates: _localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback,
localeListResolutionCallback: widget.localeListResolutionCallback,
supportedLocales: widget.supportedLocales,
showPerformanceOverlay: widget.showPerformanceOverlay,
checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
......
......@@ -98,6 +98,7 @@ class MaterialApp extends StatefulWidget {
this.theme,
this.locale,
this.localizationsDelegates,
this.localeListResolutionCallback,
this.localeResolutionCallback,
this.supportedLocales = const <Locale>[Locale('en', 'US')],
this.debugShowMaterialGrid = false,
......@@ -264,6 +265,11 @@ class MaterialApp extends StatefulWidget {
/// <https://flutter.io/tutorials/internationalization/>.
final Iterable<LocalizationsDelegate<dynamic>> localizationsDelegates;
/// {@macro flutter.widgets.widgetsApp.localeListResolutionCallback}
///
/// This callback is passed along to the [WidgetsApp] built by this widget.
final LocaleListResolutionCallback localeListResolutionCallback;
/// {@macro flutter.widgets.widgetsApp.localeResolutionCallback}
///
/// This callback is passed along to the [WidgetsApp] built by this widget.
......@@ -423,6 +429,7 @@ class _MaterialAppState extends State<MaterialApp> {
locale: widget.locale,
localizationsDelegates: _localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback,
localeListResolutionCallback: widget.localeListResolutionCallback,
supportedLocales: widget.supportedLocales,
showPerformanceOverlay: widget.showPerformanceOverlay,
checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:collection' show HashMap;
import 'dart:ui' as ui show window;
import 'package:flutter/foundation.dart';
......@@ -24,19 +25,50 @@ import 'widget_inspector.dart';
export 'dart:ui' show Locale;
/// The signature of [WidgetsApp.localeListResolutionCallback].
///
/// A [LocaleListResolutionCallback] is responsible for computing the locale of the app's
/// [Localizations] object when the app starts and when user changes the list of
/// locales for the device.
///
/// The [locales] list is the device's preferred locales when the app started, or the
/// device's preferred locales the user selected after the app was started. This list
/// is in order of preference. If this list is null or empty, then Flutter has not yet
/// recieved the locale information from the platform. The [supportedLocales] parameter
/// is just the value of [WidgetsApp.supportedLocales].
///
/// See also:
///
/// * [LocaleResolutionCallback], which takes only one default locale (instead of a list)
/// and is attempted only after this callback fails or is null. [LocaleListResolutionCallback]
/// is recommended over [LocaleResolutionCallback].
typedef LocaleListResolutionCallback = Locale Function(List<Locale> locales, Iterable<Locale> supportedLocales);
/// The signature of [WidgetsApp.localeResolutionCallback].
///
/// A `LocaleResolutionCallback` is responsible for computing the locale of the app's
/// It is recommended to provide a [LocaleListResolutionCallback] instead of a
/// [LocaleResolutionCallback] when possible, as [LocaleListResolutionCallback] as
/// this callback only recieves a subset of the information provided
/// in [LocaleListResolutionCallback].
///
/// A [LocaleResolutionCallback] is responsible for computing the locale of the app's
/// [Localizations] object when the app starts and when user changes the default
/// locale for the device.
/// locale for the device after [LocaleListResolutionCallback] fails or is not provided.
///
/// This callback is also used if the app is created with a specific locale using
/// the [new WidgetsApp] `locale` parameter.
///
/// The `locale` is either the value of [WidgetsApp.locale], or the device's
/// locale when the app started, or the device locale the user selected after
/// the app was started. The `supportedLocales` parameter is the value of
/// The [locale] is either the value of [WidgetsApp.locale], or the device's default
/// locale when the app started, or the device locale the user selected after the app
/// was started. The default locale is the first locale in the list of preferred
/// locales. If [locale] is null, then Flutter has not yet recieved the locale
/// information from the platform. The [supportedLocales] parameter is just the value of
/// [WidgetsApp.supportedLocales].
///
/// See also:
///
/// * [LocaleListResolutionCallback], which takes a list of preferred locales (instead of one locale).
/// Resolutions by [LocaleListResolutionCallback] take precedence over [LocaleResolutionCallback].
typedef LocaleResolutionCallback = Locale Function(Locale locale, Iterable<Locale> supportedLocales);
/// The signature of [WidgetsApp.onGenerateTitle].
......@@ -124,6 +156,7 @@ class WidgetsApp extends StatefulWidget {
@required this.color,
this.locale,
this.localizationsDelegates,
this.localeListResolutionCallback,
this.localeResolutionCallback,
this.supportedLocales = const <Locale>[Locale('en', 'US')],
this.showPerformanceOverlay = false,
......@@ -468,29 +501,49 @@ class WidgetsApp extends StatefulWidget {
/// {@endtemplate}
final Iterable<LocalizationsDelegate<dynamic>> localizationsDelegates;
/// {@template flutter.widgets.widgetsApp.localeResolutionCallback}
/// {@template flutter.widgets.widgetsApp.localeListResolutionCallback}
/// This callback is responsible for choosing the app's locale
/// when the app is started, and when the user changes the
/// device's locale.
///
/// The returned value becomes the locale of this app's [Localizations]
/// widget. The callback's `locale` parameter is the device's locale when
/// the app started, or the device locale the user selected after the app was
/// started. The callback's `supportedLocales` parameter is just the value
/// [supportedLocales].
/// When a [localeListResolutionCallback] is provided, Flutter will first attempt to
/// resolve the locale with the provided [localeListResolutionCallback]. If the
/// callback or result is null, it will fallback to trying the [localeResolutionCallback].
/// If both [localeResolutionCallback] and [localeListResolutionCallback] are left null
/// or fail to resolve (return null), the [WidgetsApp.basicLocaleListResolution]
/// fallback algorithm will be used.
///
/// If the callback is null or if it returns null then the resolved locale is:
/// The priority of each available fallback is:
///
/// - The callback's `locale` parameter if it's equal to a supported locale.
/// - The first supported locale with the same [Locale.languageCode] as the
/// callback's `locale` parameter.
/// - The first locale in [supportedLocales].
/// 1. [localeListResolutionCallback] is attempted first.
/// 2. [localeResolutionCallback] is attempted second.
/// 3. Flutter's [WidgetsApp.basicLocaleListResolution] algorithm is attempted last.
/// {@endtemplate}
///
/// This callback considers the entire list of preferred locales.
///
/// This algorithm should be able to handle a null or empty list of preferred locales,
/// which indicates Flutter has not yet recieved locale information from the platform.
///
/// See also:
///
/// * [MaterialApp.localeResolutionCallback], which sets the callback of the
/// [WidgetsApp] it creates.
final LocaleListResolutionCallback localeListResolutionCallback;
/// {@macro flutter.widgets.widgetsApp.localeListResolutionCallback}
///
/// This callback considers only the default locale, which is the first locale
/// in the preferred locales list. It is preferred to set [localeListResolutionCallback]
/// over [localeResolutionCallback] as it provides the full preferred locales list.
///
/// This algorithm should be able to handle a null locale, which indicates
/// Flutter has not yet recieved locale information from the platform.
///
/// See also:
///
/// * [MaterialApp.localeListResolutionCallback], which sets the callback of the
/// [WidgetsApp] it creates.
final LocaleResolutionCallback localeResolutionCallback;
/// {@template flutter.widgets.widgetsApp.supportedLocales}
......@@ -607,7 +660,7 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
void initState() {
super.initState();
_updateNavigator();
_locale = _resolveLocale(ui.window.locale, widget.supportedLocales);
_locale = _resolveLocales(ui.window.locales, widget.supportedLocales);
WidgetsBinding.instance.addObserver(this);
}
......@@ -717,35 +770,147 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
// LOCALIZATION
/// This is the resolved locale, and is one of the supportedLocales.
Locale _locale;
Locale _resolveLocale(Locale newLocale, Iterable<Locale> supportedLocales) {
Locale _resolveLocales(List<Locale> preferredLocales, Iterable<Locale> supportedLocales) {
// Attempt to use localeListResolutionCallback.
if (widget.localeListResolutionCallback != null) {
final Locale locale = widget.localeListResolutionCallback(preferredLocales, widget.supportedLocales);
if (locale != null)
return locale;
}
// localeListResolutionCallback failed, falling back to localeResolutionCallback.
if (widget.localeResolutionCallback != null) {
final Locale locale = widget.localeResolutionCallback(newLocale, widget.supportedLocales);
final Locale locale = widget.localeResolutionCallback(preferredLocales.first, widget.supportedLocales);
if (locale != null)
return locale;
}
// newLocale can be null when called before the platform has had a chance to
// initialize the locales. We default to the first supported locale.
if (newLocale == null) {
// Both callbacks failed, falling back to default algorithm.
return basicLocaleListResolution(preferredLocales, supportedLocales);
}
/// The default locale resolution algorithm.
///
/// Custom resolution algorithms can be provided through [WidgetsApp.localeListResolutionCallback]
/// or [WidgetsApp.localeResolutionCallback].
///
/// When no custom locale resolition algorithms are provided or if both fail to resolve,
/// Flutter will default to calling this algorithm.
///
/// This algorithm prioritizes speed at the cost of slightly less appropriate
/// resolutions for edge cases.
///
/// This algorithm will resolve to the earliest locale in [preferredLocales] that
/// matches the most fields, prioritizing in the order of perfect match,
/// languageCode+countryCode, languageCode+scriptCode, languageCode-only.
///
/// In the case where a locale is matched by languageCode-only and is not the
/// default (first) locale, the next locale in preferredLocales with a
/// perfect match can supercede the languageCode-only match if it exists.
///
/// When a preferredLocale matches more than one supported locale, it will resolve
/// to the first matching locale listed in the supportedLocales.
///
/// When all [preferredLocales] have been exhausted without a match, the first countryCode only
/// match will be returned.
///
/// When no match at all is found, the first (default) locale in [supportedLocales] will be
/// returned.
///
/// This algorithm does not take language distance (how similar languages are to each other)
/// into account, and will not handle edge cases such as resolving `de` to `fr` rather than `zh`
/// when `de` is not supported and `zh` is listed before `fr` (German is closer to French
/// than Chinese).
static Locale basicLocaleListResolution(List<Locale> preferredLocales, Iterable<Locale> supportedLocales) {
// preferredLocales can be null when called before the platform has had a chance to
// initialize the locales. Platforms without locale passing support will provide an empty list.
// We default to the first supported locale in these cases.
if (preferredLocales == null || preferredLocales.isEmpty) {
return supportedLocales.first;
}
// Hash the supported locales because apps can support many locales and would
// be expensive to search through them many times.
final Map<String, Locale> allSupportedLocales = HashMap<String, Locale>();
final Map<String, Locale> languageAndCountryLocales = HashMap<String, Locale>();
final Map<String, Locale> languageAndScriptLocales = HashMap<String, Locale>();
final Map<String, Locale> languageLocales = HashMap<String, Locale>();
final Map<String, Locale> countryLocales = HashMap<String, Locale>();
for (Locale locale in supportedLocales) {
allSupportedLocales['${locale.languageCode}_${locale.scriptCode}_${locale.countryCode}'] ??= locale;
languageAndScriptLocales['${locale.languageCode}_${locale.scriptCode}'] ??= locale;
languageAndCountryLocales['${locale.languageCode}_${locale.countryCode}'] ??= locale;
languageLocales[locale.languageCode] ??= locale;
countryLocales[locale.countryCode] ??= locale;
}
// Since languageCode-only matches are possibly low quality, we don't return
// it instantly when we find such a match. We check to see if the next
// preferred locale in the list has a high accuracy match, and only return
// the languageCode-only match when a higher accuracy match in the next
// preferred locale cannot be found.
Locale matchesLanguageCode;
for (Locale locale in supportedLocales) {
if (locale == newLocale)
return newLocale;
if (locale.languageCode == newLocale.languageCode)
matchesLanguageCode ??= locale;
Locale matchesCountryCode;
// Loop over user's preferred locales
for (int localeIndex = 0; localeIndex < preferredLocales.length; localeIndex += 1) {
final Locale userLocale = preferredLocales[localeIndex];
// Look for perfect match.
if (allSupportedLocales.containsKey('${userLocale.languageCode}_${userLocale.scriptCode}_${userLocale.countryCode}')) {
return userLocale;
}
// Look for language+script match.
if (userLocale.scriptCode != null) {
final Locale match = languageAndScriptLocales['${userLocale.languageCode}_${userLocale.scriptCode}'];
if (match != null) {
return match;
}
}
// Look for language+country match.
if (userLocale.countryCode != null) {
final Locale match = languageAndCountryLocales['${userLocale.languageCode}_${userLocale.countryCode}'];
if (match != null) {
return match;
}
}
// If there was a languageCode-only match in the previous iteration's higher
// ranked preferred locale, we return it if the current userLocale does not
// have a better match.
if (matchesLanguageCode != null) {
return matchesLanguageCode;
}
// Look and store language-only match.
Locale match = languageLocales[userLocale.languageCode];
if (match != null) {
matchesLanguageCode = match;
// Since first (default) locale is usually highly preferred, we will allow
// a languageCode-only match to be instantly matched. If the next preferred
// languageCode is the same, we defer hastily returning until the next iteration
// since at worst it is the same and at best an improved match.
if (localeIndex == 0 &&
!(localeIndex + 1 < preferredLocales.length && preferredLocales[localeIndex + 1].languageCode == userLocale.languageCode)) {
return matchesLanguageCode;
}
}
// countryCode-only match. When all else except default supported locale fails,
// attempt to match by country only, as a user is likely to be familar with a
// language from their listed country.
if (matchesCountryCode == null && userLocale.countryCode != null) {
match = countryLocales[userLocale.countryCode];
if (match != null) {
matchesCountryCode = match;
}
}
}
return matchesLanguageCode ?? supportedLocales.first;
// When there is no languageCode-only match. Fallback to matching countryCode only. Country
// fallback only applies on iOS. When there is no countryCode-only match, we return first
// suported locale.
final Locale resolvedLocale = matchesLanguageCode ?? matchesCountryCode ?? supportedLocales.first;
return resolvedLocale;
}
@override
void didChangeLocale(Locale locale) {
if (locale == _locale)
return;
final Locale newLocale = _resolveLocale(locale, widget.supportedLocales);
void didChangeLocales(List<Locale> locales) {
final Locale newLocale = _resolveLocales(locales, widget.supportedLocales);
if (newLocale != _locale) {
setState(() {
_locale = newLocale;
......@@ -950,7 +1115,7 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
}
final Locale appLocale = widget.locale != null
? _resolveLocale(widget.locale, widget.supportedLocales)
? _resolveLocales(<Locale>[widget.locale], widget.supportedLocales)
: _locale;
assert(_debugCheckLocalizations(appLocale));
......
......@@ -219,7 +219,7 @@ abstract class WidgetsBindingObserver {
/// settings.
///
/// This method exposes notifications from [Window.onLocaleChanged].
void didChangeLocale(Locale locale) { }
void didChangeLocales(List<Locale> locale) { }
/// Called when the system puts the app in the background or returns
/// the app to the foreground.
......@@ -413,20 +413,20 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB
@protected
@mustCallSuper
void handleLocaleChanged() {
dispatchLocaleChanged(ui.window.locale);
dispatchLocalesChanged(ui.window.locales);
}
/// Notify all the observers that the locale has changed (using
/// [WidgetsBindingObserver.didChangeLocale]), giving them the
/// `locale` argument.
/// [WidgetsBindingObserver.didChangeLocales]), giving them the
/// `locales` argument.
///
/// This is called by [handleLocaleChanged] when the [Window.onLocaleChanged]
/// notification is received.
@protected
@mustCallSuper
void dispatchLocaleChanged(Locale locale) {
void dispatchLocalesChanged(List<Locale> locales) {
for (WidgetsBindingObserver observer in _observers)
observer.didChangeLocale(locale);
observer.didChangeLocales(locales);
}
/// Notify all the observers that the active set of [AccessibilityFeatures]
......
......@@ -745,4 +745,673 @@ void main() {
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);
});
}
......@@ -225,13 +225,25 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
Duration additionalTime = const Duration(milliseconds: 250),
});
/// Artificially calls dispatchLocaleChanged on the Widget binding,
/// Artificially calls dispatchLocalesChanged on the Widget binding,
/// then flushes microtasks.
///
/// Passes only one single Locale. Use [setLocales] to pass a full preferred
/// locales list.
Future<void> setLocale(String languageCode, String countryCode) {
return TestAsyncUtils.guard<void>(() async {
assert(inTest);
final Locale locale = Locale(languageCode, countryCode);
dispatchLocaleChanged(locale);
final Locale locale = Locale(languageCode, countryCode == '' ? null : countryCode);
dispatchLocalesChanged(<Locale>[locale]);
});
}
/// Artificially calls dispatchLocalesChanged on the Widget binding,
/// then flushes microtasks.
Future<void> setLocales(List<Locale> locales) {
return TestAsyncUtils.guard<void>(() async {
assert(inTest);
dispatchLocalesChanged(locales);
});
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment