localizations.dart 21.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:ui' show Locale;

import 'package:flutter/foundation.dart';
8
import 'package:flutter/rendering.dart';
9

10
import 'basic.dart';
11 12 13 14 15 16
import 'binding.dart';
import 'container.dart';
import 'framework.dart';

// Examples can assume:
// class Intl { static String message(String s, { String name, String locale }) => ''; }
17
// Future<void> initializeMessages(String locale) => null;
18

19 20 21 22 23 24 25 26
// 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;
}

27 28
// A utility function used by Localizations to generate one future
// that completes when all of the LocalizationsDelegate.load() futures
29
// complete. The returned map is indexed by each delegate's type.
30 31 32 33 34 35 36 37 38 39 40
//
// 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.
41
Future<Map<Type, dynamic>> _loadAll(Locale locale, Iterable<LocalizationsDelegate<dynamic>> allDelegates) {
42
  final Map<Type, dynamic> output = <Type, dynamic>{};
43
  List<_Pending>? pendingList;
44

45 46
  // Only load the first delegate for each delegate type that supports
  // 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 52 53 54 55
      types.add(delegate.type);
      delegates.add(delegate);
    }
  }

56
  for (final LocalizationsDelegate<dynamic> delegate in delegates) {
57
    final Future<dynamic> inputValue = delegate.load(locale);
58 59 60 61 62
    dynamic completedValue;
    final Future<dynamic> futureValue = inputValue.then<dynamic>((dynamic value) {
      return completedValue = value;
    });
    if (completedValue != null) { // inputValue was a SynchronousFuture
63
      final Type type = delegate.type;
64 65 66
      assert(!output.containsKey(type));
      output[type] = completedValue;
    } else {
67
      pendingList ??= <_Pending>[];
68
      pendingList.add(_Pending(delegate, futureValue));
69 70 71
    }
  }

72 73
  // All of the delegate.load() values were synchronous futures, we're done.
  if (pendingList == null)
74
    return SynchronousFuture<Map<Type, dynamic>>(output);
75

76
  // Some of delegate.load() values were asynchronous futures. Wait for them.
77
  return Future.wait<dynamic>(pendingList.map<Future<dynamic>>((_Pending p) => p.futureValue))
78
    .then<Map<Type, dynamic>>((List<dynamic> values) {
79
      assert(values.length == pendingList!.length);
80
      for (int i = 0; i < values.length; i += 1) {
81
        final Type type = pendingList![i].delegate.type;
82 83 84 85 86
        assert(!output.containsKey(type));
        output[type] = values[i];
      }
      return output;
    });
87 88 89 90 91
}

/// A factory for a set of localized resources of type `T`, to be loaded by a
/// [Localizations] widget.
///
92 93 94 95
/// 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.
96 97 98 99 100
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();

101 102 103 104 105 106
  /// 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);

107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
  /// 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);
122

123 124 125 126 127 128 129 130 131 132 133
  /// 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.
134 135
  Type get type => T;

136
  @override
137
  String toString() => '${objectRuntimeType(this, 'LocalizationsDelegate')}[$type]';
138 139 140 141 142 143 144 145
}

/// Interface for localized resource values for the lowest levels of the Flutter
/// framework.
///
/// In particular, this maps locales to a specific [Directionality] using the
/// [textDirection] property.
///
146 147 148 149 150
/// See also:
///
///  * [DefaultWidgetsLocalizations], which implements this interface and
///    supports a variety of locales.
abstract class WidgetsLocalizations {
151
  /// The reading direction for text in this locale.
152
  TextDirection get textDirection;
153 154 155 156 157 158 159 160 161 162 163 164 165

  /// 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,
  /// ```
166
  static WidgetsLocalizations? of(BuildContext context) {
167 168
    return Localizations.of<WidgetsLocalizations>(context, WidgetsLocalizations);
  }
169 170
}

171 172 173
class _WidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
  const _WidgetsLocalizationsDelegate();

174 175 176 177 178
  // This is convenient simplification. It would be more correct test if the locale's
  // text-direction is LTR.
  @override
  bool isSupported(Locale locale) => true;

179 180 181 182 183
  @override
  Future<WidgetsLocalizations> load(Locale locale) => DefaultWidgetsLocalizations.load(locale);

  @override
  bool shouldReload(_WidgetsLocalizationsDelegate old) => false;
184 185 186

  @override
  String toString() => 'DefaultWidgetsLocalizations.delegate(en_US)';
187 188 189 190 191 192
}

/// US English localizations for the widgets library.
///
/// See also:
///
193 194
///  * [GlobalWidgetsLocalizations], which provides widgets localizations for
///    many languages.
195
///  * [WidgetsApp.localizationsDelegates], which automatically includes
196
///    [DefaultWidgetsLocalizations.delegate] by default.
197
class DefaultWidgetsLocalizations implements WidgetsLocalizations {
198
  /// Construct an object that defines the localized values for the widgets
199
  /// library for US English (only).
200 201
  ///
  /// [LocalizationsDelegate] implementations typically call the static [load]
202
  const DefaultWidgetsLocalizations();
203 204

  @override
205
  TextDirection get textDirection => TextDirection.ltr;
206

207 208 209 210
  /// Creates an object that provides US English resource values for the
  /// lowest levels of the widgets library.
  ///
  /// The [locale] parameter is ignored.
211 212 213 214
  ///
  /// This method is typically used to create a [LocalizationsDelegate].
  /// The [WidgetsApp] does so by default.
  static Future<WidgetsLocalizations> load(Locale locale) {
215
    return SynchronousFuture<WidgetsLocalizations>(const DefaultWidgetsLocalizations());
216
  }
217 218 219 220

  /// A [LocalizationsDelegate] that uses [DefaultWidgetsLocalizations.load]
  /// to create an instance of this class.
  ///
221
  /// [WidgetsApp] automatically adds this value to [WidgetsApp.localizationsDelegates].
222
  static const LocalizationsDelegate<WidgetsLocalizations> delegate = _WidgetsLocalizationsDelegate();
223 224
}

225
class _LocalizationsScope extends InheritedWidget {
226
  const _LocalizationsScope({
227 228 229 230 231
    Key? key,
    required this.locale,
    required this.localizationsState,
    required this.typeToResources,
    required Widget child,
232 233 234
  }) : assert(localizationsState != null),
       assert(typeToResources != null),
       super(key: key, child: child);
235 236 237

  final Locale locale;
  final _LocalizationsState localizationsState;
238
  final Map<Type, dynamic> typeToResources;
239

240 241
  @override
  bool updateShouldNotify(_LocalizationsScope old) {
242
    return typeToResources != old.typeToResources;
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
  }
}

/// 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);
262
///
263 264 265
///   @override
///   bool shouldReload(MyLocalizationsDelegate old) => false;
/// }
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
/// ```
///
/// 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
295
/// delegates' [LocalizationsDelegate.shouldReload()] methods returns true,
296 297 298 299
/// 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.
///
300 301 302 303
/// The `Localizations` widget also instantiates [Directionality] in order to
/// support the appropriate [Directionality.textDirection] of the localized
/// resources.
///
304
/// {@tool snippet}
305 306 307 308 309 310 311 312 313 314 315 316 317
///
/// 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())
318
///       .then((void _) {
319
///         return MyLocalizations(locale);
320 321 322 323 324 325 326 327 328 329 330
///       });
///   }
///
///   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()
/// }
/// ```
331
/// {@end-tool}
332 333 334 335 336 337 338 339 340
/// 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 {
341
  /// Create a widget from which localizations (like translated strings) can be obtained.
342
  Localizations({
343 344 345
    Key? key,
    required this.locale,
    required this.delegates,
346
    this.child,
347 348 349 350
  }) : assert(locale != null),
       assert(delegates != null),
       assert(delegates.any((LocalizationsDelegate<dynamic> delegate) => delegate is LocalizationsDelegate<WidgetsLocalizations>)),
       super(key: key);
351

352 353
  /// Overrides the inherited [Locale] or [LocalizationsDelegate]s for `child`.
  ///
354
  /// This factory constructor is used for the (usually rare) situation where part
355 356 357 358 359 360 361 362 363 364
  /// 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) {
365
  ///   return Localizations.override(
366 367 368 369 370 371 372 373 374 375 376 377 378 379
  ///     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({
380 381 382 383 384
    Key? key,
    required BuildContext context,
    Locale? locale,
    List<LocalizationsDelegate<dynamic>>? delegates,
    Widget? child,
385 386 387 388
  }) {
    final List<LocalizationsDelegate<dynamic>> mergedDelegates = Localizations._delegatesOf(context);
    if (delegates != null)
      mergedDelegates.insertAll(0, delegates);
389
    return Localizations(
390
      key: key,
391
      locale: locale ?? Localizations.localeOf(context)!,
392 393 394 395 396
      delegates: mergedDelegates,
      child: child,
    );
  }

397 398 399 400
  /// 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
401 402
  /// be retrieved with [Localizations.of].
  final List<LocalizationsDelegate<dynamic>> delegates;
403 404

  /// The widget below this widget in the tree.
405 406
  ///
  /// {@macro flutter.widgets.child}
407
  final Widget? child;
408 409 410

  /// The locale of the Localizations widget for the widget tree that
  /// corresponds to [BuildContext] `context`.
Ian Hickson's avatar
Ian Hickson committed
411 412 413 414
  ///
  /// If no [Localizations] widget is in scope then the [Localizations.localeOf]
  /// method will throw an exception, unless the `nullOk` argument is set to
  /// true, in which case it returns null.
415
  static Locale? localeOf(BuildContext context, { bool nullOk = false }) {
416
    assert(context != null);
Ian Hickson's avatar
Ian Hickson committed
417
    assert(nullOk != null);
418
    final _LocalizationsScope? scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
Ian Hickson's avatar
Ian Hickson committed
419 420
    if (nullOk && scope == null)
      return null;
421
    assert(scope != null, 'a Localizations ancestor was not found');
422
    return scope!.localizationsState.locale;
423 424
  }

425 426 427 428
  // 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) {
    assert(context != null);
429
    final _LocalizationsScope? scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
430
    assert(scope != null, 'a Localizations ancestor was not found');
431
    return List<LocalizationsDelegate<dynamic>>.from(scope!.localizationsState.widget.delegates);
432 433
  }

434 435 436
  /// Returns the localized resources object of the given `type` for the widget
  /// tree that corresponds to the given `context`.
  ///
437
  /// Returns null if no resources object of the given `type` exists within
438
  /// the given `context`.
439
  ///
440
  /// This method is typically used by a static factory method on the `type`
441 442 443 444 445 446 447 448
  /// 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);
  /// }
  /// ```
449
  static T? of<T>(BuildContext context, Type type) {
450 451
    assert(context != null);
    assert(type != null);
452 453
    final _LocalizationsScope? scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
    return scope?.localizationsState.resourcesFor<T>(type);
454 455 456
  }

  @override
457
  _LocalizationsState createState() => _LocalizationsState();
458 459

  @override
460 461
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
462 463
    properties.add(DiagnosticsProperty<Locale>('locale', locale));
    properties.add(IterableProperty<LocalizationsDelegate<dynamic>>('delegates', delegates));
464
  }
465 466 467
}

class _LocalizationsState extends State<Localizations> {
468
  final GlobalKey _localizedResourcesScopeKey = GlobalKey();
469 470
  Map<Type, dynamic> _typeToResources = <Type, dynamic>{};

471 472
  Locale? get locale => _locale;
  Locale? _locale;
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497

  @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
498
        || (widget.delegates == null)
499 500 501 502 503 504 505 506 507 508 509 510
        || (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;
    }

511
    Map<Type, dynamic>? typeToResources;
512
    final Future<Map<Type, dynamic>> typeToResourcesFuture = _loadAll(locale, delegates)
513
      .then<Map<Type, dynamic>>((Map<Type, dynamic> value) {
514 515 516 517 518
        return typeToResources = value;
      });

    if (typeToResources != null) {
      // All of the delegates' resources loaded synchronously.
519
      _typeToResources = typeToResources!;
520 521 522 523 524 525
      _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.
526
      RendererBinding.instance!.deferFirstFrame();
527
      typeToResourcesFuture.then<void>((Map<Type, dynamic> value) {
528 529 530 531 532 533
        if (mounted) {
          setState(() {
            _typeToResources = value;
            _locale = locale;
          });
        }
534
        RendererBinding.instance!.allowFirstFrame();
535 536 537 538 539 540
      });
    }
  }

  T resourcesFor<T>(Type type) {
    assert(type != null);
541
    final T resources = _typeToResources[type] as T;
542 543 544
    return resources;
  }

545
  TextDirection get _textDirection {
546
    final WidgetsLocalizations resources = _typeToResources[WidgetsLocalizations] as WidgetsLocalizations;
547 548 549 550
    assert(resources != null);
    return resources.textDirection;
  }

551 552
  @override
  Widget build(BuildContext context) {
553
    if (_locale == null)
554 555
      return Container();
    return Semantics(
556
      textDirection: _textDirection,
557
      child: _LocalizationsScope(
558
        key: _localizedResourcesScopeKey,
559
        locale: _locale!,
560 561
        localizationsState: this,
        typeToResources: _typeToResources,
562
        child: Directionality(
563
          textDirection: _textDirection,
564
          child: widget.child!,
565
        ),
566
      ),
567 568 569
    );
  }
}