1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart';
6import 'package:flutter/rendering.dart';
7
8import 'basic.dart';
9import 'debug.dart';
10import 'framework.dart';
11
12// Examples can assume:
13// class Intl { Intl._(); static String message(String s, { String? name, String? locale }) => ''; }
14// Future initializeMessages(String locale) => Future.value();
15// late BuildContext context;
16// class Foo { }
17// const Widget myWidget = Placeholder();
18
19// Used by loadAll() to record LocalizationsDelegate.load() futures we're
20// waiting for.
21class _Pending {
22 _Pending(this.delegate, this.futureValue);
23 final LocalizationsDelegate<dynamic> delegate;
24 final Future<dynamic> futureValue;
25}
26
27// A utility function used by Localizations to generate one future
28// that completes when all of the LocalizationsDelegate.load() futures
29// complete. The returned map is indexed by each delegate's type.
30//
31// The input future values must have distinct types.
32//
33// The returned Future will resolve when all of the input map's
34// future values have resolved. If all of the input map's values are
35// SynchronousFutures then a SynchronousFuture will be returned
36// immediately.
37//
38// This is more complicated than just applying Future.wait to input
39// because some of the input.values may be SynchronousFutures. We don't want
40// to Future.wait for the synchronous futures.
41Future<Map<Type, dynamic>> _loadAll(Locale locale, Iterable<LocalizationsDelegate<dynamic>> allDelegates) {
42 final Map<Type, dynamic> output = <Type, dynamic>{};
43 List<_Pending>? pendingList;
44
45 // Only load the first delegate for each delegate type that supports
46 // locale.languageCode.
47 final Set<Type> types = <Type>{};
48 final List<LocalizationsDelegate<dynamic>> delegates = <LocalizationsDelegate<dynamic>>[];
49 for (final LocalizationsDelegate<dynamic> delegate in allDelegates) {
50 if (!types.contains(delegate.type) && delegate.isSupported(locale)) {
51 types.add(delegate.type);
52 delegates.add(delegate);
53 }
54 }
55
56 for (final LocalizationsDelegate<dynamic> delegate in delegates) {
57 final Future<dynamic> inputValue = delegate.load(locale);
58 dynamic completedValue;
59 final Future<dynamic> futureValue = inputValue.then<dynamic>((dynamic value) {
60 return completedValue = value;
61 });
62 if (completedValue != null) { // inputValue was a SynchronousFuture
63 final Type type = delegate.type;
64 assert(!output.containsKey(type));
65 output[type] = completedValue;
66 } else {
67 pendingList ??= <_Pending>[];
68 pendingList.add(_Pending(delegate, futureValue));
69 }
70 }
71
72 // All of the delegate.load() values were synchronous futures, we're done.
73 if (pendingList == null) {
74 return SynchronousFuture<Map<Type, dynamic>>(output);
75 }
76
77 // Some of delegate.load() values were asynchronous futures. Wait for them.
78 return Future.wait<dynamic>(pendingList.map<Future<dynamic>>((_Pending p) => p.futureValue))
79 .then<Map<Type, dynamic>>((List<dynamic> values) {
80 assert(values.length == pendingList!.length);
81 for (int i = 0; i < values.length; i += 1) {
82 final Type type = pendingList![i].delegate.type;
83 assert(!output.containsKey(type));
84 output[type] = values[i];
85 }
86 return output;
87 });
88}
89
90/// A factory for a set of localized resources of type `T`, to be loaded by a
91/// [Localizations] widget.
92///
93/// Typical applications have one [Localizations] widget which is created by the
94/// [WidgetsApp] and configured with the app's `localizationsDelegates`
95/// parameter (a list of delegates). The delegate's [type] is used to identify
96/// the object created by an individual delegate's [load] method.
97///
98/// An example of a class used as the value of `T` here would be
99/// [MaterialLocalizations].
100abstract class LocalizationsDelegate<T> {
101 /// Abstract const constructor. This constructor enables subclasses to provide
102 /// const constructors so that they can be used in const expressions.
103 const LocalizationsDelegate();
104
105 /// Whether resources for the given locale can be loaded by this delegate.
106 ///
107 /// Return true if the instance of `T` loaded by this delegate's [load]
108 /// method supports the given `locale`'s language.
109 bool isSupported(Locale locale);
110
111 /// Start loading the resources for `locale`. The returned future completes
112 /// when the resources have finished loading.
113 ///
114 /// It's assumed that this method will return an object that contains a
115 /// collection of related string resources (typically defined with one method
116 /// per resource). The object will be retrieved with [Localizations.of].
117 Future<T> load(Locale locale);
118
119 /// Returns true if the resources for this delegate should be loaded
120 /// again by calling the [load] method.
121 ///
122 /// This method is called whenever its [Localizations] widget is
123 /// rebuilt. If it returns true then dependent widgets will be rebuilt
124 /// after [load] has completed.
125 bool shouldReload(covariant LocalizationsDelegate<T> old);
126
127 /// The type of the object returned by the [load] method, T by default.
128 ///
129 /// This type is used to retrieve the object "loaded" by this
130 /// [LocalizationsDelegate] from the [Localizations] inherited widget.
131 /// For example the object loaded by `LocalizationsDelegate<Foo>` would
132 /// be retrieved with:
133 ///
134 /// ```dart
135 /// Foo foo = Localizations.of<Foo>(context, Foo)!;
136 /// ```
137 ///
138 /// It's rarely necessary to override this getter.
139 Type get type => T;
140
141 @override
142 String toString() => '${objectRuntimeType(this, 'LocalizationsDelegate')}[$type]';
143}
144
145/// Interface for localized resource values for the lowest levels of the Flutter
146/// framework.
147///
148/// This class also maps locales to a specific [Directionality] using the
149/// [textDirection] property.
150///
151/// See also:
152///
153/// * [DefaultWidgetsLocalizations], which implements this interface and
154/// supports a variety of locales.
155abstract class WidgetsLocalizations {
156 /// The reading direction for text in this locale.
157 TextDirection get textDirection;
158
159 /// The semantics label used for [SliverReorderableList] to reorder an item in the
160 /// list to the start of the list.
161 String get reorderItemToStart;
162
163 /// The semantics label used for [SliverReorderableList] to reorder an item in the
164 /// list to the end of the list.
165 String get reorderItemToEnd;
166
167 /// The semantics label used for [SliverReorderableList] to reorder an item in the
168 /// list one space up the list.
169 String get reorderItemUp;
170
171 /// The semantics label used for [SliverReorderableList] to reorder an item in the
172 /// list one space down the list.
173 String get reorderItemDown;
174
175 /// The semantics label used for [SliverReorderableList] to reorder an item in the
176 /// list one space left in the list.
177 String get reorderItemLeft;
178
179 /// The semantics label used for [SliverReorderableList] to reorder an item in the
180 /// list one space right in the list.
181 String get reorderItemRight;
182
183 /// The `WidgetsLocalizations` from the closest [Localizations] instance
184 /// that encloses the given context.
185 ///
186 /// This method is just a convenient shorthand for:
187 /// `Localizations.of<WidgetsLocalizations>(context, WidgetsLocalizations)!`.
188 ///
189 /// References to the localized resources defined by this class are typically
190 /// written in terms of this method. For example:
191 ///
192 /// ```dart
193 /// textDirection: WidgetsLocalizations.of(context).textDirection,
194 /// ```
195 static WidgetsLocalizations of(BuildContext context) {
196 assert(debugCheckHasWidgetsLocalizations(context));
197 return Localizations.of<WidgetsLocalizations>(context, WidgetsLocalizations)!;
198 }
199}
200
201class _WidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
202 const _WidgetsLocalizationsDelegate();
203
204 // This is convenient simplification. It would be more correct test if the locale's
205 // text-direction is LTR.
206 @override
207 bool isSupported(Locale locale) => true;
208
209 @override
210 Future<WidgetsLocalizations> load(Locale locale) => DefaultWidgetsLocalizations.load(locale);
211
212 @override
213 bool shouldReload(_WidgetsLocalizationsDelegate old) => false;
214
215 @override
216 String toString() => 'DefaultWidgetsLocalizations.delegate(en_US)';
217}
218
219/// US English localizations for the widgets library.
220///
221/// See also:
222///
223/// * [GlobalWidgetsLocalizations], which provides widgets localizations for
224/// many languages.
225/// * [WidgetsApp.localizationsDelegates], which automatically includes
226/// [DefaultWidgetsLocalizations.delegate] by default.
227class DefaultWidgetsLocalizations implements WidgetsLocalizations {
228 /// Construct an object that defines the localized values for the widgets
229 /// library for US English (only).
230 ///
231 /// [LocalizationsDelegate] implementations typically call the static [load]
232 const DefaultWidgetsLocalizations();
233
234 @override
235 String get reorderItemUp => 'Move up';
236
237 @override
238 String get reorderItemDown => 'Move down';
239
240 @override
241 String get reorderItemLeft => 'Move left';
242
243 @override
244 String get reorderItemRight => 'Move right';
245
246 @override
247 String get reorderItemToEnd => 'Move to the end';
248
249 @override
250 String get reorderItemToStart => 'Move to the start';
251
252 @override
253 TextDirection get textDirection => TextDirection.ltr;
254
255 /// Creates an object that provides US English resource values for the
256 /// lowest levels of the widgets library.
257 ///
258 /// The [locale] parameter is ignored.
259 ///
260 /// This method is typically used to create a [LocalizationsDelegate].
261 /// The [WidgetsApp] does so by default.
262 static Future<WidgetsLocalizations> load(Locale locale) {
263 return SynchronousFuture<WidgetsLocalizations>(const DefaultWidgetsLocalizations());
264 }
265
266 /// A [LocalizationsDelegate] that uses [DefaultWidgetsLocalizations.load]
267 /// to create an instance of this class.
268 ///
269 /// [WidgetsApp] automatically adds this value to [WidgetsApp.localizationsDelegates].
270 static const LocalizationsDelegate<WidgetsLocalizations> delegate = _WidgetsLocalizationsDelegate();
271}
272
273class _LocalizationsScope extends InheritedWidget {
274 const _LocalizationsScope({
275 super.key,
276 required this.locale,
277 required this.localizationsState,
278 required this.typeToResources,
279 required super.child,
280 });
281
282 final Locale locale;
283 final _LocalizationsState localizationsState;
284 final Map<Type, dynamic> typeToResources;
285
286 @override
287 bool updateShouldNotify(_LocalizationsScope old) {
288 return typeToResources != old.typeToResources;
289 }
290}
291
292/// Defines the [Locale] for its `child` and the localized resources that the
293/// child depends on.
294///
295/// ## Defining localized resources
296///
297/// {@tool snippet}
298///
299/// This following class is defined in terms of the
300/// [Dart `intl` package](https://github.com/dart-lang/intl). Using the `intl`
301/// package isn't required.
302///
303/// ```dart
304/// class MyLocalizations {
305/// MyLocalizations(this.locale);
306///
307/// final Locale locale;
308///
309/// static Future<MyLocalizations> load(Locale locale) {
310/// return initializeMessages(locale.toString())
311/// .then((void _) {
312/// return MyLocalizations(locale);
313/// });
314/// }
315///
316/// static MyLocalizations of(BuildContext context) {
317/// return Localizations.of<MyLocalizations>(context, MyLocalizations)!;
318/// }
319///
320/// String title() => Intl.message('<title>', name: 'title', locale: locale.toString());
321/// // ... more Intl.message() methods like title()
322/// }
323/// ```
324/// {@end-tool}
325/// A class based on the `intl` package imports a generated message catalog that provides
326/// the `initializeMessages()` function and the per-locale backing store for `Intl.message()`.
327/// The message catalog is produced by an `intl` tool that analyzes the source code for
328/// classes that contain `Intl.message()` calls. In this case that would just be the
329/// `MyLocalizations` class.
330///
331/// One could choose another approach for loading localized resources and looking them up while
332/// still conforming to the structure of this example.
333///
334/// ## Loading localized resources
335///
336/// Localized resources are loaded by the list of [LocalizationsDelegate]
337/// `delegates`. Each delegate is essentially a factory for a collection
338/// of localized resources. There are multiple delegates because there are
339/// multiple sources for localizations within an app.
340///
341/// Delegates are typically simple subclasses of [LocalizationsDelegate] that
342/// override [LocalizationsDelegate.load]. For example a delegate for the
343/// `MyLocalizations` class defined above would be:
344///
345/// ```dart
346/// // continuing from previous example...
347/// class _MyDelegate extends LocalizationsDelegate<MyLocalizations> {
348/// @override
349/// Future<MyLocalizations> load(Locale locale) => MyLocalizations.load(locale);
350///
351/// @override
352/// bool isSupported(Locale locale) {
353/// // in a real implementation this would only return true for
354/// // locales that are definitely supported.
355/// return true;
356/// }
357///
358/// @override
359/// bool shouldReload(_MyDelegate old) => false;
360/// }
361/// ```
362///
363/// Each delegate can be viewed as a factory for objects that encapsulate a set
364/// of localized resources. These objects are retrieved with
365/// by runtime type with [Localizations.of].
366///
367/// The [WidgetsApp] class creates a [Localizations] widget so most apps
368/// will not need to create one. The widget app's [Localizations] delegates can
369/// be initialized with [WidgetsApp.localizationsDelegates]. The [MaterialApp]
370/// class also provides a `localizationsDelegates` parameter that's just
371/// passed along to the [WidgetsApp].
372///
373/// ## Obtaining localized resources for use in user interfaces
374///
375/// Apps should retrieve collections of localized resources with
376/// `Localizations.of<MyLocalizations>(context, MyLocalizations)`,
377/// where MyLocalizations is an app specific class defines one function per
378/// resource. This is conventionally done by a static `.of` method on the
379/// custom localized resource class (`MyLocalizations` in the example above).
380///
381/// For example, using the `MyLocalizations` class defined above, one would
382/// lookup a localized title string like this:
383///
384/// ```dart
385/// // continuing from previous example...
386/// MyLocalizations.of(context).title()
387/// ```
388///
389/// If [Localizations] were to be rebuilt with a new `locale` then
390/// the widget subtree that corresponds to [BuildContext] `context` would
391/// be rebuilt after the corresponding resources had been loaded.
392///
393/// This class is effectively an [InheritedWidget]. If it's rebuilt with
394/// a new `locale` or a different list of delegates or any of its
395/// delegates' [LocalizationsDelegate.shouldReload()] methods returns true,
396/// then widgets that have created a dependency by calling
397/// `Localizations.of(context)` will be rebuilt after the resources
398/// for the new locale have been loaded.
399///
400/// The [Localizations] widget also instantiates [Directionality] in order to
401/// support the appropriate [Directionality.textDirection] of the localized
402/// resources.
403class Localizations extends StatefulWidget {
404 /// Create a widget from which localizations (like translated strings) can be obtained.
405 Localizations({
406 super.key,
407 required this.locale,
408 required this.delegates,
409 this.child,
410 }) : assert(delegates.any((LocalizationsDelegate<dynamic> delegate) => delegate is LocalizationsDelegate<WidgetsLocalizations>));
411
412 /// Overrides the inherited [Locale] or [LocalizationsDelegate]s for `child`.
413 ///
414 /// This factory constructor is used for the (usually rare) situation where part
415 /// of an app should be localized for a different locale than the one defined
416 /// for the device, or if its localizations should come from a different list
417 /// of [LocalizationsDelegate]s than the list defined by
418 /// [WidgetsApp.localizationsDelegates].
419 ///
420 /// For example you could specify that `myWidget` was only to be localized for
421 /// the US English locale:
422 ///
423 /// ```dart
424 /// Widget build(BuildContext context) {
425 /// return Localizations.override(
426 /// context: context,
427 /// locale: const Locale('en', 'US'),
428 /// child: myWidget,
429 /// );
430 /// }
431 /// ```
432 ///
433 /// The `locale` and `delegates` parameters default to the [Localizations.locale]
434 /// and [Localizations.delegates] values from the nearest [Localizations] ancestor.
435 ///
436 /// To override the [Localizations.locale] or [Localizations.delegates] for an
437 /// entire app, specify [WidgetsApp.locale] or [WidgetsApp.localizationsDelegates]
438 /// (or specify the same parameters for [MaterialApp]).
439 factory Localizations.override({
440 Key? key,
441 required BuildContext context,
442 Locale? locale,
443 List<LocalizationsDelegate<dynamic>>? delegates,
444 Widget? child,
445 }) {
446 final List<LocalizationsDelegate<dynamic>> mergedDelegates = Localizations._delegatesOf(context);
447 if (delegates != null) {
448 mergedDelegates.insertAll(0, delegates);
449 }
450 return Localizations(
451 key: key,
452 locale: locale ?? Localizations.localeOf(context),
453 delegates: mergedDelegates,
454 child: child,
455 );
456 }
457
458 /// The resources returned by [Localizations.of] will be specific to this locale.
459 final Locale locale;
460
461 /// This list collectively defines the localized resources objects that can
462 /// be retrieved with [Localizations.of].
463 final List<LocalizationsDelegate<dynamic>> delegates;
464
465 /// The widget below this widget in the tree.
466 ///
467 /// {@macro flutter.widgets.ProxyWidget.child}
468 final Widget? child;
469
470 /// The locale of the Localizations widget for the widget tree that
471 /// corresponds to [BuildContext] `context`.
472 ///
473 /// If no [Localizations] widget is in scope then the [Localizations.localeOf]
474 /// method will throw an exception.
475 static Locale localeOf(BuildContext context) {
476 final _LocalizationsScope? scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
477 assert(() {
478 if (scope == null) {
479 throw FlutterError(
480 'Requested the Locale of a context that does not include a Localizations ancestor.\n'
481 'To request the Locale, the context used to retrieve the Localizations widget must '
482 'be that of a widget that is a descendant of a Localizations widget.',
483 );
484 }
485 if (scope.localizationsState.locale == null) {
486 throw FlutterError(
487 'Localizations.localeOf found a Localizations widget that had a unexpected null locale.\n',
488 );
489 }
490 return true;
491 }());
492 return scope!.localizationsState.locale!;
493 }
494
495 /// The locale of the Localizations widget for the widget tree that
496 /// corresponds to [BuildContext] `context`.
497 ///
498 /// If no [Localizations] widget is in scope then this function will return
499 /// null.
500 static Locale? maybeLocaleOf(BuildContext context) {
501 final _LocalizationsScope? scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
502 return scope?.localizationsState.locale;
503 }
504
505 // There doesn't appear to be a need to make this public. See the
506 // Localizations.override factory constructor.
507 static List<LocalizationsDelegate<dynamic>> _delegatesOf(BuildContext context) {
508 final _LocalizationsScope? scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
509 assert(scope != null, 'a Localizations ancestor was not found');
510 return List<LocalizationsDelegate<dynamic>>.of(scope!.localizationsState.widget.delegates);
511 }
512
513 /// Returns the localized resources object of the given `type` for the widget
514 /// tree that corresponds to the given `context`.
515 ///
516 /// Returns null if no resources object of the given `type` exists within
517 /// the given `context`.
518 ///
519 /// This method is typically used by a static factory method on the `type`
520 /// class. For example Flutter's MaterialLocalizations class looks up Material
521 /// resources with a method defined like this:
522 ///
523 /// ```dart
524 /// static MaterialLocalizations of(BuildContext context) {
525 /// return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations)!;
526 /// }
527 /// ```
528 static T? of<T>(BuildContext context, Type type) {
529 final _LocalizationsScope? scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
530 return scope?.localizationsState.resourcesFor<T?>(type);
531 }
532
533 @override
534 State<Localizations> createState() => _LocalizationsState();
535
536 @override
537 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
538 super.debugFillProperties(properties);
539 properties.add(DiagnosticsProperty<Locale>('locale', locale));
540 properties.add(IterableProperty<LocalizationsDelegate<dynamic>>('delegates', delegates));
541 }
542}
543
544class _LocalizationsState extends State<Localizations> {
545 final GlobalKey _localizedResourcesScopeKey = GlobalKey();
546 Map<Type, dynamic> _typeToResources = <Type, dynamic>{};
547
548 Locale? get locale => _locale;
549 Locale? _locale;
550
551 @override
552 void initState() {
553 super.initState();
554 load(widget.locale);
555 }
556
557 bool _anyDelegatesShouldReload(Localizations old) {
558 if (widget.delegates.length != old.delegates.length) {
559 return true;
560 }
561 final List<LocalizationsDelegate<dynamic>> delegates = widget.delegates.toList();
562 final List<LocalizationsDelegate<dynamic>> oldDelegates = old.delegates.toList();
563 for (int i = 0; i < delegates.length; i += 1) {
564 final LocalizationsDelegate<dynamic> delegate = delegates[i];
565 final LocalizationsDelegate<dynamic> oldDelegate = oldDelegates[i];
566 if (delegate.runtimeType != oldDelegate.runtimeType || delegate.shouldReload(oldDelegate)) {
567 return true;
568 }
569 }
570 return false;
571 }
572
573 @override
574 void didUpdateWidget(Localizations old) {
575 super.didUpdateWidget(old);
576 if (widget.locale != old.locale || (_anyDelegatesShouldReload(old))) {
577 load(widget.locale);
578 }
579 }
580
581 void load(Locale locale) {
582 final Iterable<LocalizationsDelegate<dynamic>> delegates = widget.delegates;
583 if (delegates.isEmpty) {
584 _locale = locale;
585 return;
586 }
587
588 Map<Type, dynamic>? typeToResources;
589 final Future<Map<Type, dynamic>> typeToResourcesFuture = _loadAll(locale, delegates)
590 .then<Map<Type, dynamic>>((Map<Type, dynamic> value) {
591 return typeToResources = value;
592 });
593
594 if (typeToResources != null) {
595 // All of the delegates' resources loaded synchronously.
596 _typeToResources = typeToResources!;
597 _locale = locale;
598 } else {
599 // - Don't rebuild the dependent widgets until the resources for the new locale
600 // have finished loading. Until then the old locale will continue to be used.
601 // - If we're running at app startup time then defer reporting the first
602 // "useful" frame until after the async load has completed.
603 RendererBinding.instance.deferFirstFrame();
604 typeToResourcesFuture.then<void>((Map<Type, dynamic> value) {
605 if (mounted) {
606 setState(() {
607 _typeToResources = value;
608 _locale = locale;
609 });
610 }
611 RendererBinding.instance.allowFirstFrame();
612 });
613 }
614 }
615
616 T resourcesFor<T>(Type type) {
617 final T resources = _typeToResources[type] as T;
618 return resources;
619 }
620
621 TextDirection get _textDirection {
622 final WidgetsLocalizations resources = _typeToResources[WidgetsLocalizations] as WidgetsLocalizations;
623 return resources.textDirection;
624 }
625
626 @override
627 Widget build(BuildContext context) {
628 if (_locale == null) {
629 return const SizedBox.shrink();
630 }
631 return Semantics(
632 textDirection: _textDirection,
633 child: _LocalizationsScope(
634 key: _localizedResourcesScopeKey,
635 locale: _locale!,
636 localizationsState: this,
637 typeToResources: _typeToResources,
638 child: Directionality(
639 textDirection: _textDirection,
640 child: widget.child!,
641 ),
642 ),
643 );
644 }
645}
646