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
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7 8 9 10
import 'dart:async';
import 'dart:ui' show Locale;

import 'package:flutter/foundation.dart';
11
import 'package:flutter/rendering.dart';
12

13
import 'basic.dart';
14 15 16 17 18 19
import 'binding.dart';
import 'container.dart';
import 'framework.dart';

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

22 23 24 25 26 27 28 29
// 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;
}

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

48 49
  // Only load the first delegate for each delegate type that supports
  // locale.languageCode.
50
  final Set<Type> types = <Type>{};
51
  final List<LocalizationsDelegate<dynamic>> delegates = <LocalizationsDelegate<dynamic>>[];
52
  for (final LocalizationsDelegate<dynamic> delegate in allDelegates) {
53
    if (!types.contains(delegate.type) && delegate.isSupported(locale)) {
54 55 56 57 58
      types.add(delegate.type);
      delegates.add(delegate);
    }
  }

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

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

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

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

104 105 106 107 108 109
  /// 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);

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

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

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

/// 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.
///
149 150 151 152 153
/// See also:
///
///  * [DefaultWidgetsLocalizations], which implements this interface and
///    supports a variety of locales.
abstract class WidgetsLocalizations {
154
  /// The reading direction for text in this locale.
155
  TextDirection get textDirection;
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171

  /// 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) {
    return Localizations.of<WidgetsLocalizations>(context, WidgetsLocalizations);
  }
172 173
}

174 175 176
class _WidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
  const _WidgetsLocalizationsDelegate();

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

182 183 184 185 186
  @override
  Future<WidgetsLocalizations> load(Locale locale) => DefaultWidgetsLocalizations.load(locale);

  @override
  bool shouldReload(_WidgetsLocalizationsDelegate old) => false;
187 188 189

  @override
  String toString() => 'DefaultWidgetsLocalizations.delegate(en_US)';
190 191 192 193 194 195
}

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

  @override
208
  TextDirection get textDirection => TextDirection.ltr;
209

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

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

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

  final Locale locale;
  final _LocalizationsState localizationsState;
241
  final Map<Type, dynamic> typeToResources;
242

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

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

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

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

  /// The widget below this widget in the tree.
408 409
  ///
  /// {@macro flutter.widgets.child}
410 411 412 413
  final Widget child;

  /// The locale of the Localizations widget for the widget tree that
  /// corresponds to [BuildContext] `context`.
Ian Hickson's avatar
Ian Hickson committed
414 415 416 417
  ///
  /// 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.
418
  static Locale localeOf(BuildContext context, { bool nullOk = false }) {
419
    assert(context != null);
Ian Hickson's avatar
Ian Hickson committed
420
    assert(nullOk != null);
421
    final _LocalizationsScope scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
Ian Hickson's avatar
Ian Hickson committed
422 423
    if (nullOk && scope == null)
      return null;
424
    assert(scope != null, 'a Localizations ancestor was not found');
425 426 427
    return scope.localizationsState.locale;
  }

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

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

  @override
460
  _LocalizationsState createState() => _LocalizationsState();
461 462

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

class _LocalizationsState extends State<Localizations> {
471
  final GlobalKey _localizedResourcesScopeKey = GlobalKey();
472 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 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
  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;
    }

    Map<Type, dynamic> typeToResources;
515
    final Future<Map<Type, dynamic>> typeToResourcesFuture = _loadAll(locale, delegates)
516
      .then<Map<Type, dynamic>>((Map<Type, dynamic> value) {
517 518 519 520 521 522 523 524 525 526 527 528
        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.
529
      RendererBinding.instance.deferFirstFrame();
530
      typeToResourcesFuture.then<void>((Map<Type, dynamic> value) {
531 532 533 534 535 536 537
        if (mounted) {
          setState(() {
            _typeToResources = value;
            _locale = locale;
          });
        }
        RendererBinding.instance.allowFirstFrame();
538 539 540 541 542 543
      });
    }
  }

  T resourcesFor<T>(Type type) {
    assert(type != null);
544
    final T resources = _typeToResources[type] as T;
545 546 547
    return resources;
  }

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

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