Commit 4262c1e9 authored by Hans Muller's avatar Hans Muller Committed by Ian Hickson

Make an app's supported locales configurable (#11946)

* Make an app's supported locales configurable

* Added an supportedLocales.isNotEmpty assert

* WidgetsApp no longer const because supportedLocales.isNotEmpty

* updated per review feedback

* tweaked dartdoc to restart the build

* updated per review feedback

* Updated per review feedback
parent 3bf3df33
......@@ -121,6 +121,10 @@ class StocksAppState extends State<StocksApp> {
localizationsDelegates: <_StocksLocalizationsDelegate>[
new _StocksLocalizationsDelegate(),
],
supportedLocales: const <Locale>[
const Locale('en', 'US'),
const Locale('es', 'ES'),
],
debugShowMaterialGrid: _configuration.debugShowGrid,
showPerformanceOverlay: _configuration.showPerformanceOverlay,
showSemanticsDebugger: _configuration.showSemanticsDebugger,
......
......@@ -94,6 +94,8 @@ class MaterialApp extends StatefulWidget {
this.onUnknownRoute,
this.locale,
this.localizationsDelegates,
this.localeResolutionCallback,
this.supportedLocales: const <Locale>[const Locale('en', 'US')],
this.navigatorObservers: const <NavigatorObserver>[],
this.debugShowMaterialGrid: false,
this.showPerformanceOverlay: false,
......@@ -233,6 +235,65 @@ class MaterialApp extends StatefulWidget {
/// for this application's [Localizations] widget.
final Iterable<LocalizationsDelegate<dynamic>> localizationsDelegates;
/// 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].
///
/// An app could use this callback to substitute locales based on the app's
/// intended audience. If the device's OS provides a prioritized
/// list of locales, this callback could be used to defer to it.
///
/// If the callback is null then the resolved locale 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 supported locale.
///
/// This callback is passed along to the [WidgetsApp] built by this widget.
final LocaleResolutionCallback localeResolutionCallback;
/// The list of locales that this app has been localized for.
///
/// By default only the American English locale is supported. Apps should
/// configure this list to match the locales they support.
///
/// This list must not null. It's default value is just
/// `[const Locale('en', 'US')]`. It is simply passed along to the
/// [WidgetsApp] built by this widget.
///
/// The order of the list matters. By default, if the device's locale doesn't
/// exactly match a locale in [supportedLocales] then the first locale in
/// [supportedLocales] with a matching [Locale.languageCode] is used. If that
/// fails then the first locale in [supportedLocales] is used. The default
/// locale resolution algorithm can be overridden with [localeResolutionCallback].
///
/// The material widgets include translations for locales with the following
/// language codes:
/// ```
/// ar - Arabic
/// de - German
/// en - English
/// es - Spanish
/// fa - Farsi (Persian)
/// fr - French
/// he - Hebrew
/// it - Italian
/// ja - Japanese
/// ps - Pashto
/// pt - Portugese
/// ru - Russian
/// sd - Sindhi
/// ur - Urdu
/// zh - Chinese (simplified)
/// ```
final Iterable<Locale> supportedLocales;
/// Turns on a performance overlay.
///
/// See also:
......@@ -399,6 +460,8 @@ class _MaterialAppState extends State<MaterialApp> {
onUnknownRoute: _onUnknownRoute,
locale: widget.locale,
localizationsDelegates: _localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback,
supportedLocales: widget.supportedLocales,
showPerformanceOverlay: widget.showPerformanceOverlay,
checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
......
......@@ -23,6 +23,17 @@ import 'widget_inspector.dart';
export 'dart:ui' show Locale;
/// The signature of [WidgetsApp.localeResolutionCallback].
///
/// 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.
///
/// The `locale` is the device's locale when the app started, or the device
/// locale the user selected after the app was started. The `supportedLocales`
/// parameter is just the value of [WidgetApp.supportedLocales].
typedef Locale LocaleResolutionCallback(Locale locale, Iterable<Locale> supportedLocales);
// Delegate that fetches the default (English) strings.
class _WidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
const _WidgetsLocalizationsDelegate();
......@@ -52,7 +63,10 @@ class WidgetsApp extends StatefulWidget {
///
/// The boolean arguments, [color], [navigatorObservers], and
/// [onGenerateRoute] must not be null.
const WidgetsApp({
///
/// The `supportedLocales` argument must be a list of one or more elements.
/// By default supportedLocales is `[const Locale('en', 'US')]`.
WidgetsApp({ // can't be const because the asserts use methods on Iterable :-(
Key key,
@required this.onGenerateRoute,
this.onUnknownRoute,
......@@ -63,6 +77,8 @@ class WidgetsApp extends StatefulWidget {
this.initialRoute,
this.locale,
this.localizationsDelegates,
this.localeResolutionCallback,
this.supportedLocales: const <Locale>[const Locale('en', 'US')],
this.showPerformanceOverlay: false,
this.checkerboardRasterCacheImages: false,
this.checkerboardOffscreenLayers: false,
......@@ -73,6 +89,7 @@ class WidgetsApp extends StatefulWidget {
}) : assert(onGenerateRoute != null),
assert(color != null),
assert(navigatorObservers != null),
assert(supportedLocales != null && supportedLocales.isNotEmpty),
assert(showPerformanceOverlay != null),
assert(checkerboardRasterCacheImages != null),
assert(checkerboardOffscreenLayers != null),
......@@ -149,6 +166,55 @@ class WidgetsApp extends StatefulWidget {
/// for this application's [Localizations] widget.
final Iterable<LocalizationsDelegate<dynamic>> localizationsDelegates;
/// 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].
///
/// If the callback is null or if it returns null then the resolved locale is:
///
/// - The callback's `locale` parameter if it's equal to a supported locale.
/// - The first supported locale with the same [Locale.langaugeCode] as the
/// callback's `locale` parameter.
/// - The first locale in [supportedLocales].
///
/// See also:
///
/// * [MaterialApp.localeResolutionCallback], which sets the callback of the
/// [WidgetsApp] it creates.
final LocaleResolutionCallback localeResolutionCallback;
/// The list of locales that this app has been localized for.
///
/// By default only the American English locale is supported. Apps should
/// configure this list to match the locales they support.
///
/// This list must not null. Its default value is just
/// `[const Locale('en', 'US')]`.
///
/// The order of the list matters. By default, if the device's locale doesn't
/// exactly match a locale in [supportedLocales] then the first locale in
/// [supportedLocales] with a matching [Locale.languageCode] is used. If that
/// fails then the first locale in [supportedLocales] is used. The default
/// locale resolution algorithm can be overridden with [localeResolutionCallback].
///
/// See also:
///
/// * [MaterialApp.supportedLocales], which sets the `supportedLocales`
/// of the [WidgetsApp] it creates.
///
/// * [localeResolutionCallback], an app callback that resolves the app's locale
/// when the device's locale changes.
///
/// * [localizationDelegates], which collectively define all of the localized
/// resources used by this app.
final Iterable<Locale> supportedLocales;
/// Turns on a performance overlay.
/// https://flutter.io/debugging/#performanceoverlay
final bool showPerformanceOverlay;
......@@ -231,11 +297,28 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
GlobalObjectKey<NavigatorState> _navigator;
Locale _locale;
Locale _resolveLocale(Locale newLocale, Iterable<Locale> supportedLocales) {
if (widget.localeResolutionCallback != null) {
final Locale locale = widget.localeResolutionCallback(newLocale, widget.supportedLocales);
if (locale != null)
return locale;
}
Locale matchesLanguageCode;
for (Locale locale in supportedLocales) {
if (locale == newLocale)
return newLocale;
if (locale.languageCode == newLocale.languageCode)
matchesLanguageCode ??= locale;
}
return matchesLanguageCode ?? supportedLocales.first;
}
@override
void initState() {
super.initState();
_navigator = new GlobalObjectKey<NavigatorState>(this);
_locale = ui.window.locale;
_locale = _resolveLocale(ui.window.locale, widget.supportedLocales);
WidgetsBinding.instance.addObserver(this);
}
......@@ -273,9 +356,12 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
@override
void didChangeLocale(Locale locale) {
if (locale != _locale) {
if (locale == _locale)
return;
final Locale newLocale = _resolveLocale(locale, widget.supportedLocales);
if (newLocale != _locale) {
setState(() {
_locale = locale;
_locale = newLocale;
});
}
}
......
......@@ -12,6 +12,10 @@ Widget buildFrame({
return new MaterialApp(
color: const Color(0xFFFFFFFF),
locale: locale,
supportedLocales: const <Locale>[
const Locale('en', 'US'),
const Locale('es', 'es'),
],
onGenerateRoute: (RouteSettings settings) {
return new MaterialPageRoute<Null>(
builder: (BuildContext context) {
......@@ -39,15 +43,16 @@ void main() {
expect(tester.widget<Text>(find.byKey(textKey)).data, 'Back');
// Unrecognized locale falls back to 'en'
await tester.binding.setLocale('foo', 'bar');
await tester.pump();
expect(tester.widget<Text>(find.byKey(textKey)).data, 'Back');
// Spanish Bolivia locale, falls back to just 'es'
await tester.binding.setLocale('es', 'bo');
await tester.pump();
expect(tester.widget<Text>(find.byKey(textKey)).data, 'Espalda');
// Unrecognized locale falls back to 'en'
await tester.binding.setLocale('foo', 'bar');
await tester.pump();
expect(tester.widget<Text>(find.byKey(textKey)).data, 'Back');
});
testWidgets('translations exist for all materia/i18n languages', (WidgetTester tester) async {
......
......@@ -107,11 +107,18 @@ Widget buildFrame({
Locale locale,
Iterable<LocalizationsDelegate<dynamic>> delegates,
WidgetBuilder buildContent,
LocaleResolutionCallback localeResolutionCallback,
List<Locale> supportedLocales: const <Locale>[
const Locale('en', 'US'),
const Locale('en', 'GB'),
],
}) {
return new WidgetsApp(
color: const Color(0xFFFFFFFF),
locale: locale,
localizationsDelegates: delegates,
localeResolutionCallback: localeResolutionCallback,
supportedLocales: supportedLocales,
onGenerateRoute: (RouteSettings settings) {
return new PageRouteBuilder<Null>(
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) {
......@@ -182,7 +189,7 @@ void main() {
);
expect(TestLocalizations.of(pageContext), isNotNull);
expect(find.text('_'), findsOneWidget); // default test locale is '_'
expect(find.text('en_US'), findsOneWidget);
await tester.binding.setLocale('en', 'GB');
await tester.pump();
......@@ -205,25 +212,25 @@ void main() {
)
);
await tester.pump(const Duration(milliseconds: 50)); // TestLocalizations.loadAsync() takes 100ms
expect(find.text('_'), findsNothing); // TestLocalizations hasn't been loaded yet
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('_'), findsOneWidget); // default test locale is '_'
expect(find.text('en_US'), findsOneWidget); // default test locale is US english
await tester.binding.setLocale('en', 'US');
await tester.binding.setLocale('en', 'GB');
await tester.pump(const Duration(milliseconds: 100));
await tester.pumpAndSettle();
expect(find.text('en_US'), findsOneWidget);
expect(find.text('en_GB'), findsOneWidget);
await tester.binding.setLocale('en', 'GB');
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_US'), findsOneWidget);
expect(find.text('en_GB'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 50)); // finish the async load
await tester.pumpAndSettle();
expect(find.text('en_GB'), findsOneWidget);
expect(find.text('en_US'), findsOneWidget);
});
testWidgets('Localizations with multiple sync delegates', (WidgetTester tester) async {
......@@ -422,6 +429,10 @@ void main() {
await tester.pumpWidget(
buildFrame(
supportedLocales: const <Locale>[
const Locale('en', 'GB'),
const Locale('ar', 'EG'),
],
buildContent: (BuildContext context) {
pageContext = context;
return const Text('Hello World');
......@@ -437,6 +448,62 @@ void main() {
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 new 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>[
const Locale('zh', 'CN'),
const Locale('en', 'GB'),
const Locale('en', 'CA'),
],
buildContent: (BuildContext context) {
return new Text(Localizations.localeOf(context).toString());
}
)
);
// Startup time. Default test locale is const Locale('', ''), so
// no supported matches. Use the first locale.
await tester.pumpAndSettle();
expect(find.text('zh_CN'), 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);
});
}
// Same as _WidgetsLocalizationsDelegate in widgets/app.dart
......
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