localizations.dart 20.9 KB
Newer Older
1 2 3 4 5 6 7 8 9
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:ui' show Locale;

import 'package:flutter/foundation.dart';

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

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

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 48 49
  final Set<Type> types = new Set<Type>();
  final List<LocalizationsDelegate<dynamic>> delegates = <LocalizationsDelegate<dynamic>>[];
  for (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 57
  for (LocalizationsDelegate<dynamic> delegate in delegates) {
    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 68
      pendingList ??= <_Pending>[];
      pendingList.add(new _Pending(delegate, futureValue));
69 70 71
    }
  }

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

76 77 78 79 80 81 82 83 84 85 86
  // Some of delegate.load() values were asynchronous futures. Wait for them.
  return Future.wait<dynamic>(pendingList.map((_Pending p) => p.futureValue))
    .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;
    });
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() => '$runtimeType[$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 166 167 168

  /// 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);
  }
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 184 185 186 187 188 189
  @override
  Future<WidgetsLocalizations> load(Locale locale) => DefaultWidgetsLocalizations.load(locale);

  @override
  bool shouldReload(_WidgetsLocalizationsDelegate old) => false;
}

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

  @override
202
  TextDirection get textDirection => TextDirection.ltr;
203

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

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

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

  final Locale locale;
  final _LocalizationsState localizationsState;
235
  final Map<Type, dynamic> typeToResources;
236

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

/// 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);
259
///
260 261 262
///   @override
///   bool shouldReload(MyLocalizationsDelegate old) => false;
/// }
263 264 265 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
/// ```
///
/// 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
292
/// delegates' [LocalizationsDelegate.shouldReload()] methods returns true,
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
/// then widgets that have created a dependency by calling
/// `Localizations.of(context)` will be rebuilt after the resources
/// for the new locale have been loaded.
///
/// ## Sample code
///
/// This following class is defined in terms of the
/// [Dart `intl` package](https://github.com/dart-lang/intl). Using the `intl`
/// package isn't required.
///
/// ```dart
/// class MyLocalizations {
///   MyLocalizations(this.locale);
///
///   final Locale locale;
///
///   static Future<MyLocalizations> load(Locale locale) {
///     return initializeMessages(locale.toString())
///       .then((Null _) {
///         return new MyLocalizations(locale);
///       });
///   }
///
///   static MyLocalizations of(BuildContext context) {
///     return Localizations.of<MyLocalizations>(context, MyLocalizations);
///   }
///
///   String title() => Intl.message('<title>', name: 'title', locale: locale.toString());
///   // ... more Intl.message() methods like title()
/// }
/// ```
/// A class based on the `intl` package imports a generated message catalog that provides
/// the `initializeMessages()` function and the per-locale backing store for `Intl.message()`.
/// The message catalog is produced by an `intl` tool that analyzes the source code for
/// classes that contain `Intl.message()` calls. In this case that would just be the
/// `MyLocalizations` class.
///
/// One could choose another approach for loading localized resources and looking them up while
/// still conforming to the structure of this example.
class Localizations extends StatefulWidget {
333
  /// Create a widget from which localizations (like translated strings) can be obtained.
334 335 336
  Localizations({
    Key key,
    @required this.locale,
337 338
    @required this.delegates,
    this.child,
339 340 341 342
  }) : assert(locale != null),
       assert(delegates != null),
       assert(delegates.any((LocalizationsDelegate<dynamic> delegate) => delegate is LocalizationsDelegate<WidgetsLocalizations>)),
       super(key: key);
343

344 345
  /// Overrides the inherited [Locale] or [LocalizationsDelegate]s for `child`.
  ///
346
  /// This factory constructor is used for the (usually rare) situation where part
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
  /// 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 new 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 new Localizations(
      key: key,
      locale: locale ?? Localizations.localeOf(context),
      delegates: mergedDelegates,
      child: child,
    );
  }

389 390 391 392
  /// 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
393 394
  /// be retrieved with [Localizations.of].
  final List<LocalizationsDelegate<dynamic>> delegates;
395 396

  /// The widget below this widget in the tree.
397 398
  ///
  /// {@macro flutter.widgets.child}
399 400 401 402
  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
403 404 405 406
  ///
  /// 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.
407
  static Locale localeOf(BuildContext context, { bool nullOk = false }) {
408
    assert(context != null);
Ian Hickson's avatar
Ian Hickson committed
409
    assert(nullOk != null);
410
    final _LocalizationsScope scope = context.inheritFromWidgetOfExactType(_LocalizationsScope);
Ian Hickson's avatar
Ian Hickson committed
411 412
    if (nullOk && scope == null)
      return null;
413
    assert(scope != null, 'a Localizations ancestor was not found');
414 415 416
    return scope.localizationsState.locale;
  }

417 418 419 420 421 422 423 424 425
  // 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);
    final _LocalizationsScope scope = context.inheritFromWidgetOfExactType(_LocalizationsScope);
    assert(scope != null, 'a Localizations ancestor was not found');
    return new List<LocalizationsDelegate<dynamic>>.from(scope.localizationsState.widget.delegates);
  }

426 427 428
  /// Returns the localized resources object of the given `type` for the widget
  /// tree that corresponds to the given `context`.
  ///
429
  /// Returns null if no resources object of the given `type` exists within
430
  /// the given `context`.
431
  ///
432
  /// This method is typically used by a static factory method on the `type`
433 434 435 436 437 438 439 440 441 442 443 444
  /// class. For example Flutter's MaterialLocalizations class looks up Material
  /// resources with a method defined like this:
  ///
  /// ```dart
  /// static MaterialLocalizations of(BuildContext context) {
  ///    return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
  /// }
  /// ```
  static T of<T>(BuildContext context, Type type) {
    assert(context != null);
    assert(type != null);
    final _LocalizationsScope scope = context.inheritFromWidgetOfExactType(_LocalizationsScope);
445
    return scope?.localizationsState?.resourcesFor<T>(type);
446 447 448 449
  }

  @override
  _LocalizationsState createState() => new _LocalizationsState();
450 451

  @override
452 453 454 455
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(new DiagnosticsProperty<Locale>('locale', locale));
    properties.add(new IterableProperty<LocalizationsDelegate<dynamic>>('delegates', delegates));
456
  }
457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 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
}

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

  Locale get locale => _locale;
  Locale _locale;

  @override
  void initState() {
    super.initState();
    load(widget.locale);
  }

  bool _anyDelegatesShouldReload(Localizations old) {
    if (widget.delegates.length != old.delegates.length)
      return true;
    final List<LocalizationsDelegate<dynamic>> delegates = widget.delegates.toList();
    final List<LocalizationsDelegate<dynamic>> oldDelegates = old.delegates.toList();
    for (int i = 0; i < delegates.length; i += 1) {
      final LocalizationsDelegate<dynamic> delegate = delegates[i];
      final LocalizationsDelegate<dynamic> oldDelegate = oldDelegates[i];
      if (delegate.runtimeType != oldDelegate.runtimeType || delegate.shouldReload(oldDelegate))
        return true;
    }
    return false;
  }

  @override
  void didUpdateWidget(Localizations old) {
    super.didUpdateWidget(old);
    if (widget.locale != old.locale
        || (widget.delegates == null && old.delegates != null)
        || (widget.delegates != null && old.delegates == null)
        || (widget.delegates != null && _anyDelegatesShouldReload(old)))
      load(widget.locale);
  }

  void load(Locale locale) {
    final Iterable<LocalizationsDelegate<dynamic>> delegates = widget.delegates;
    if (delegates == null || delegates.isEmpty) {
      _locale = locale;
      return;
    }

    Map<Type, dynamic> typeToResources;
504
    final Future<Map<Type, dynamic>> typeToResourcesFuture = _loadAll(locale, delegates)
505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532
      .then((Map<Type, dynamic> value) {
        return typeToResources = value;
      });

    if (typeToResources != null) {
      // All of the delegates' resources loaded synchronously.
      _typeToResources = typeToResources;
      _locale = locale;
    } else {
      // - Don't rebuild the dependent widgets until the resources for the new locale
      // have finished loading. Until then the old locale will continue to be used.
      // - If we're running at app startup time then defer reporting the first
      // "useful" frame until after the async load has completed.
      WidgetsBinding.instance.deferFirstFrameReport();
      typeToResourcesFuture.then((Map<Type, dynamic> value) {
        WidgetsBinding.instance.allowFirstFrameReport();
        if (!mounted)
          return;
        setState(() {
          _typeToResources = value;
          _locale = locale;
        });
      });
    }
  }

  T resourcesFor<T>(Type type) {
    assert(type != null);
533
    final T resources = _typeToResources[type];
534 535 536
    return resources;
  }

537 538 539 540 541 542
  TextDirection get _textDirection {
    final WidgetsLocalizations resources = _typeToResources[WidgetsLocalizations];
    assert(resources != null);
    return resources.textDirection;
  }

543 544
  @override
  Widget build(BuildContext context) {
545 546
    if (_locale == null)
      return new Container();
547 548 549 550 551 552 553 554 555 556 557
    return new Semantics(
      textDirection: _textDirection,
      child: new _LocalizationsScope(
        key: _localizedResourcesScopeKey,
        locale: _locale,
        localizationsState: this,
        typeToResources: _typeToResources,
        child: new Directionality(
          textDirection: _textDirection,
          child: widget.child,
        ),
558
      ),
559 560 561
    );
  }
}