| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /// @docImport 'package:flutter/material.dart'; |
| /// @docImport 'package:flutter_localizations/flutter_localizations.dart'; |
| /// |
| /// @docImport 'app.dart'; |
| /// @docImport 'reorderable_list.dart'; |
| library; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/rendering.dart'; |
| |
| import 'app.dart'; |
| import 'basic.dart'; |
| import 'binding.dart'; |
| import 'debug.dart'; |
| import 'framework.dart'; |
| |
| // Examples can assume: |
| // class Intl { Intl._(); static String message(String s, { String? name, String? locale }) => ''; } |
| // Future<void> initializeMessages(String locale) => Future<void>.value(); |
| // late BuildContext context; |
| // class Foo { } |
| // const Widget myWidget = Placeholder(); |
| |
| // Used by loadAll() to record LocalizationsDelegate.load() futures we're |
| // waiting for. |
| class _Pending { |
| _Pending(this.delegate, this.futureValue); |
| final LocalizationsDelegate<dynamic> delegate; |
| final Future<dynamic> futureValue; |
| } |
| |
| // 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 each delegate's type. |
| // |
| // 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( |
| Locale locale, |
| Iterable<LocalizationsDelegate<dynamic>> allDelegates, |
| ) { |
| final output = <Type, dynamic>{}; |
| List<_Pending>? pendingList; |
| |
| // Only load the first delegate for each delegate type that supports |
| // locale.languageCode. |
| final types = <Type>{}; |
| final delegates = <LocalizationsDelegate<dynamic>>[]; |
| for (final delegate in allDelegates) { |
| if (!types.contains(delegate.type) && delegate.isSupported(locale)) { |
| types.add(delegate.type); |
| delegates.add(delegate); |
| } |
| } |
| |
| for (final delegate in delegates) { |
| final Future<dynamic> inputValue = delegate.load(locale); |
| dynamic completedValue; |
| final Future<dynamic> futureValue = inputValue.then<dynamic>((dynamic value) { |
| return completedValue = value; |
| }); |
| if (completedValue != null) { |
| // inputValue was a SynchronousFuture |
| final Type type = delegate.type; |
| assert(!output.containsKey(type)); |
| output[type] = completedValue; |
| } else { |
| pendingList ??= <_Pending>[]; |
| pendingList.add(_Pending(delegate, futureValue)); |
| } |
| } |
| |
| // All of the delegate.load() values were synchronous futures, we're done. |
| if (pendingList == null) { |
| return SynchronousFuture<Map<Type, dynamic>>(output); |
| } |
| |
| // Some of delegate.load() values were asynchronous futures. Wait for them. |
| return Future.wait<dynamic>( |
| pendingList.map<Future<dynamic>>((_Pending p) => p.futureValue), |
| ).then<Map<Type, dynamic>>((List<dynamic> values) { |
| assert(values.length == pendingList!.length); |
| for (var i = 0; i < values.length; i += 1) { |
| final Type type = pendingList![i].delegate.type; |
| assert(!output.containsKey(type)); |
| output[type] = values[i]; |
| } |
| 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 (a list of delegates). The delegate's [type] is used to identify |
| /// the object created by an individual delegate's [load] method. |
| /// |
| /// An example of a class used as the value of `T` here would be |
| /// [MaterialLocalizations]. |
| 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(); |
| |
| /// Whether resources for the given locale can be loaded by this delegate. |
| /// |
| /// Return true if the instance of `T` loaded by this delegate's [load] |
| /// method supports the given `locale`'s language. |
| bool isSupported(Locale locale); |
| |
| /// Start loading the resources for `locale`. The returned future completes |
| /// when the resources have finished loading. |
| /// |
| /// It's assumed that this method will return an object that contains a |
| /// collection of related string 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); |
| |
| /// The type of the object returned by the [load] method, T by default. |
| /// |
| /// This type is used to retrieve the object "loaded" by this |
| /// [LocalizationsDelegate] from the [Localizations] inherited widget. |
| /// For example the object loaded by `LocalizationsDelegate<Foo>` would |
| /// be retrieved with: |
| /// |
| /// ```dart |
| /// Foo foo = Localizations.of<Foo>(context, Foo)!; |
| /// ``` |
| /// |
| /// It's rarely necessary to override this getter. |
| Type get type => T; |
| |
| @override |
| String toString() => '${objectRuntimeType(this, 'LocalizationsDelegate')}[$type]'; |
| } |
| |
| /// Interface for localized resource values for the lowest levels of the Flutter |
| /// framework. |
| /// |
| /// This class also maps locales to a specific [Directionality] using the |
| /// [textDirection] property. |
| /// |
| /// See also: |
| /// |
| /// * [DefaultWidgetsLocalizations], which implements this interface and |
| /// supports a variety of locales. |
| abstract class WidgetsLocalizations { |
| /// The reading direction for text in this locale. |
| TextDirection get textDirection; |
| |
| /// The semantics label used for [SliverReorderableList] to reorder an item in the |
| /// list to the start of the list. |
| String get reorderItemToStart; |
| |
| /// The semantics label used for [SliverReorderableList] to reorder an item in the |
| /// list to the end of the list. |
| String get reorderItemToEnd; |
| |
| /// The semantics label used for [SliverReorderableList] to reorder an item in the |
| /// list one space up the list. |
| String get reorderItemUp; |
| |
| /// The semantics label used for [SliverReorderableList] to reorder an item in the |
| /// list one space down the list. |
| String get reorderItemDown; |
| |
| /// The semantics label used for [SliverReorderableList] to reorder an item in the |
| /// list one space left in the list. |
| String get reorderItemLeft; |
| |
| /// The semantics label used for [SliverReorderableList] to reorder an item in the |
| /// list one space right in the list. |
| String get reorderItemRight; |
| |
| /// The semantics label used for [RawAutocomplete] when the options list goes |
| /// from empty to non-empty. |
| String get searchResultsFound => 'Search results found'; |
| |
| /// The semantics label used for [RawAutocomplete] when the options list goes |
| /// from non-empty to empty. |
| String get noResultsFound => 'No results found'; |
| |
| /// Label for "copy" edit buttons and menu items. |
| String get copyButtonLabel; |
| |
| /// Label for "cut" edit buttons and menu items. |
| String get cutButtonLabel; |
| |
| /// Label for "paste" edit buttons and menu items. |
| String get pasteButtonLabel; |
| |
| /// Label for "select all" edit buttons and menu items. |
| String get selectAllButtonLabel; |
| |
| /// Label for "look up" edit buttons and menu items. |
| String get lookUpButtonLabel; |
| |
| /// Label for "search web" edit buttons and menu items. |
| String get searchWebButtonLabel; |
| |
| /// Label for "share" edit buttons and menu items. |
| String get shareButtonLabel; |
| |
| /// The accessibility hint for an unselected radio button. |
| String get radioButtonUnselectedLabel; |
| |
| /// The `WidgetsLocalizations` from the closest [Localizations] instance |
| /// that encloses the given context. |
| /// |
| /// This method is just a convenient shorthand for: |
| /// `Localizations.of<WidgetsLocalizations>(context, WidgetsLocalizations)!`. |
| /// |
| /// References to the localized resources defined by this class are typically |
| /// written in terms of this method. For example: |
| /// |
| /// ```dart |
| /// textDirection: WidgetsLocalizations.of(context).textDirection, |
| /// ``` |
| static WidgetsLocalizations of(BuildContext context) { |
| assert(debugCheckHasWidgetsLocalizations(context)); |
| return Localizations.of<WidgetsLocalizations>(context, WidgetsLocalizations)!; |
| } |
| } |
| |
| class _WidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> { |
| const _WidgetsLocalizationsDelegate(); |
| |
| // This is convenient simplification. It would be more correct test if the locale's |
| // text-direction is LTR. |
| @override |
| bool isSupported(Locale locale) => true; |
| |
| @override |
| Future<WidgetsLocalizations> load(Locale locale) => DefaultWidgetsLocalizations.load(locale); |
| |
| @override |
| bool shouldReload(_WidgetsLocalizationsDelegate old) => false; |
| |
| @override |
| String toString() => 'DefaultWidgetsLocalizations.delegate(en_US)'; |
| } |
| |
| /// US English localizations for the widgets library. |
| /// |
| /// See also: |
| /// |
| /// * [GlobalWidgetsLocalizations], which provides widgets localizations for |
| /// many languages. |
| /// * [WidgetsApp.localizationsDelegates], which automatically includes |
| /// [DefaultWidgetsLocalizations.delegate] by default. |
| class DefaultWidgetsLocalizations implements WidgetsLocalizations { |
| /// Construct an object that defines the localized values for the widgets |
| /// library for US English (only). |
| /// |
| /// [LocalizationsDelegate] implementations typically call the static [load] |
| const DefaultWidgetsLocalizations(); |
| |
| @override |
| String get reorderItemUp => 'Move up'; |
| |
| @override |
| String get reorderItemDown => 'Move down'; |
| |
| @override |
| String get reorderItemLeft => 'Move left'; |
| |
| @override |
| String get reorderItemRight => 'Move right'; |
| |
| @override |
| String get reorderItemToEnd => 'Move to the end'; |
| |
| @override |
| String get reorderItemToStart => 'Move to the start'; |
| |
| @override |
| String get searchResultsFound => 'Search results found'; |
| |
| @override |
| String get noResultsFound => 'No results found'; |
| |
| @override |
| String get copyButtonLabel => 'Copy'; |
| |
| @override |
| String get cutButtonLabel => 'Cut'; |
| |
| @override |
| String get pasteButtonLabel => 'Paste'; |
| |
| @override |
| String get selectAllButtonLabel => 'Select all'; |
| |
| @override |
| String get lookUpButtonLabel => 'Look Up'; |
| |
| @override |
| String get searchWebButtonLabel => 'Search Web'; |
| |
| @override |
| String get shareButtonLabel => 'Share'; |
| |
| @override |
| String get radioButtonUnselectedLabel => 'Not selected'; |
| @override |
| TextDirection get textDirection => TextDirection.ltr; |
| |
| /// Creates an object that provides US English resource values for the |
| /// lowest levels of the widgets library. |
| /// |
| /// The [locale] parameter is ignored. |
| /// |
| /// This method is typically used to create a [LocalizationsDelegate]. |
| /// The [WidgetsApp] does so by default. |
| static Future<WidgetsLocalizations> load(Locale locale) { |
| return SynchronousFuture<WidgetsLocalizations>(const DefaultWidgetsLocalizations()); |
| } |
| |
| /// A [LocalizationsDelegate] that uses [DefaultWidgetsLocalizations.load] |
| /// to create an instance of this class. |
| /// |
| /// [WidgetsApp] automatically adds this value to [WidgetsApp.localizationsDelegates]. |
| static const LocalizationsDelegate<WidgetsLocalizations> delegate = |
| _WidgetsLocalizationsDelegate(); |
| } |
| |
| class _LocalizationsScope extends InheritedWidget { |
| const _LocalizationsScope({ |
| super.key, |
| required this.locale, |
| required this.localizationsState, |
| required this.typeToResources, |
| required super.child, |
| }); |
| |
| final Locale locale; |
| final _LocalizationsState localizationsState; |
| final Map<Type, dynamic> typeToResources; |
| |
| @override |
| bool updateShouldNotify(_LocalizationsScope old) { |
| return typeToResources != old.typeToResources; |
| } |
| } |
| |
| /// Defines the [Locale] for its `child` and the localized resources that the |
| /// child depends on. |
| /// |
| /// ## Defining localized resources |
| /// |
| /// {@tool snippet} |
| /// |
| /// This following class is defined in terms of the |
| /// [Dart `intl` package](https://github.com/dart-lang/i18n/tree/main/pkgs/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((void _) { |
| /// return 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() |
| /// } |
| /// ``` |
| /// {@end-tool} |
| /// 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. |
| /// |
| /// ## Loading localized resources |
| /// |
| /// 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 above would be: |
| /// |
| /// ```dart |
| /// // continuing from previous example... |
| /// class _MyDelegate extends LocalizationsDelegate<MyLocalizations> { |
| /// @override |
| /// Future<MyLocalizations> load(Locale locale) => MyLocalizations.load(locale); |
| /// |
| /// @override |
| /// bool isSupported(Locale locale) { |
| /// // in a real implementation this would only return true for |
| /// // locales that are definitely supported. |
| /// return true; |
| /// } |
| /// |
| /// @override |
| /// bool shouldReload(_MyDelegate old) => false; |
| /// } |
| /// ``` |
| /// |
| /// Each delegate can be viewed as a factory for objects that encapsulate 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]. |
| /// |
| /// ## Obtaining localized resources for use in user interfaces |
| /// |
| /// 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 |
| /// custom localized resource class (`MyLocalizations` in the example above). |
| /// |
| /// For example, using the `MyLocalizations` class defined above, one would |
| /// lookup a localized title string like this: |
| /// |
| /// ```dart |
| /// // continuing from previous example... |
| /// 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' [LocalizationsDelegate.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. |
| /// |
| /// The [Localizations] widget also instantiates [Directionality] in order to |
| /// support the appropriate [Directionality.textDirection] of the localized |
| /// resources. |
| class Localizations extends StatefulWidget { |
| /// Create a widget from which localizations (like translated strings) can be obtained. |
| Localizations({ |
| super.key, |
| required this.locale, |
| required this.delegates, |
| this.child, |
| this.isApplicationLevel = false, |
| }) : assert( |
| delegates.any( |
| (LocalizationsDelegate<dynamic> delegate) => |
| delegate is LocalizationsDelegate<WidgetsLocalizations>, |
| ), |
| ); |
| |
| /// Overrides the inherited [Locale] or [LocalizationsDelegate]s for `child`. |
| /// |
| /// This factory constructor is used for the (usually rare) situation where part |
| /// of an app should be localized for a different locale than the one defined |
| /// for the device, or if its localizations should come from a different list |
| /// of [LocalizationsDelegate]s than the list defined by |
| /// [WidgetsApp.localizationsDelegates]. |
| /// |
| /// For example you could specify that `myWidget` was only to be localized for |
| /// the US English locale: |
| /// |
| /// ```dart |
| /// Widget build(BuildContext context) { |
| /// return Localizations.override( |
| /// context: context, |
| /// locale: const Locale('en', 'US'), |
| /// child: myWidget, |
| /// ); |
| /// } |
| /// ``` |
| /// |
| /// The `locale` and `delegates` parameters default to the [Localizations.locale] |
| /// and [Localizations.delegates] values from the nearest [Localizations] ancestor. |
| /// |
| /// To override the [Localizations.locale] or [Localizations.delegates] for an |
| /// entire app, specify [WidgetsApp.locale] or [WidgetsApp.localizationsDelegates] |
| /// (or specify the same parameters for [MaterialApp]). |
| factory Localizations.override({ |
| Key? key, |
| required BuildContext context, |
| Locale? locale, |
| List<LocalizationsDelegate<dynamic>>? delegates, |
| Widget? child, |
| }) { |
| final List<LocalizationsDelegate<dynamic>> mergedDelegates = Localizations._delegatesOf( |
| context, |
| ); |
| if (delegates != null) { |
| mergedDelegates.insertAll(0, delegates); |
| } |
| return Localizations( |
| key: key, |
| locale: locale ?? Localizations.localeOf(context), |
| delegates: mergedDelegates, |
| child: child, |
| ); |
| } |
| |
| /// 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 List<LocalizationsDelegate<dynamic>> delegates; |
| |
| /// The widget below this widget in the tree. |
| /// |
| /// {@macro flutter.widgets.ProxyWidget.child} |
| final Widget? child; |
| |
| /// Whether this is the main localizations widget that represents the app's |
| /// locale. |
| final bool isApplicationLevel; |
| |
| /// The locale of the Localizations widget for the widget tree that |
| /// corresponds to [BuildContext] `context`. |
| /// |
| /// If no [Localizations] widget is in scope then the [Localizations.localeOf] |
| /// method will throw an exception. |
| static Locale localeOf(BuildContext context) { |
| final _LocalizationsScope? scope = context |
| .dependOnInheritedWidgetOfExactType<_LocalizationsScope>(); |
| assert(() { |
| if (scope == null) { |
| throw FlutterError( |
| 'Requested the Locale of a context that does not include a Localizations ancestor.\n' |
| 'To request the Locale, the context used to retrieve the Localizations widget must ' |
| 'be that of a widget that is a descendant of a Localizations widget.', |
| ); |
| } |
| if (scope.localizationsState.locale == null) { |
| throw FlutterError( |
| 'Localizations.localeOf found a Localizations widget that had a unexpected null locale.\n', |
| ); |
| } |
| return true; |
| }()); |
| return scope!.localizationsState.locale!; |
| } |
| |
| /// The locale of the Localizations widget for the widget tree that |
| /// corresponds to [BuildContext] `context`. |
| /// |
| /// If no [Localizations] widget is in scope then this function will return |
| /// null. |
| static Locale? maybeLocaleOf(BuildContext context) { |
| final _LocalizationsScope? scope = context |
| .dependOnInheritedWidgetOfExactType<_LocalizationsScope>(); |
| return scope?.localizationsState.locale; |
| } |
| |
| // There doesn't appear to be a need to make this public. See the |
| // Localizations.override factory constructor. |
| static List<LocalizationsDelegate<dynamic>> _delegatesOf(BuildContext context) { |
| final _LocalizationsScope? scope = context |
| .dependOnInheritedWidgetOfExactType<_LocalizationsScope>(); |
| assert(scope != null, 'a Localizations ancestor was not found'); |
| return List<LocalizationsDelegate<dynamic>>.of(scope!.localizationsState.widget.delegates); |
| } |
| |
| /// Returns the localized resources object of the given `type` for the widget |
| /// tree that corresponds to the given `context`. |
| /// |
| /// Returns null if no resources object of the given `type` exists within |
| /// the given `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) { |
| final _LocalizationsScope? scope = context |
| .dependOnInheritedWidgetOfExactType<_LocalizationsScope>(); |
| return scope?.localizationsState.resourcesFor<T?>(type); |
| } |
| |
| @override |
| State<Localizations> createState() => _LocalizationsState(); |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DiagnosticsProperty<Locale>('locale', locale)); |
| properties.add(IterableProperty<LocalizationsDelegate<dynamic>>('delegates', delegates)); |
| } |
| } |
| |
| class _LocalizationsState extends State<Localizations> { |
| final GlobalKey _localizedResourcesScopeKey = GlobalKey(); |
| Map<Type, dynamic> _typeToResources = <Type, dynamic>{}; |
| |
| Locale? get locale => _locale; |
| Locale? _locale; |
| set locale(Locale? locale) { |
| assert(locale != null); |
| if (_locale == locale) { |
| return; |
| } |
| WidgetsBinding.instance.platformDispatcher.setApplicationLocale(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 (var 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 || (_anyDelegatesShouldReload(old))) { |
| load(widget.locale); |
| } |
| } |
| |
| void load(Locale locale) { |
| final Iterable<LocalizationsDelegate<dynamic>> delegates = widget.delegates; |
| if (delegates.isEmpty) { |
| this.locale = locale; |
| return; |
| } |
| |
| Map<Type, dynamic>? typeToResources; |
| final Future<Map<Type, dynamic>> typeToResourcesFuture = _loadAll(locale, delegates) |
| .then<Map<Type, dynamic>>((Map<Type, dynamic> value) { |
| return typeToResources = value; |
| }); |
| |
| if (typeToResources != null) { |
| // All of the delegates' resources loaded synchronously. |
| _typeToResources = typeToResources!; |
| this.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. |
| RendererBinding.instance.deferFirstFrame(); |
| typeToResourcesFuture.then<void>((Map<Type, dynamic> value) { |
| if (mounted) { |
| setState(() { |
| _typeToResources = value; |
| this.locale = locale; |
| }); |
| } |
| RendererBinding.instance.allowFirstFrame(); |
| }); |
| } |
| } |
| |
| T resourcesFor<T>(Type type) { |
| final resources = _typeToResources[type] as T; |
| return resources; |
| } |
| |
| TextDirection get _textDirection { |
| final resources = _typeToResources[WidgetsLocalizations] as WidgetsLocalizations; |
| return resources.textDirection; |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| // TODO(chunhtai): notify engine about application locale if this is |
| // application level locale. |
| if (_locale == null) { |
| return const SizedBox.shrink(); |
| } |
| return Semantics( |
| // If this is not application level, we need to explicit mark the |
| // semantics subtree with the locale. |
| localeForSubtree: widget.isApplicationLevel ? null : widget.locale, |
| container: !widget.isApplicationLevel, |
| textDirection: _textDirection, |
| child: _LocalizationsScope( |
| key: _localizedResourcesScopeKey, |
| locale: _locale!, |
| localizationsState: this, |
| typeToResources: _typeToResources, |
| child: Directionality(textDirection: _textDirection, child: widget.child!), |
| ), |
| ); |
| } |
| } |
| |
| /// A helper class used to manage localization resolution. |
| /// |
| /// See also: |
| /// * [WidgetsApp], which utilizes [LocalizationsResolver] to handle locales. |
| class LocalizationsResolver extends ChangeNotifier with WidgetsBindingObserver { |
| /// Creates a [LocalizationsResolver] that determines the best-fit locale from the set of |
| /// [supportedLocales]. |
| /// |
| /// If provided, locale resolution will attempt to use [locale] as the current locale rather |
| /// than the system locale. |
| /// |
| /// Locale resolution behavior can be overridden by providing [localeListResolutionCallback] |
| /// or [localeResolutionCallback]. |
| /// |
| /// The delegates set via [localizationsDelegates] collectively define all of the localized |
| /// resources for a [Localizations] widget. |
| /// |
| /// See also: |
| /// |
| /// * [LocalizationsResolver.localeListResolutionCallback] and |
| /// [LocalizationsResolver.localeResolutionCallback] for more details on locale resolution |
| /// behavior. |
| /// * [LocalizationsDelegate] for more details about providing localized resources to a |
| /// [Localizations] widget. |
| LocalizationsResolver({ |
| required Iterable<Locale> supportedLocales, |
| Locale? locale, |
| LocaleListResolutionCallback? localeListResolutionCallback, |
| LocaleResolutionCallback? localeResolutionCallback, |
| Iterable<LocalizationsDelegate<Object?>>? localizationsDelegates, |
| }) : _locale = locale, |
| _localeListResolutionCallback = localeListResolutionCallback, |
| _localeResolutionCallback = localeResolutionCallback, |
| _localizationsDelegates = localizationsDelegates, |
| _supportedLocales = supportedLocales { |
| _resolvedLocale = _resolveLocales( |
| WidgetsBinding.instance.platformDispatcher.locales, |
| supportedLocales, |
| ); |
| WidgetsBinding.instance.addObserver(this); |
| } |
| |
| @override |
| void dispose() { |
| WidgetsBinding.instance.removeObserver(this); |
| super.dispose(); |
| } |
| |
| /// Replace one or more of the properties used for localization resolution and re-resolve the |
| /// locale. |
| void update({ |
| required Locale? locale, |
| required LocaleListResolutionCallback? localeListResolutionCallback, |
| required LocaleResolutionCallback? localeResolutionCallback, |
| required Iterable<LocalizationsDelegate<Object?>>? localizationsDelegates, |
| required Iterable<Locale> supportedLocales, |
| }) { |
| _locale = locale; |
| _localeListResolutionCallback = localeListResolutionCallback; |
| _localeResolutionCallback = localeResolutionCallback; |
| _localizationsDelegates = localizationsDelegates; |
| if (_supportedLocales != supportedLocales) { |
| _supportedLocales = supportedLocales; |
| _updateResolvedLocale(WidgetsBinding.instance.platformDispatcher.locales); |
| } |
| } |
| |
| /// The currently resolved [Locale] based on the current platform locale and |
| /// the provided set of [supportedLocales]. |
| Locale get locale { |
| final Locale appLocale = _locale != null |
| ? _resolveLocales(<Locale>[_locale!], supportedLocales) |
| : _resolvedLocale!; |
| assert(_debugCheckLocalizations(appLocale)); |
| return appLocale; |
| } |
| |
| /// {@macro flutter.widgets.widgetsApp.localizationsDelegates} |
| Iterable<LocalizationsDelegate<Object?>> get localizationsDelegates { |
| // Combine the Localizations for Widgets with the ones contributed |
| // by the localizationsDelegates parameter, if any. Only the first delegate |
| // of a particular LocalizationsDelegate.type is loaded so the |
| // localizationsDelegate parameter can be used to override |
| // WidgetsLocalizations.delegate. |
| return <LocalizationsDelegate<Object?>>[ |
| ...?_localizationsDelegates, |
| DefaultWidgetsLocalizations.delegate, |
| ]; |
| } |
| |
| Iterable<LocalizationsDelegate<Object?>>? _localizationsDelegates; |
| |
| /// {@macro flutter.widgets.widgetsApp.localeListResolutionCallback} |
| /// |
| /// See also: |
| /// |
| /// * [basicLocaleListResolution], the default locale resolution algorithm. |
| LocaleListResolutionCallback? get localeListResolutionCallback => _localeListResolutionCallback; |
| LocaleListResolutionCallback? _localeListResolutionCallback; |
| |
| /// {@macro flutter.widgets.LocaleResolutionCallback} |
| LocaleResolutionCallback? get localeResolutionCallback => _localeResolutionCallback; |
| LocaleResolutionCallback? _localeResolutionCallback; |
| |
| /// {@macro flutter.widgets.widgetsApp.supportedLocales} |
| /// |
| /// See also: |
| /// |
| /// * [localeResolutionCallback], an app callback that resolves the app's locale |
| /// when the device's locale changes. |
| /// * [localizationsDelegates], which collectively define all of the localized |
| /// resources used by this app. |
| /// * [basicLocaleListResolution], the default locale resolution algorithm. |
| Iterable<Locale> get supportedLocales => _supportedLocales; |
| Iterable<Locale> _supportedLocales; |
| |
| Locale? _locale; |
| |
| /// This is the resolved locale, and is one of the supportedLocales. |
| Locale? _resolvedLocale; |
| |
| @override |
| void didChangeLocales(List<Locale>? locales) { |
| _updateResolvedLocale(locales); |
| } |
| |
| /// Recompute the resolved locale based on [preferredLocales] and |
| /// [supportedLocales], update the resolved locale, and notify listeners |
| /// only if the resolved locale changed. |
| /// |
| /// This method is called from both [update] (when locale resolution |
| /// parameters change) and [didChangeLocales] (when system locales change). |
| void _updateResolvedLocale(List<Locale>? preferredLocales) { |
| final Locale newLocale = _resolveLocales(preferredLocales, supportedLocales); |
| if (newLocale != _resolvedLocale) { |
| _resolvedLocale = newLocale; |
| notifyListeners(); |
| } |
| } |
| |
| Locale _resolveLocales(List<Locale>? preferredLocales, Iterable<Locale> supportedLocales) { |
| // Attempt to use localeListResolutionCallback. |
| if (localeListResolutionCallback != null) { |
| final Locale? locale = localeListResolutionCallback!(preferredLocales, supportedLocales); |
| if (locale != null) { |
| return locale; |
| } |
| } |
| // localeListResolutionCallback failed, falling back to localeResolutionCallback. |
| if (localeResolutionCallback != null) { |
| final Locale? locale = localeResolutionCallback!( |
| preferredLocales != null && preferredLocales.isNotEmpty ? preferredLocales.first : null, |
| supportedLocales, |
| ); |
| if (locale != null) { |
| return locale; |
| } |
| } |
| // Both callbacks failed, falling back to default algorithm. |
| return basicLocaleListResolution(preferredLocales, supportedLocales); |
| } |
| |
| @override |
| String toString() => '$LocalizationsResolver'; |
| |
| bool _debugCheckLocalizations(Locale locale) { |
| assert(() { |
| final Set<Type> unsupportedTypes = localizationsDelegates |
| .map<Type>((LocalizationsDelegate<dynamic> delegate) => delegate.type) |
| .toSet(); |
| for (final LocalizationsDelegate<dynamic> delegate in localizationsDelegates) { |
| if (!unsupportedTypes.contains(delegate.type)) { |
| continue; |
| } |
| if (delegate.isSupported(locale)) { |
| unsupportedTypes.remove(delegate.type); |
| } |
| } |
| if (unsupportedTypes.isEmpty) { |
| return true; |
| } |
| |
| FlutterError.reportError( |
| FlutterErrorDetails( |
| exception: |
| "Warning: This application's locale, $locale, is not supported by all of its localization delegates.", |
| library: 'widgets', |
| informationCollector: () => <DiagnosticsNode>[ |
| for (final Type unsupportedType in unsupportedTypes) |
| ErrorDescription( |
| '• A $unsupportedType delegate that supports the $locale locale was not found.', |
| ), |
| ErrorSpacer(), |
| if (unsupportedTypes.length == 1 && |
| unsupportedTypes.single.toString() == 'CupertinoLocalizations') |
| // We previously explicitly avoided checking for this class so it's not uncommon for applications |
| // to have omitted importing the required delegate. |
| ...<DiagnosticsNode>[ |
| ErrorHint( |
| 'If the application is built using GlobalMaterialLocalizations.delegate, consider using ' |
| 'GlobalMaterialLocalizations.delegates (plural) instead, as that will automatically declare ' |
| 'the appropriate Cupertino localizations.', |
| ), |
| ErrorSpacer(), |
| ], |
| ErrorHint( |
| 'The declared supported locales for this app are: ${supportedLocales.join(", ")}', |
| ), |
| ErrorSpacer(), |
| ErrorDescription( |
| 'See https://flutter.dev/to/internationalization/ for more ' |
| "information about configuring an app's locale, supportedLocales, " |
| 'and localizationsDelegates parameters.', |
| ), |
| ], |
| ), |
| ); |
| return true; |
| }()); |
| return true; |
| } |
| } |