localizations.dart 21 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
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 = Set<Type>();
48 49
  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
      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 79 80 81 82 83 84 85 86
    .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
  @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 195 196
///  * [GlobalWidgetsLocalizations], which provides widgets localizations for
///    many languages.
///  * [WidgetsApp.delegates], which automatically includes
///    [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 221

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

225
class _LocalizationsScope extends InheritedWidget {
226
  const _LocalizationsScope ({
227 228 229
    Key key,
    @required this.locale,
    @required this.localizationsState,
230
    @required this.typeToResources,
231
    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
/// {@tool sample}
301 302 303 304 305 306 307 308 309 310 311 312 313
///
/// 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())
314
///       .then((void _) {
315
///         return MyLocalizations(locale);
316 317 318 319 320 321 322 323 324 325 326
///       });
///   }
///
///   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()
/// }
/// ```
327
/// {@end-tool}
328 329 330 331 332 333 334 335 336
/// 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 {
337
  /// Create a widget from which localizations (like translated strings) can be obtained.
338 339 340
  Localizations({
    Key key,
    @required this.locale,
341 342
    @required this.delegates,
    this.child,
343 344 345 346
  }) : assert(locale != null),
       assert(delegates != null),
       assert(delegates.any((LocalizationsDelegate<dynamic> delegate) => delegate is LocalizationsDelegate<WidgetsLocalizations>)),
       super(key: key);
347

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

393 394 395 396
  /// 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
397 398
  /// be retrieved with [Localizations.of].
  final List<LocalizationsDelegate<dynamic>> delegates;
399 400

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

421 422 423 424 425 426
  // 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');
427
    return List<LocalizationsDelegate<dynamic>>.from(scope.localizationsState.widget.delegates);
428 429
  }

430 431 432
  /// Returns the localized resources object of the given `type` for the widget
  /// tree that corresponds to the given `context`.
  ///
433
  /// Returns null if no resources object of the given `type` exists within
434
  /// the given `context`.
435
  ///
436
  /// This method is typically used by a static factory method on the `type`
437 438 439 440 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);
  /// }
  /// ```
  static T of<T>(BuildContext context, Type type) {
    assert(context != null);
    assert(type != null);
    final _LocalizationsScope scope = context.inheritFromWidgetOfExactType(_LocalizationsScope);
449
    return scope?.localizationsState?.resourcesFor<T>(type);
450 451 452
  }

  @override
453
  _LocalizationsState createState() => _LocalizationsState();
454 455

  @override
456 457
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
458 459
    properties.add(DiagnosticsProperty<Locale>('locale', locale));
    properties.add(IterableProperty<LocalizationsDelegate<dynamic>>('delegates', delegates));
460
  }
461 462 463
}

class _LocalizationsState extends State<Localizations> {
464
  final GlobalKey _localizedResourcesScopeKey = GlobalKey();
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 504 505 506 507
  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;
508
    final Future<Map<Type, dynamic>> typeToResourcesFuture = _loadAll(locale, delegates)
509
      .then<Map<Type, dynamic>>((Map<Type, dynamic> value) {
510 511 512 513 514 515 516 517 518 519 520 521 522
        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();
523
      typeToResourcesFuture.then<void>((Map<Type, dynamic> value) {
524 525 526 527 528 529 530 531 532 533 534 535 536
        WidgetsBinding.instance.allowFirstFrameReport();
        if (!mounted)
          return;
        setState(() {
          _typeToResources = value;
          _locale = locale;
        });
      });
    }
  }

  T resourcesFor<T>(Type type) {
    assert(type != null);
537
    final T resources = _typeToResources[type];
538 539 540
    return resources;
  }

541 542 543 544 545 546
  TextDirection get _textDirection {
    final WidgetsLocalizations resources = _typeToResources[WidgetsLocalizations];
    assert(resources != null);
    return resources.textDirection;
  }

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