Updated I18N API for Flutter (#11497)
This commit is contained in:
parent
7676e5119e
commit
8c2c502257
@ -1,10 +1,33 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
|
@ -0,0 +1,58 @@
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,45 +0,0 @@
|
||||
// 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));
|
||||
}
|
||||
}
|
346
packages/flutter/lib/src/widgets/localizations.dart
Normal file
346
packages/flutter/lib/src/widgets/localizations.dart
Normal file
@ -0,0 +1,346 @@
|
||||
// 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));
|
||||
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.text('1'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(_selectedDate, equals(new DateTime(2016, DateTime.JULY, 1)));
|
||||
|
||||
await tester.tapAt(const Offset(300.0, 100.0));
|
||||
expect(_selectedDate, equals(new DateTime(2016, DateTime.AUGUST, 5)));
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
await tester.tap(find.byTooltip('Next month'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(_selectedDate, equals(new DateTime(2016, DateTime.JULY, 1)));
|
||||
|
||||
await tester.drag(find.byKey(_datePickerKey), const Offset(-300.0, 0.0));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 100));
|
||||
await tester.tap(find.text('5'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(_selectedDate, equals(new DateTime(2016, DateTime.AUGUST, 5)));
|
||||
|
||||
await tester.tapAt(const Offset(45.0, 270.0));
|
||||
expect(_selectedDate, equals(new DateTime(2016, DateTime.SEPTEMBER, 25)));
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
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.drag(find.byKey(_datePickerKey), const Offset(300.0, 0.0));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 100));
|
||||
await tester.tap(find.text('25'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(_selectedDate, equals(new DateTime(2016, DateTime.SEPTEMBER, 25)));
|
||||
|
||||
await tester.tapAt(const Offset(210.0, 180.0));
|
||||
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.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));
|
||||
|
@ -1,32 +0,0 @@
|
||||
// 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));
|
||||
});
|
||||
}
|
412
packages/flutter/test/widgets/localizations_test.dart
Normal file
412
packages/flutter/test/widgets/localizations_test.dart
Normal file
@ -0,0 +1,412 @@
|
||||
// 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]);
|
||||
});
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user