Commit 8c2c5022 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Updated I18N API for Flutter (#11497)

parent 7676e511
To rebuild the i18n files:
## Regenerating the i18n files
The files in this directory are based on ../lib/stock_strings.dart
which defines all of the localizable strings used by the stocks
app. The stocks app uses
the [Dart `intl` package](https://github.com/dart-lang/intl).
Rebuilding everything requires two steps.
With the `examples/stocks` as the current directory, generate
`intl_messages.arb` from `lib/stock_strings.dart`:
```
pub run intl_translation:generate_from_arb \
--output-dir=lib/i18n \
--generated-file-prefix=stock_ \
--no-use-deferred-loading \
lib/*.dart \
lib/i18n/*.arb
flutter pub pub run intl_translation:extract_to_arb --output-dir=lib/i18n lib/stock_strings.dart
```
The `intl_messages.arb` file is a JSON format map with one entry for
each `Intl.message()` function defined in `stock_strings.dart`. This
file was used to create the English and Spanish localizations,
`stocks_en.arb` and `stocks_es.arb`. The `intl_messages.arb` wasn't
checked into the repository, since it only serves as a template for
the other `.arb` files.
With the `examples/stocks` as the current directory, generate a
`stock_messages_<locale>.dart` for each `stocks_<locale>.arb` file and
`stock_messages_all.dart`, which imports all of the messages files:
```
flutter pub pub run intl_translation:generate_from_arb --output-dir=lib/i18n \
--generated-file-prefix=stock_ --no-use-deferred-loading lib/*.dart lib/i18n/stocks_*.arb
```
The `StockStrings` class uses the generated `initializeMessages()`
function (`stock_messages_all.dart`) to load the localized messages
and `Intl.message()` to look them up.
......@@ -11,7 +11,8 @@ import 'package:intl/src/intl_helpers.dart';
import 'stock_messages_en.dart' as messages_en;
import 'stock_messages_es.dart' as messages_es;
Map<String, Function> _deferredLibraries = {
typedef Future<dynamic> LibraryLoader();
Map<String, LibraryLoader> _deferredLibraries = {
'en': () => new Future.value(null),
'es': () => new Future.value(null),
};
......
......@@ -13,9 +13,7 @@ import 'package:flutter/rendering.dart' show
debugPaintLayerBordersEnabled,
debugPaintPointersEnabled,
debugRepaintRainbowEnabled;
import 'package:intl/intl.dart';
import 'i18n/stock_messages_all.dart';
import 'stock_data.dart';
import 'stock_home.dart';
import 'stock_settings.dart';
......@@ -23,6 +21,14 @@ import 'stock_strings.dart';
import 'stock_symbol_viewer.dart';
import 'stock_types.dart';
class _StocksLocalizationsDelegate extends LocalizationsDelegate<StockStrings> {
@override
Future<StockStrings> load(Locale locale) => StockStrings.load(locale);
@override
bool shouldReload(_StocksLocalizationsDelegate old) => false;
}
class StocksApp extends StatefulWidget {
@override
StocksAppState createState() => new StocksAppState();
......@@ -99,13 +105,6 @@ class StocksAppState extends State<StocksApp> {
return null;
}
Future<LocaleQueryData> _onLocaleChanged(Locale locale) async {
final String localeString = locale.toString();
await initializeMessages(localeString);
Intl.defaultLocale = localeString;
return StockStrings.instance;
}
@override
Widget build(BuildContext context) {
assert(() {
......@@ -119,6 +118,9 @@ class StocksAppState extends State<StocksApp> {
return new MaterialApp(
title: 'Stocks',
theme: theme,
localizationsDelegates: <_StocksLocalizationsDelegate>[
new _StocksLocalizationsDelegate(),
],
debugShowMaterialGrid: _configuration.debugShowGrid,
showPerformanceOverlay: _configuration.showPerformanceOverlay,
showSemanticsDebugger: _configuration.showSemanticsDebugger,
......@@ -127,7 +129,6 @@ class StocksAppState extends State<StocksApp> {
'/settings': (BuildContext context) => new StockSettings(_configuration, configurationUpdater)
},
onGenerateRoute: _getRoute,
onLocaleChanged: _onLocaleChanged
);
}
}
......
......@@ -2,39 +2,52 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:intl/intl.dart';
import 'package:flutter/widgets.dart';
// Wrappers for strings that are shown in the UI. The strings can be
// translated for different locales using the Dart intl package.
//
// Locale-specific values for the strings live in the i18n/*.arb files.
//
// To generate the stock_messages_*.dart files from the ARB files, run:
// pub run intl:generate_from_arb --output-dir=lib/i18n --generated-file-prefix=stock_ --no-use-deferred-loading lib/stock_strings.dart lib/i18n/stocks_*.arb
import 'i18n/stock_messages_all.dart';
class StockStrings extends LocaleQueryData {
static StockStrings of(BuildContext context) {
return LocaleQuery.of(context);
// Information about how this file relates to i18n/stock_messages_all.dart and how the i18n files
// were generated can be found in i18n/regenerate.md.
class StockStrings {
StockStrings(Locale locale) : _localeName = locale.toString();
final String _localeName;
static Future<StockStrings> load(Locale locale) {
return initializeMessages(locale.toString())
.then((Null _) {
return new StockStrings(locale);
});
}
static final StockStrings instance = new StockStrings();
static StockStrings of(BuildContext context) {
return Localizations.of<StockStrings>(context, StockStrings);
}
String title() => Intl.message(
'Stocks',
name: 'title',
desc: 'Title for the Stocks application'
);
String title() {
return Intl.message(
'<Stocks>',
name: 'title',
desc: 'Title for the Stocks application',
locale: _localeName,
);
}
String market() => Intl.message(
'MARKET',
name: 'market',
desc: 'Label for the Market tab'
desc: 'Label for the Market tab',
locale: _localeName,
);
String portfolio() => Intl.message(
'PORTFOLIO',
name: 'portfolio',
desc: 'Label for the Portfolio tab'
desc: 'Label for the Portfolio tab',
locale: _localeName,
);
}
......@@ -13,9 +13,19 @@ void main() {
stocks.main();
await tester.idle(); // see https://github.com/flutter/flutter/issues/1865
await tester.pump();
// The initial test app's locale is "_", so we're seeing the fallback translation here.
expect(find.text('MARKET'), findsOneWidget);
await tester.binding.setLocale('es', 'US');
await tester.idle();
// The Localizations widget has been built with the new locale. The
// new locale's strings are loaded asynchronously, so we're still
// displaying the previous locale's strings.
await tester.pump();
expect(find.text('MARKET'), findsOneWidget);
// The localized strings have finished loading and dependent
// widgets have been updated.
await tester.pump();
expect(find.text('MERCADO'), findsOneWidget);
});
......
......@@ -57,6 +57,7 @@ export 'src/material/ink_well.dart';
export 'src/material/input_decorator.dart';
export 'src/material/list_tile.dart';
export 'src/material/material.dart';
export 'src/material/material_localizations.dart';
export 'src/material/mergeable_material.dart';
export 'src/material/page.dart';
export 'src/material/paginated_data_table.dart';
......
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/rendering.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
......@@ -10,11 +12,10 @@ import 'arc.dart';
import 'colors.dart';
import 'floating_action_button.dart';
import 'icons.dart';
import 'material_localizations.dart';
import 'page.dart';
import 'theme.dart';
export 'dart:ui' show Locale;
const TextStyle _errorTextStyle = const TextStyle(
color: const Color(0xD0FF0000),
fontFamily: 'monospace',
......@@ -25,6 +26,16 @@ const TextStyle _errorTextStyle = const TextStyle(
decorationStyle: TextDecorationStyle.double
);
class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
const _MaterialLocalizationsDelegate();
@override
Future<MaterialLocalizations> load(Locale locale) => MaterialLocalizations.load(locale);
@override
bool shouldReload(_MaterialLocalizationsDelegate old) => false;
}
/// An application that uses material design.
///
/// A convenience widget that wraps a number of widgets that are commonly
......@@ -80,7 +91,8 @@ class MaterialApp extends StatefulWidget {
this.initialRoute,
this.onGenerateRoute,
this.onUnknownRoute,
this.onLocaleChanged,
this.locale,
this.localizationsDelegates,
this.navigatorObservers: const <NavigatorObserver>[],
this.debugShowMaterialGrid: false,
this.showPerformanceOverlay: false,
......@@ -129,10 +141,9 @@ class MaterialApp extends StatefulWidget {
/// normally, unless [initialRoute] is specified. It's also the route that's
/// displayed if the [initialRoute] can't be displayed.
///
/// To be able to directly call [Theme.of], [MediaQuery.of],
/// [LocaleQuery.of], etc, in the code sets the [home] argument in
/// the constructor, you can use a [Builder] widget to get a
/// [BuildContext].
/// To be able to directly call [Theme.of], [MediaQuery.of], etc, in the code
/// that sets the [home] argument in the constructor, you can use a [Builder]
/// widget to get a [BuildContext].
///
/// If [home] is specified, then [routes] must not include an entry for `/`,
/// as [home] takes its place.
......@@ -210,9 +221,16 @@ class MaterialApp extends StatefulWidget {
/// message.
final RouteFactory onUnknownRoute;
/// Callback that is called when the operating system changes the
/// current locale.
final LocaleChangedCallback onLocaleChanged;
/// The initial locale for this app's [Localizations] widget.
///
/// If the `locale` is null the system's locale value is used.
final Locale locale;
/// The delegates for this app's [Localizations] widget.
///
/// The delegates collectively define all of the localized resources
/// for this application's [Localizations] widget.
final Iterable<LocalizationsDelegate<dynamic>> localizationsDelegates;
/// Turns on a performance overlay.
///
......@@ -297,6 +315,14 @@ class _MaterialAppState extends State<MaterialApp> {
_heroController = new HeroController(createRectTween: _createRectTween);
}
// Combine the Localizations for Material with the ones contributed
// by the localizationsDelegates parameter, if any.
Iterable<LocalizationsDelegate<dynamic>> _createLocalizationsDelegates() sync* {
yield const _MaterialLocalizationsDelegate();
if (widget.localizationsDelegates != null)
yield* widget.localizationsDelegates;
}
RectTween _createRectTween(Rect begin, Rect end) {
return new MaterialRectArcTween(begin: begin, end: end);
}
......@@ -319,6 +345,7 @@ class _MaterialAppState extends State<MaterialApp> {
return null;
}
Route<dynamic> _onUnknownRoute(RouteSettings settings) {
assert(() {
if (widget.onUnknownRoute == null) {
......@@ -369,7 +396,8 @@ class _MaterialAppState extends State<MaterialApp> {
initialRoute: widget.initialRoute,
onGenerateRoute: _onGenerateRoute,
onUnknownRoute: _onUnknownRoute,
onLocaleChanged: widget.onLocaleChanged,
locale: widget.locale,
localizationsDelegates: _createLocalizationsDelegates(),
showPerformanceOverlay: widget.showPerformanceOverlay,
checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
......
......@@ -15,6 +15,7 @@ import 'flexible_space_bar.dart';
import 'icon_button.dart';
import 'icons.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'page.dart';
import 'scaffold.dart';
import 'tabs.dart';
......@@ -352,7 +353,7 @@ class _AppBarState extends State<AppBar> {
leading = new IconButton(
icon: const Icon(Icons.menu),
onPressed: _handleDrawerButton,
tooltip: 'Open navigation menu' // TODO(ianh): Figure out how to localize this string
tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
);
} else {
if (canPop)
......
......@@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart';
import 'icon_button.dart';
import 'icons.dart';
import 'material_localizations.dart';
import 'theme.dart';
/// A "back" icon that's appropriate for the current [TargetPlatform].
......@@ -83,7 +84,7 @@ class BackButton extends StatelessWidget {
return new IconButton(
icon: const BackButtonIcon(),
color: color,
tooltip: 'Back', // TODO(ianh): Figure out how to localize this string
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
onPressed: () {
Navigator.of(context).maybePop();
}
......@@ -115,7 +116,7 @@ class CloseButton extends StatelessWidget {
Widget build(BuildContext context) {
return new IconButton(
icon: const Icon(Icons.close),
tooltip: 'Close', // TODO(ianh): Figure out how to localize this string
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
onPressed: () {
Navigator.of(context).maybePop();
},
......
......@@ -23,6 +23,7 @@ import 'icon_button.dart';
import 'icons.dart';
import 'ink_well.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'theme.dart';
import 'typography.dart';
......@@ -548,7 +549,7 @@ class _MonthPickerState extends State<MonthPicker> {
left: 8.0,
child: new IconButton(
icon: const Icon(Icons.chevron_left),
tooltip: 'Previous month',
tooltip: MaterialLocalizations.of(context).previousMonthTooltip,
onPressed: _isDisplayingFirstMonth ? null : _handlePreviousMonth,
),
),
......@@ -557,7 +558,7 @@ class _MonthPickerState extends State<MonthPicker> {
right: 8.0,
child: new IconButton(
icon: const Icon(Icons.chevron_right),
tooltip: 'Next month',
tooltip: MaterialLocalizations.of(context).nextMonthTooltip,
onPressed: _isDisplayingLastMonth ? null : _handleNextMonth,
),
),
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
/// Default localized resource values for the material widgets.
///
/// This class is just a placeholder, it only provides English values.
class MaterialLocalizations {
const MaterialLocalizations._(this.locale) : assert(locale != null);
/// The locale for which the values of this class's localized resources
/// have been translated.
final Locale locale;
/// Creates an object that provides default localized resource values for the
/// for the widgets of the material library.
///
/// This method is typically used to create a [DefaultLocalizationsDelegate].
/// The [MaterialApp] does so by default.
static Future<MaterialLocalizations> load(Locale locale) {
return new SynchronousFuture<MaterialLocalizations>(new MaterialLocalizations._(locale));
}
/// The `MaterialLocalizations` from the closest [Localizations] instance
/// that encloses the given context.
///
/// This method is just a convenient shorthand for:
/// `Localizations.of<MaterialLocalizations>(context, MaterialLocalizations)`.
///
/// References to the localized resources defined by this class are typically
/// written in terms of this method. For example:
/// ```dart
/// tooltip: MaterialLocalizations.of(context).backButtonTooltip,
/// ```
static MaterialLocalizations of(BuildContext context) {
return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
}
/// The tooltip for the leading [AppBar] menu (aka 'hamburger') button
String get openAppDrawerTooltip => 'Open navigation menu';
/// The [BackButton]'s tooltip.
String get backButtonTooltip => 'Back';
/// The [CloseButton]'s tooltip.
String get closeButtonTooltip => 'Close';
/// The tooltip for the [MonthPicker]'s "next month" button.
String get nextMonthTooltip => 'Next month';
/// The tooltip for the [MonthPicker]'s "previous month" button.
String get previousMonthTooltip => 'Previous month';
}
......@@ -11,9 +11,8 @@ import 'package:flutter/rendering.dart';
import 'banner.dart';
import 'basic.dart';
import 'binding.dart';
import 'container.dart';
import 'framework.dart';
import 'locale_query.dart';
import 'localizations.dart';
import 'media_query.dart';
import 'navigator.dart';
import 'performance_overlay.dart';
......@@ -22,10 +21,7 @@ import 'text.dart';
import 'title.dart';
import 'widget_inspector.dart';
/// Signature for a function that is called when the operating system changes the current locale.
///
/// Used by [WidgetsApp.onLocaleChanged].
typedef Future<LocaleQueryData> LocaleChangedCallback(Locale locale);
export 'dart:ui' show Locale;
/// A convenience class that wraps a number of widgets that are commonly
/// required for an application.
......@@ -34,7 +30,7 @@ typedef Future<LocaleQueryData> LocaleChangedCallback(Locale locale);
/// back button to popping the [Navigator] or quitting the application.
///
/// See also: [CheckedModeBanner], [DefaultTextStyle], [MediaQuery],
/// [LocaleQuery], [Title], [Navigator], [Overlay], [SemanticsDebugger] (the
/// [Localizations], [Title], [Navigator], [Overlay], [SemanticsDebugger] (the
/// widgets wrapped by this one).
///
/// The [onGenerateRoute] argument is required, and corresponds to
......@@ -54,7 +50,8 @@ class WidgetsApp extends StatefulWidget {
@required this.color,
this.navigatorObservers: const <NavigatorObserver>[],
this.initialRoute,
this.onLocaleChanged,
this.locale,
this.localizationsDelegates,
this.showPerformanceOverlay: false,
this.checkerboardRasterCacheImages: false,
this.checkerboardOffscreenLayers: false,
......@@ -130,9 +127,16 @@ class WidgetsApp extends StatefulWidget {
/// * [Navigator.pop], for removing a route from the stack.
final String initialRoute;
/// Callback that is called when the operating system changes the
/// current locale.
final LocaleChangedCallback onLocaleChanged;
/// The initial locale for this app's [Localizations] widget.
///
/// If the 'locale' is null the system's locale value is used.
final Locale locale;
/// The delegates for this app's [Localizations] widget.
///
/// The delegates collectively define all of the localized resources
/// for this application's [Localizations] widget.
final Iterable<LocalizationsDelegate<dynamic>> localizationsDelegates;
/// Turns on a performance overlay.
/// https://flutter.io/debugging/#performanceoverlay
......@@ -214,13 +218,13 @@ class WidgetsApp extends StatefulWidget {
class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserver {
GlobalObjectKey<NavigatorState> _navigator;
LocaleQueryData _localeData;
Locale _locale;
@override
void initState() {
super.initState();
_navigator = new GlobalObjectKey<NavigatorState>(this);
didChangeLocale(ui.window.locale);
_locale = ui.window.locale;
WidgetsBinding.instance.addObserver(this);
}
......@@ -258,10 +262,9 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
@override
void didChangeLocale(Locale locale) {
if (widget.onLocaleChanged != null) {
widget.onLocaleChanged(locale).then<Null>((LocaleQueryData data) {
if (mounted)
setState(() { _localeData = data; });
if (locale != _locale) {
setState(() {
_locale = locale;
});
}
}
......@@ -274,19 +277,11 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
@override
Widget build(BuildContext context) {
if (widget.onLocaleChanged != null && _localeData == null) {
// If the app expects a locale but we don't yet know the locale, then
// don't build the widgets now.
// TODO(ianh): Make this unnecessary. See https://github.com/flutter/flutter/issues/1865
// TODO(ianh): The following line should not be included in release mode, only in profile and debug modes.
WidgetsBinding.instance.preventThisFrameFromBeingReportedAsFirstFrame();
return new Container();
}
Widget result = new MediaQuery(
data: new MediaQueryData.fromWindow(ui.window),
child: new LocaleQuery(
data: _localeData,
child: new Localizations(
locale: widget.locale ?? _locale,
delegates: widget.localizationsDelegates,
child: new Title(
title: widget.title,
color: widget.color,
......
......@@ -389,19 +389,35 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
}
bool _needToReportFirstFrame = true;
bool _thisFrameWasUseful = true;
int _deferFirstFrameReportCount = 0;
bool get _reportFirstFrame => _deferFirstFrameReportCount == 0;
/// Tell the framework that the frame we are currently building
/// should not be considered to be a useful first frame.
/// Tell the framework not to report the frame it is building as a "useful"
/// first frame until there is a corresponding call to [allowFirstFrameReport].
///
/// This is used by [WidgetsApp] to report the first frame.
//
// TODO(ianh): This method should only be available in debug and profile modes.
void preventThisFrameFromBeingReportedAsFirstFrame() {
_thisFrameWasUseful = false;
void deferFirstFrameReport() {
assert(_deferFirstFrameReportCount >= 0);
_deferFirstFrameReportCount += 1;
}
void _handleBuildScheduled() {
/// When called after [deferFirstFrameReport]: tell the framework to report
/// the frame it is building as a "useful" first frame.
///
/// This method may only be called once for each corresponding call
/// to [deferFirstFrameReport].
///
/// This is used by [WidgetsApp] to report the first frame.
//
// TODO(ianh): This method should only be available in debug and profile modes.
void allowFirstFrameReport() {
assert(_deferFirstFrameReportCount >= 1);
_deferFirstFrameReportCount -= 1;
}
void _handleBuildScheduled() {
// If we're in the process of building dirty elements, then changes
// should not trigger a new frame.
assert(() {
......@@ -522,14 +538,10 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
}
// TODO(ianh): Following code should not be included in release mode, only profile and debug modes.
// See https://github.com/dart-lang/sdk/issues/27192
if (_needToReportFirstFrame) {
if (_thisFrameWasUseful) {
developer.Timeline.instantSync('Widgets completed first useful frame');
developer.postEvent('Flutter.FirstFrame', <String, dynamic>{});
_needToReportFirstFrame = false;
} else {
_thisFrameWasUseful = true;
}
if (_needToReportFirstFrame && _reportFirstFrame) {
developer.Timeline.instantSync('Widgets completed first useful frame');
developer.postEvent('Flutter.FirstFrame', <String, dynamic>{});
_needToReportFirstFrame = false;
}
}
......@@ -556,11 +568,15 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
@override
Future<Null> performReassemble() {
_needToReportFirstFrame = true;
preventThisFrameFromBeingReportedAsFirstFrame();
deferFirstFrameReport();
if (renderViewElement != null)
buildOwner.reassemble(renderViewElement);
return super.performReassemble();
// TODO(hansmuller): eliminate the value variable after analyzer bug
// https://github.com/flutter/flutter/issues/11646 is fixed.
final Future<Null> value = super.performReassemble();
return value.then((Null _) {
allowFirstFrameReport();
});
}
}
......
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'framework.dart';
/// Superclass for locale-specific data provided by the application.
class LocaleQueryData { } // TODO(ianh): We need a better type here. This doesn't really make sense.
/// Establishes a subtree in which locale queries resolve to the given data.
class LocaleQuery extends InheritedWidget {
/// Creates a widget that provides [LocaleQueryData] to its descendants.
const LocaleQuery({
Key key,
@required this.data,
@required Widget child
}) : assert(child != null),
super(key: key, child: child);
/// The locale data for this subtree.
final LocaleQueryData data;
/// The data from the closest instance of this class that encloses the given context.
///
/// Typical usage is as follows:
///
/// ```dart
/// MyLocaleData data = LocaleQueryData.of(context);
/// ```
static LocaleQueryData of(BuildContext context) {
final LocaleQuery query = context.inheritFromWidgetOfExactType(LocaleQuery);
return query?.data;
}
@override
bool updateShouldNotify(LocaleQuery old) => data != old.data;
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DiagnosticsProperty<LocaleQueryData>('data', data, showName: false));
}
}
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui' show Locale;
import 'package:flutter/foundation.dart';
import 'binding.dart';
import 'container.dart';
import 'framework.dart';
// Examples can assume:
// class Intl { static String message(String s, { String name, String locale }) => ''; }
// Future<Null> initializeMessages(String locale) => null;
// A utility function used by Localizations to generate one future
// that completes when all of the LocalizationsDelegate.load() futures
// complete. The returned map is indexed by the type of each input
// future's value.
//
// The input future values must have distinct types.
//
// The returned Future<Map> will resolve when all of the input map's
// future values have resolved. If all of the input map's values are
// SynchronousFutures then a SynchronousFuture will be returned
// immediately.
//
// 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<Map<Type, dynamic>> _loadAll(Iterable<Future<dynamic>> inputValues) {
final Map<Type, dynamic> output = <Type, dynamic>{};
List<Future<dynamic>> outputFutures;
for (Future<dynamic> inputValue in inputValues) {
dynamic completedValue;
final Future<dynamic> futureValue = inputValue.then<dynamic>((dynamic value) {
return completedValue = value;
});
if (completedValue != null) { // inputValue was a SynchronousFuture
final Type type = completedValue.runtimeType;
assert(!output.containsKey(type));
output[type] = completedValue;
} else {
outputFutures ??= <Future<dynamic>>[];
outputFutures.add(futureValue);
}
}
// All of the input.values were synchronous futures, we're done.
if (outputFutures == null)
return new SynchronousFuture<Map<Type, dynamic>>(output);
// Some of input.values were asynchronous futures. Wait for them.
return Future.wait<dynamic>(outputFutures).then<Map<Type, dynamic>>((List<dynamic> values) {
for (dynamic value in values) {
final Type type = value.runtimeType;
assert(!output.containsKey(type));
output[type] = value;
}
return output;
});
}
/// A factory for a set of localized resources of type `T`, to be loaded by a
/// [Localizations] widget.
///
/// Typical applications have one [Localizations] widget which is
/// created by the [WidgetsApp] and configured with the app's
/// `localizationsDelegates` parameter.
abstract class LocalizationsDelegate<T> {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const LocalizationsDelegate();
/// Start loading the resources for `locale`. The returned future completes
/// when the resources have finished loading.
///
/// It's assumed that the this method will return an object that contains
/// a collection of related resources (typically defined with one method per
/// resource). The object will be retrieved with [Localizations.of].
Future<T> load(Locale locale);
/// Returns true if the resources for this delegate should be loaded
/// again by calling the [load] method.
///
/// This method is called whenever its [Localizations] widget is
/// rebuilt. If it returns true then dependent widgets will be rebuilt
/// after [load] has completed.
bool shouldReload(covariant LocalizationsDelegate<T> old);
}
class _LocalizationsScope extends InheritedWidget {
_LocalizationsScope ({
Key key,
@required this.locale,
@required this.localizationsState,
Widget child,
}) : super(key: key, child: child) {
assert(localizationsState != null);
}
final Locale locale;
final _LocalizationsState localizationsState;
@override
bool updateShouldNotify(_LocalizationsScope old) {
// Changes in Localizations.locale trigger a load(), see _LocalizationsState.didUpdateWidget()
return false;
}
}
/// Defines the [Locale] for its `child` and the localized resources that the
/// child depends on.
///
/// Localized resources are loaded by the list of [LocalizationsDelegate]
/// `delegates`. Each delegate is essentially a factory for a collection
/// of localized resources. There are multiple delegates because there are
/// multiple sources for localizations within an app.
///
/// Delegates are typically simple subclasses of [LocalizationsDelegate] that
/// override [LocalizationsDelegate.load]. For example a delegate for the
/// `MyLocalizations` class defined below would be:
///
/// ```dart
/// class _MyDelegate extends LocalizationsDelegate<MyLocalizations> {
/// @override
/// Future<MyLocalizations> load(Locale locale) => MyLocalizations.load(locale);
///}
/// ```
///
/// Each delegate can be viewed as a factory for objects that encapsulate a
/// a set of localized resources. These objects are retrieved with
/// by runtime type with [Localizations.of].
///
/// The [WidgetsApp] class creates a `Localizations` widget so most apps
/// will not need to create one. The widget app's `Localizations` delegates can
/// be initialized with [WidgetsApp.localizationsDelegates]. The [MaterialApp]
/// class also provides a `localizationsDelegates` parameter that's just
/// passed along to the [WidgetsApp].
///
/// Apps should retrieve collections of localized resources with
/// `Localizations.of<MyLocalizations>(context, MyLocalizations)`,
/// where MyLocalizations is an app specific class defines one function per
/// resource. This is conventionally done by a static `.of` method on the
/// MyLocalizations class.
///
/// For example, using the `MyLocalizations` class defined below, one would
/// lookup a localized title string like this:
/// ```dart
/// MyLocalizations.of(context).title()
/// ```
/// If `Localizations` were to be rebuilt with a new `locale` then
/// the widget subtree that corresponds to [BuildContext] `context` would
/// be rebuilt after the corresponding resources had been loaded.
///
/// This class is effectively an [InheritedWidget]. If it's rebuilt with
/// a new `locale` or a different list of delegates or any of its
/// delegates' [LocalizationDelegate.shouldReload()] methods returns true,
/// then widgets that have created a dependency by calling
/// `Localizations.of(context)` will be rebuilt after the resources
/// for the new locale have been loaded.
///
/// ## Sample code
///
/// This following class is defined in terms of the
/// [Dart `intl` package](https://github.com/dart-lang/intl). Using the `intl`
/// package isn't required.
///
/// ```dart
/// class MyLocalizations {
/// MyLocalizations(this.locale);
///
/// final Locale locale;
///
/// static Future<MyLocalizations> load(Locale locale) {
/// return initializeMessages(locale.toString())
/// .then((Null _) {
/// return new MyLocalizations(locale);
/// });
/// }
///
/// static MyLocalizations of(BuildContext context) {
/// return Localizations.of<MyLocalizations>(context, MyLocalizations);
/// }
///
/// String title() => Intl.message('<title>', name: 'title', locale: locale.toString());
/// // ... more Intl.message() methods like title()
/// }
/// ```
/// A class based on the `intl` package imports a generated message catalog that provides
/// the `initializeMessages()` function and the per-locale backing store for `Intl.message()`.
/// The message catalog is produced by an `intl` tool that analyzes the source code for
/// classes that contain `Intl.message()` calls. In this case that would just be the
/// `MyLocalizations` class.
///
/// 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 {
Localizations({
Key key,
@required this.locale,
this.delegates,
this.child
}) : super(key: key) {
assert(locale != null);
}
/// The resources returned by [Localizations.of] will be specific to this locale.
final Locale locale;
/// This list collectively defines the localized resources objects that can
/// be retrieved with [ Localizations.of].
final Iterable<LocalizationsDelegate<dynamic>> delegates;
/// The widget below this widget in the tree.
final Widget child;
/// The locale of the Localizations widget for the widget tree that
/// corresponds to [BuildContext] `context`.
static Locale localeOf(BuildContext context) {
assert(context != null);
final _LocalizationsScope scope = context.inheritFromWidgetOfExactType(_LocalizationsScope);
return scope.localizationsState.locale;
}
/// Returns the 'type' localized resources for the widget tree that
/// corresponds to [BuildContext] `context`.
///
/// This method is typically used by a static factory method on the 'type'
/// class. For example Flutter's MaterialLocalizations class looks up Material
/// resources with a method defined like this:
///
/// ```dart
/// static MaterialLocalizations of(BuildContext context) {
/// return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
/// }
/// ```
static T of<T>(BuildContext context, Type type) {
assert(context != null);
assert(type != null);
final _LocalizationsScope scope = context.inheritFromWidgetOfExactType(_LocalizationsScope);
return scope.localizationsState.resourcesFor<T>(type);
}
@override
_LocalizationsState createState() => new _LocalizationsState();
}
class _LocalizationsState extends State<Localizations> {
final GlobalKey _localizedResourcesScopeKey = new GlobalKey();
Map<Type, dynamic> _typeToResources = <Type, dynamic>{};
Locale get locale => _locale;
Locale _locale;
@override
void initState() {
super.initState();
load(widget.locale);
}
bool _anyDelegatesShouldReload(Localizations old) {
if (widget.delegates.length != old.delegates.length)
return true;
final List<LocalizationsDelegate<dynamic>> delegates = widget.delegates.toList();
final List<LocalizationsDelegate<dynamic>> oldDelegates = old.delegates.toList();
for (int i = 0; i < delegates.length; i += 1) {
final LocalizationsDelegate<dynamic> delegate = delegates[i];
final LocalizationsDelegate<dynamic> oldDelegate = oldDelegates[i];
if (delegate.runtimeType != oldDelegate.runtimeType || delegate.shouldReload(oldDelegate))
return true;
}
return false;
}
@override
void didUpdateWidget(Localizations old) {
super.didUpdateWidget(old);
if (widget.locale != old.locale
|| (widget.delegates == null && old.delegates != null)
|| (widget.delegates != null && old.delegates == null)
|| (widget.delegates != null && _anyDelegatesShouldReload(old)))
load(widget.locale);
}
void load(Locale locale) {
final Iterable<LocalizationsDelegate<dynamic>> delegates = widget.delegates;
if (delegates == null || delegates.isEmpty) {
_locale = locale;
return;
}
final Iterable<Future<dynamic>> allResources = delegates.map((LocalizationsDelegate<dynamic> delegate) {
return delegate.load(locale);
});
Map<Type, dynamic> typeToResources;
final Future<Map<Type, dynamic>> typeToResourcesFuture = _loadAll(allResources)
.then((Map<Type, dynamic> value) {
return typeToResources = value;
});
if (typeToResources != null) {
// All of the delegates' resources loaded synchronously.
_typeToResources = typeToResources;
_locale = locale;
} else {
// - Don't rebuild the dependent widgets until the resources for the new locale
// have finished loading. Until then the old locale will continue to be used.
// - If we're running at app startup time then defer reporting the first
// "useful" frame until after the async load has completed.
WidgetsBinding.instance.deferFirstFrameReport();
typeToResourcesFuture.then((Map<Type, dynamic> value) {
WidgetsBinding.instance.allowFirstFrameReport();
if (!mounted)
return;
setState(() {
_typeToResources = value;
_locale = locale;
});
final InheritedElement scopeElement = _localizedResourcesScopeKey.currentContext;
scopeElement?.dispatchDidChangeDependencies();
});
}
}
T resourcesFor<T>(Type type) {
assert(type != null);
final dynamic resources = _typeToResources[type];
assert(resources.runtimeType == type);
return resources;
}
@override
Widget build(BuildContext context) {
return new _LocalizationsScope(
key: _localizedResourcesScopeKey,
locale: widget.locale,
localizationsState: this,
child: _locale != null ? widget.child : new Container(),
);
}
}
......@@ -45,7 +45,7 @@ export 'src/widgets/image.dart';
export 'src/widgets/image_icon.dart';
export 'src/widgets/implicit_animations.dart';
export 'src/widgets/layout_builder.dart';
export 'src/widgets/locale_query.dart';
export 'src/widgets/localizations.dart';
export 'src/widgets/media_query.dart';
export 'src/widgets/modal_barrier.dart';
export 'src/widgets/navigation_toolbar.dart';
......
......@@ -28,94 +28,88 @@ void main() {
DateTime _selectedDate = new DateTime(2016, DateTime.JULY, 26);
await tester.pumpWidget(
new Overlay(
initialEntries: <OverlayEntry>[
new OverlayEntry(
builder: (BuildContext context) => new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return new Positioned(
width: 400.0,
child: new SingleChildScrollView(
child: new Material(
child: new MonthPicker(
firstDate: new DateTime(0),
lastDate: new DateTime(9999),
key: _datePickerKey,
selectedDate: _selectedDate,
onChanged: (DateTime value) {
setState(() {
_selectedDate = value;
});
},
),
),
new MaterialApp(
home: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return new Container(
width: 400.0,
child: new SingleChildScrollView(
child: new Material(
child: new MonthPicker(
firstDate: new DateTime(0),
lastDate: new DateTime(9999),
key: _datePickerKey,
selectedDate: _selectedDate,
onChanged: (DateTime value) {
setState(() {
_selectedDate = value;
});
},
),
);
},
),
),
],
),
),
),
);
},
),
)
);
expect(_selectedDate, equals(new DateTime(2016, DateTime.JULY, 26)));
await tester.tapAt(const Offset(50.0, 100.0));
expect(_selectedDate, equals(new DateTime(2016, DateTime.JULY, 26)));
await tester.pumpAndSettle();
await tester.pump(const Duration(seconds: 2));
await tester.tapAt(const Offset(300.0, 100.0));
await tester.tap(find.text('1'));
await tester.pumpAndSettle();
expect(_selectedDate, equals(new DateTime(2016, DateTime.JULY, 1)));
await tester.pump(const Duration(seconds: 2));
await tester.tapAt(const Offset(380.0, 20.0));
await tester.pumpAndSettle(const Duration(milliseconds: 100));
await tester.tap(find.byTooltip('Next month'));
await tester.pumpAndSettle();
expect(_selectedDate, equals(new DateTime(2016, DateTime.JULY, 1)));
await tester.tapAt(const Offset(300.0, 100.0));
await tester.tap(find.text('5'));
await tester.pumpAndSettle();
expect(_selectedDate, equals(new DateTime(2016, DateTime.AUGUST, 5)));
await tester.pump(const Duration(seconds: 2));
await tester.drag(find.byKey(_datePickerKey), const Offset(-300.0, 0.0));
await tester.pumpAndSettle(const Duration(milliseconds: 100));
await tester.drag(find.byKey(_datePickerKey), const Offset(-400.0, 0.0));
await tester.pumpAndSettle();
expect(_selectedDate, equals(new DateTime(2016, DateTime.AUGUST, 5)));
await tester.tapAt(const Offset(45.0, 270.0));
await tester.tap(find.text('25'));
await tester.pumpAndSettle();
expect(_selectedDate, equals(new DateTime(2016, DateTime.SEPTEMBER, 25)));
await tester.pump(const Duration(seconds: 2));
await tester.drag(find.byKey(_datePickerKey), const Offset(300.0, 0.0));
await tester.pumpAndSettle(const Duration(milliseconds: 100));
await tester.drag(find.byKey(_datePickerKey), const Offset(800.0, 0.0));
await tester.pumpAndSettle();
expect(_selectedDate, equals(new DateTime(2016, DateTime.SEPTEMBER, 25)));
await tester.tapAt(const Offset(210.0, 180.0));
await tester.tap(find.text('17'));
await tester.pumpAndSettle();
expect(_selectedDate, equals(new DateTime(2016, DateTime.AUGUST, 17)));
});
testWidgets('render picker with intrinsic dimensions', (WidgetTester tester) async {
await tester.pumpWidget(
new Overlay(
initialEntries: <OverlayEntry>[
new OverlayEntry(
builder: (BuildContext context) => new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return new IntrinsicWidth(
child: new IntrinsicHeight(
child: new Material(
child: new SingleChildScrollView(
child: new MonthPicker(
firstDate: new DateTime(0),
lastDate: new DateTime(9999),
onChanged: (DateTime value) { },
selectedDate: new DateTime(2000, DateTime.JANUARY, 1),
),
),
new MaterialApp(
home: new StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return new IntrinsicWidth(
child: new IntrinsicHeight(
child: new Material(
child: new SingleChildScrollView(
child: new MonthPicker(
firstDate: new DateTime(0),
lastDate: new DateTime(9999),
onChanged: (DateTime value) { },
selectedDate: new DateTime(2000, DateTime.JANUARY, 1),
),
),
);
},
),
),
],
),
),
);
},
),
),
);
await tester.pump(const Duration(seconds: 5));
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
class TestLocaleQueryData extends LocaleQueryData {
@override
String toString() => 'Test data';
}
void main() {
testWidgets('LocaleQuery control test', (WidgetTester tester) async {
await tester.pumpWidget(new Container());
expect(LocaleQuery.of(tester.element(find.byType(Container))), isNull);
final LocaleQueryData data = new TestLocaleQueryData();
final Widget widget = new LocaleQuery(
data: data,
child: new Container(),
);
expect(widget, hasOneLineDescription);
expect(widget.toString(), contains('Test data'));
await tester.pumpWidget(widget);
expect(LocaleQuery.of(tester.element(find.byType(Container))), equals(data));
});
}
// Copyright 2017 The Chromium Authors. All rights reserved.rint
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
class TestLocalizations {
TestLocalizations(this.locale, this.prefix);
final Locale locale;
final String prefix;
static Future<TestLocalizations> loadSync(Locale locale, String prefix) {
return new SynchronousFuture<TestLocalizations>(new TestLocalizations(locale, prefix));
}
static Future<TestLocalizations> loadAsync(Locale locale, String prefix) {
return new Future<TestLocalizations>.delayed(const Duration(milliseconds: 100))
.then((_) => new TestLocalizations(locale, prefix));
}
static TestLocalizations of(BuildContext context) {
return Localizations.of<TestLocalizations>(context, TestLocalizations);
}
String get message => '${prefix ?? ""}$locale';
}
class SyncTestLocalizationsDelegate extends LocalizationsDelegate<TestLocalizations> {
SyncTestLocalizationsDelegate([this.prefix]);
final String prefix; // Changing this value triggers a rebuild
final List<bool> shouldReloadValues = <bool>[];
@override
Future<TestLocalizations> load(Locale locale) => TestLocalizations.loadSync(locale, prefix);
@override
bool shouldReload(SyncTestLocalizationsDelegate old) {
shouldReloadValues.add(prefix != old.prefix);
return prefix != old.prefix;
}
}
class AsyncTestLocalizationsDelegate extends LocalizationsDelegate<TestLocalizations> {
AsyncTestLocalizationsDelegate([this.prefix]);
final String prefix; // Changing this value triggers a rebuild
final List<bool> shouldReloadValues = <bool>[];
@override
Future<TestLocalizations> load(Locale locale) => TestLocalizations.loadAsync(locale, prefix);
@override
bool shouldReload(AsyncTestLocalizationsDelegate old) {
shouldReloadValues.add(prefix != old.prefix);
return prefix != old.prefix;
}
}
class MoreLocalizations {
MoreLocalizations(this.locale);
final Locale locale;
static Future<MoreLocalizations> loadSync(Locale locale) {
return new SynchronousFuture<MoreLocalizations>(new MoreLocalizations(locale));
}
static Future<MoreLocalizations> loadAsync(Locale locale) {
return new Future<MoreLocalizations>.delayed(const Duration(milliseconds: 100))
.then((_) => new MoreLocalizations(locale));
}
static MoreLocalizations of(BuildContext context) {
return Localizations.of<MoreLocalizations>(context, MoreLocalizations);
}
String get message => '$locale';
}
class SyncMoreLocalizationsDelegate extends LocalizationsDelegate<MoreLocalizations> {
@override
Future<MoreLocalizations> load(Locale locale) => MoreLocalizations.loadSync(locale);
@override
bool shouldReload(SyncMoreLocalizationsDelegate old) => false;
}
class AsyncMoreLocalizationsDelegate extends LocalizationsDelegate<MoreLocalizations> {
@override
Future<MoreLocalizations> load(Locale locale) => MoreLocalizations.loadAsync(locale);
@override
bool shouldReload(AsyncMoreLocalizationsDelegate old) => false;
}
Widget buildFrame({
Locale locale,
Iterable<LocalizationsDelegate<dynamic>> delegates,
WidgetBuilder buildContent,
}) {
return new WidgetsApp(
color: const Color(0xFFFFFFFF),
locale: locale,
localizationsDelegates: delegates,
onGenerateRoute: (RouteSettings settings) {
return new PageRouteBuilder<Null>(
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) {
return buildContent(context);
}
);
},
);
}
void main() {
testWidgets('Localizations.localeFor in a WidgetsApp with system locale', (WidgetTester tester) async {
BuildContext pageContext;
await tester.pumpWidget(
buildFrame(
buildContent: (BuildContext context) {
pageContext = context;
return new Text('Hello World');
}
)
);
await tester.binding.setLocale('en', 'GB');
await tester.pump();
expect(Localizations.localeOf(pageContext), const Locale('en', 'GB'));
await tester.binding.setLocale('en', 'US');
await tester.pump();
expect(Localizations.localeOf(pageContext), const Locale('en', 'US'));
});
testWidgets('Localizations.localeFor in a WidgetsApp with an explicit locale', (WidgetTester tester) async {
final Locale locale = const Locale('en', 'US');
BuildContext pageContext;
await tester.pumpWidget(
buildFrame(
locale: locale,
buildContent: (BuildContext context) {
pageContext = context;
return new Text('Hello World');
},
)
);
expect(Localizations.localeOf(pageContext), locale);
await tester.binding.setLocale('en', 'GB');
await tester.pump();
// The WidgetApp's explicit locale overrides the system's locale.
expect(Localizations.localeOf(pageContext), locale);
});
testWidgets('Synchronously loaded localizations in a WidgetsApp', (WidgetTester tester) async {
BuildContext pageContext;
await tester.pumpWidget(
buildFrame(
delegates: <LocalizationsDelegate<dynamic>>[
new SyncTestLocalizationsDelegate()
],
buildContent: (BuildContext context) {
pageContext = context;
return new Text(TestLocalizations.of(context).message);
}
)
);
expect(TestLocalizations.of(pageContext), isNotNull);
expect(find.text('_'), findsOneWidget); // default test locale is '_'
await tester.binding.setLocale('en', 'GB');
await tester.pump();
expect(find.text('en_GB'), findsOneWidget);
await tester.binding.setLocale('en', 'US');
await tester.pump();
expect(find.text('en_US'), findsOneWidget);
});
testWidgets('Asynchronously loaded localizations in a WidgetsApp', (WidgetTester tester) async {
await tester.pumpWidget(
buildFrame(
delegates: <LocalizationsDelegate<dynamic>>[
new AsyncTestLocalizationsDelegate(),
],
buildContent: (BuildContext context) {
return new Text(TestLocalizations.of(context).message);
}
)
);
await tester.pump(const Duration(milliseconds: 50)); // TestLocalizations.loadAsync() takes 100ms
expect(find.text('_'), 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);
await tester.binding.setLocale('en', 'GB');
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);
await tester.pump(const Duration(milliseconds: 50)); // finish the async load
await tester.pumpAndSettle();
expect(find.text('en_GB'), findsOneWidget);
});
testWidgets('Localizations with multiple sync delegates', (WidgetTester tester) async {
await tester.pumpWidget(
buildFrame(
delegates: <LocalizationsDelegate<dynamic>>[
new SyncTestLocalizationsDelegate(),
new SyncMoreLocalizationsDelegate(),
],
locale: const Locale('en', 'US'),
buildContent: (BuildContext context) {
return new Column(
children: <Widget>[
new Text('A: ${TestLocalizations.of(context).message}'),
new Text('B: ${MoreLocalizations.of(context).message}'),
],
);
}
)
);
// All localizations were loaded synchonously
expect(find.text('A: en_US'), findsOneWidget);
expect(find.text('B: en_US'), findsOneWidget);
});
testWidgets('Localizations with multiple delegates', (WidgetTester tester) async {
await tester.pumpWidget(
buildFrame(
delegates: <LocalizationsDelegate<dynamic>>[
new SyncTestLocalizationsDelegate(),
new AsyncMoreLocalizationsDelegate(), // No resources until this completes
],
locale: const Locale('en', 'US'),
buildContent: (BuildContext context) {
return new Column(
children: <Widget>[
new Text('A: ${TestLocalizations.of(context).message}'),
new Text('B: ${MoreLocalizations.of(context).message}'),
],
);
}
)
);
await tester.pump(const Duration(milliseconds: 50));
expect(find.text('A: en_US'), findsNothing); // MoreLocalizations.load() hasn't completed yet
expect(find.text('B: en_US'), findsNothing);
await tester.pump(const Duration(milliseconds: 50));
await tester.pumpAndSettle();
expect(find.text('A: en_US'), findsOneWidget);
expect(find.text('B: en_US'), findsOneWidget);
});
testWidgets('Muliple Localizations', (WidgetTester tester) async {
await tester.pumpWidget(
buildFrame(
delegates: <LocalizationsDelegate<dynamic>>[
new SyncTestLocalizationsDelegate(),
],
locale: const Locale('en', 'US'),
buildContent: (BuildContext context) {
return new Column(
children: <Widget>[
new Text('A: ${TestLocalizations.of(context).message}'),
new Localizations(
locale: const Locale('en', 'GB'),
delegates: <LocalizationsDelegate<dynamic>>[
new SyncTestLocalizationsDelegate(),
],
// Create a new context within the en_GB Localization
child: new Builder(
builder: (BuildContext context) {
return new Text('B: ${TestLocalizations.of(context).message}');
},
),
),
],
);
}
)
);
expect(find.text('A: en_US'), findsOneWidget);
expect(find.text('B: en_GB'), findsOneWidget);
});
// If both the locale and the length and type of a Localizations delegate list
// stays the same BUT one of its delegate.shouldReload() methods returns true,
// then the dependent widgets should rebuild.
testWidgets('Localizations sync delegate shouldReload returns true', (WidgetTester tester) async {
final SyncTestLocalizationsDelegate originalDelegate = new SyncTestLocalizationsDelegate();
await tester.pumpWidget(
buildFrame(
delegates: <LocalizationsDelegate<dynamic>>[
originalDelegate,
new SyncMoreLocalizationsDelegate(),
],
locale: const Locale('en', 'US'),
buildContent: (BuildContext context) {
return new Column(
children: <Widget>[
new Text('A: ${TestLocalizations.of(context).message}'),
new Text('B: ${MoreLocalizations.of(context).message}'),
],
);
}
)
);
await tester.pumpAndSettle();
expect(find.text('A: en_US'), findsOneWidget);
expect(find.text('B: en_US'), findsOneWidget);
expect(originalDelegate.shouldReloadValues, <bool>[]);
final SyncTestLocalizationsDelegate modifiedDelegate = new SyncTestLocalizationsDelegate('---');
await tester.pumpWidget(
buildFrame(
delegates: <LocalizationsDelegate<dynamic>>[
modifiedDelegate,
new SyncMoreLocalizationsDelegate(),
],
locale: const Locale('en', 'US'),
buildContent: (BuildContext context) {
return new Column(
children: <Widget>[
new Text('A: ${TestLocalizations.of(context).message}'),
new Text('B: ${MoreLocalizations.of(context).message}'),
],
);
}
)
);
await tester.pumpAndSettle();
expect(find.text('A: ---en_US'), findsOneWidget);
expect(find.text('B: en_US'), findsOneWidget);
expect(modifiedDelegate.shouldReloadValues, <bool>[true]);
});
testWidgets('Localizations async delegate shouldReload returns true', (WidgetTester tester) async {
await tester.pumpWidget(
buildFrame(
delegates: <LocalizationsDelegate<dynamic>>[
new AsyncTestLocalizationsDelegate(),
new AsyncMoreLocalizationsDelegate(),
],
locale: const Locale('en', 'US'),
buildContent: (BuildContext context) {
return new Column(
children: <Widget>[
new Text('A: ${TestLocalizations.of(context).message}'),
new Text('B: ${MoreLocalizations.of(context).message}'),
],
);
}
)
);
await tester.pumpAndSettle();
expect(find.text('A: en_US'), findsOneWidget);
expect(find.text('B: en_US'), findsOneWidget);
final AsyncTestLocalizationsDelegate modifiedDelegate = new AsyncTestLocalizationsDelegate('---');
await tester.pumpWidget(
buildFrame(
delegates: <LocalizationsDelegate<dynamic>>[
modifiedDelegate,
new AsyncMoreLocalizationsDelegate(),
],
locale: const Locale('en', 'US'),
buildContent: (BuildContext context) {
return new Column(
children: <Widget>[
new Text('A: ${TestLocalizations.of(context).message}'),
new Text('B: ${MoreLocalizations.of(context).message}'),
],
);
}
)
);
await tester.pumpAndSettle();
expect(find.text('A: ---en_US'), findsOneWidget);
expect(find.text('B: en_US'), findsOneWidget);
expect(modifiedDelegate.shouldReloadValues, <bool>[true]);
});
}
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