localizations.dart 21.1 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 8
// 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';
9
import 'package:flutter/rendering.dart';
10

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

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

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

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

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

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

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

77
  // Some of delegate.load() values were asynchronous futures. Wait for them.
78
  return Future.wait<dynamic>(pendingList.map<Future<dynamic>>((_Pending p) => p.futureValue))
79 80 81 82 83 84 85 86 87
    .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;
    });
88 89 90 91 92
}

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

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

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

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

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

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

  /// 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);
  }
170 171
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

class _LocalizationsState extends State<Localizations> {
465
  final GlobalKey _localizedResourcesScopeKey = GlobalKey();
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 508
  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;
509
    final Future<Map<Type, dynamic>> typeToResourcesFuture = _loadAll(locale, delegates)
510
      .then<Map<Type, dynamic>>((Map<Type, dynamic> value) {
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.
523
      RendererBinding.instance.deferFirstFrame();
524
      typeToResourcesFuture.then<void>((Map<Type, dynamic> value) {
525 526 527 528 529 530 531
        if (mounted) {
          setState(() {
            _typeToResources = value;
            _locale = locale;
          });
        }
        RendererBinding.instance.allowFirstFrame();
532 533 534 535 536 537
      });
    }
  }

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

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

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