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
This commit is contained in:
parent
3bf3df33ea
commit
4262c1e9d3
@ -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 '_'
|
||||
|
||||
await tester.binding.setLocale('en', 'US');
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('en_US'), findsOneWidget);
|
||||
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_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
|
||||
|
Loading…
x
Reference in New Issue
Block a user