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

import 'package:flutter/foundation.dart';
6
import 'package:flutter/rendering.dart';
7

8
import 'basic.dart';
9
import 'debug.dart';
10 11 12
import 'framework.dart';

// Examples can assume:
13
// class Intl { Intl._(); static String message(String s, { String? name, String? locale }) => ''; }
14
// Future<void> initializeMessages(String locale) => Future<void>.value();
15 16 17
// late BuildContext context;
// class Foo { }
// const Widget myWidget = Placeholder();
18

19 20 21 22 23 24 25 26
// Used by loadAll() to record LocalizationsDelegate.load() futures we're
// waiting for.
class _Pending {
  _Pending(this.delegate, this.futureValue);
  final LocalizationsDelegate<dynamic> delegate;
  final Future<dynamic> futureValue;
}

27 28
// A utility function used by Localizations to generate one future
// that completes when all of the LocalizationsDelegate.load() futures
29
// complete. The returned map is indexed by each delegate's type.
30 31 32 33 34 35 36 37 38 39 40
//
// The input future values must have distinct types.
//
// The returned Future<Map> will resolve when all of the input map's
// future values have resolved. If all of the input map's values are
// SynchronousFutures then a SynchronousFuture will be returned
// immediately.
//
// This is more complicated than just applying Future.wait to input
// because some of the input.values may be SynchronousFutures. We don't want
// to Future.wait for the synchronous futures.
41
Future<Map<Type, dynamic>> _loadAll(Locale locale, Iterable<LocalizationsDelegate<dynamic>> allDelegates) {
42
  final Map<Type, dynamic> output = <Type, dynamic>{};
43
  List<_Pending>? pendingList;
44

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

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

72
  // All of the delegate.load() values were synchronous futures, we're done.
73
  if (pendingList == null) {
74
    return SynchronousFuture<Map<Type, dynamic>>(output);
75
  }
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
    .then<Map<Type, dynamic>>((List<dynamic> values) {
80
      assert(values.length == pendingList!.length);
81
      for (int i = 0; i < values.length; i += 1) {
82
        final Type type = pendingList![i].delegate.type;
83 84 85 86 87
        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
///
/// An example of a class used as the value of `T` here would be
/// MaterialLocalizations.
100 101 102 103 104
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();

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

111 112 113
  /// Start loading the resources for `locale`. The returned future completes
  /// when the resources have finished loading.
  ///
114 115 116
  /// It's assumed that this method will return an object that contains a
  /// collection of related string resources (typically defined with one method
  /// per resource). The object will be retrieved with [Localizations.of].
117 118 119 120 121 122 123 124 125
  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);
126

127 128 129 130 131 132
  /// 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:
133
  ///
134
  /// ```dart
135
  /// Foo foo = Localizations.of<Foo>(context, Foo)!;
136 137 138
  /// ```
  ///
  /// It's rarely necessary to override this getter.
139 140
  Type get type => T;

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

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

  /// The `WidgetsLocalizations` from the closest [Localizations] instance
  /// that encloses the given context.
  ///
  /// This method is just a convenient shorthand for:
163
  /// `Localizations.of<WidgetsLocalizations>(context, WidgetsLocalizations)!`.
164 165 166 167 168 169 170
  ///
  /// 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,
  /// ```
171 172 173
  static WidgetsLocalizations of(BuildContext context) {
    assert(debugCheckHasWidgetsLocalizations(context));
    return Localizations.of<WidgetsLocalizations>(context, WidgetsLocalizations)!;
174
  }
175 176
}

177 178 179
class _WidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
  const _WidgetsLocalizationsDelegate();

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

185 186 187 188 189
  @override
  Future<WidgetsLocalizations> load(Locale locale) => DefaultWidgetsLocalizations.load(locale);

  @override
  bool shouldReload(_WidgetsLocalizationsDelegate old) => false;
190 191 192

  @override
  String toString() => 'DefaultWidgetsLocalizations.delegate(en_US)';
193 194 195 196 197 198
}

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

  @override
211
  TextDirection get textDirection => TextDirection.ltr;
212

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

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

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

  final Locale locale;
  final _LocalizationsState localizationsState;
243
  final Map<Type, dynamic> typeToResources;
244

245 246
  @override
  bool updateShouldNotify(_LocalizationsScope old) {
247
    return typeToResources != old.typeToResources;
248 249 250 251 252 253
  }
}

/// Defines the [Locale] for its `child` and the localized resources that the
/// child depends on.
///
254 255 256 257 258 259 260 261 262 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 292 293 294
/// ## Defining localized resources
///
/// {@tool snippet}
///
/// 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((void _) {
///         return 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()
/// }
/// ```
/// {@end-tool}
/// 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.
///
/// ## Loading localized resources
///
295 296 297 298 299 300 301
/// 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
302
/// `MyLocalizations` class defined above would be:
303 304
///
/// ```dart
305
/// // continuing from previous example...
306 307 308
/// class _MyDelegate extends LocalizationsDelegate<MyLocalizations> {
///   @override
///   Future<MyLocalizations> load(Locale locale) => MyLocalizations.load(locale);
309
///
310
///   @override
311 312 313 314 315 316 317 318
///   bool isSupported(Locale locale) {
///     // in a real implementation this would only return true for
///     // locales that are definitely supported.
///     return true;
///   }
///
///   @override
///   bool shouldReload(_MyDelegate old) => false;
319
/// }
320 321 322 323 324 325
/// ```
///
/// 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].
///
326 327
/// The [WidgetsApp] class creates a [Localizations] widget so most apps
/// will not need to create one. The widget app's [Localizations] delegates can
328 329 330 331
/// be initialized with [WidgetsApp.localizationsDelegates]. The [MaterialApp]
/// class also provides a `localizationsDelegates` parameter that's just
/// passed along to the [WidgetsApp].
///
332 333
/// ## Obtaining localized resources for use in user interfaces
///
334 335 336 337
/// 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
338
/// custom localized resource class (`MyLocalizations` in the example above).
339
///
340
/// For example, using the `MyLocalizations` class defined above, one would
341
/// lookup a localized title string like this:
342
///
343
/// ```dart
344
/// // continuing from previous example...
345 346
/// MyLocalizations.of(context).title()
/// ```
347
///
348
/// If [Localizations] were to be rebuilt with a new `locale` then
349 350 351 352 353
/// 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
354
/// delegates' [LocalizationsDelegate.shouldReload()] methods returns true,
355 356 357 358
/// 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.
///
359
/// The [Localizations] widget also instantiates [Directionality] in order to
360 361
/// support the appropriate [Directionality.textDirection] of the localized
/// resources.
362
class Localizations extends StatefulWidget {
363
  /// Create a widget from which localizations (like translated strings) can be obtained.
364
  Localizations({
365
    super.key,
366 367
    required this.locale,
    required this.delegates,
368
    this.child,
369 370
  }) : assert(locale != null),
       assert(delegates != null),
371
       assert(delegates.any((LocalizationsDelegate<dynamic> delegate) => delegate is LocalizationsDelegate<WidgetsLocalizations>));
372

373 374
  /// Overrides the inherited [Locale] or [LocalizationsDelegate]s for `child`.
  ///
375
  /// This factory constructor is used for the (usually rare) situation where part
376 377 378 379 380 381 382 383 384 385
  /// 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) {
386
  ///   return Localizations.override(
387 388 389 390 391 392 393 394 395 396 397 398 399 400
  ///     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({
401 402 403 404 405
    Key? key,
    required BuildContext context,
    Locale? locale,
    List<LocalizationsDelegate<dynamic>>? delegates,
    Widget? child,
406 407
  }) {
    final List<LocalizationsDelegate<dynamic>> mergedDelegates = Localizations._delegatesOf(context);
408
    if (delegates != null) {
409
      mergedDelegates.insertAll(0, delegates);
410
    }
411
    return Localizations(
412
      key: key,
413
      locale: locale ?? Localizations.localeOf(context),
414 415 416 417 418
      delegates: mergedDelegates,
      child: child,
    );
  }

419 420 421 422
  /// 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
423 424
  /// be retrieved with [Localizations.of].
  final List<LocalizationsDelegate<dynamic>> delegates;
425 426

  /// The widget below this widget in the tree.
427
  ///
428
  /// {@macro flutter.widgets.ProxyWidget.child}
429
  final Widget? child;
430 431 432

  /// The locale of the Localizations widget for the widget tree that
  /// corresponds to [BuildContext] `context`.
Ian Hickson's avatar
Ian Hickson committed
433 434
  ///
  /// If no [Localizations] widget is in scope then the [Localizations.localeOf]
435 436
  /// method will throw an exception.
  static Locale localeOf(BuildContext context) {
437
    assert(context != null);
438
    final _LocalizationsScope? scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
439 440 441 442 443
    assert(() {
      if (scope == null) {
        throw FlutterError(
          'Requested the Locale of a context that does not include a Localizations ancestor.\n'
          'To request the Locale, the context used to retrieve the Localizations widget must '
444
          'be that of a widget that is a descendant of a Localizations widget.',
445 446
        );
      }
447
      if (scope.localizationsState.locale == null) {
448
        throw FlutterError(
449
          'Localizations.localeOf found a Localizations widget that had a unexpected null locale.\n',
450 451 452 453
        );
      }
      return true;
    }());
454
    return scope!.localizationsState.locale!;
455 456
  }

457 458 459 460
  /// The locale of the Localizations widget for the widget tree that
  /// corresponds to [BuildContext] `context`.
  ///
  /// If no [Localizations] widget is in scope then this function will return
461
  /// null.
462 463 464 465 466 467
  static Locale? maybeLocaleOf(BuildContext context) {
    assert(context != null);
    final _LocalizationsScope? scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
    return scope?.localizationsState.locale;
  }

468 469 470 471
  // 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);
472
    final _LocalizationsScope? scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
473
    assert(scope != null, 'a Localizations ancestor was not found');
474
    return List<LocalizationsDelegate<dynamic>>.of(scope!.localizationsState.widget.delegates);
475 476
  }

477 478 479
  /// Returns the localized resources object of the given `type` for the widget
  /// tree that corresponds to the given `context`.
  ///
480
  /// Returns null if no resources object of the given `type` exists within
481
  /// the given `context`.
482
  ///
483
  /// This method is typically used by a static factory method on the `type`
484 485 486 487 488
  /// class. For example Flutter's MaterialLocalizations class looks up Material
  /// resources with a method defined like this:
  ///
  /// ```dart
  /// static MaterialLocalizations of(BuildContext context) {
489
  ///   return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations)!;
490 491
  /// }
  /// ```
492
  static T? of<T>(BuildContext context, Type type) {
493 494
    assert(context != null);
    assert(type != null);
495
    final _LocalizationsScope? scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
496
    return scope?.localizationsState.resourcesFor<T?>(type);
497 498 499
  }

  @override
500
  State<Localizations> createState() => _LocalizationsState();
501 502

  @override
503 504
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
505 506
    properties.add(DiagnosticsProperty<Locale>('locale', locale));
    properties.add(IterableProperty<LocalizationsDelegate<dynamic>>('delegates', delegates));
507
  }
508 509 510
}

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

514 515
  Locale? get locale => _locale;
  Locale? _locale;
516 517 518 519 520 521 522 523

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

  bool _anyDelegatesShouldReload(Localizations old) {
524
    if (widget.delegates.length != old.delegates.length) {
525
      return true;
526
    }
527 528 529 530 531
    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];
532
      if (delegate.runtimeType != oldDelegate.runtimeType || delegate.shouldReload(oldDelegate)) {
533
        return true;
534
      }
535 536 537 538 539 540 541 542
    }
    return false;
  }

  @override
  void didUpdateWidget(Localizations old) {
    super.didUpdateWidget(old);
    if (widget.locale != old.locale
543
        || (widget.delegates == null)
544
        || (widget.delegates != null && old.delegates == null)
545
        || (widget.delegates != null && _anyDelegatesShouldReload(old))) {
546
      load(widget.locale);
547
    }
548 549 550 551 552 553 554 555 556
  }

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

557
    Map<Type, dynamic>? typeToResources;
558
    final Future<Map<Type, dynamic>> typeToResourcesFuture = _loadAll(locale, delegates)
559
      .then<Map<Type, dynamic>>((Map<Type, dynamic> value) {
560 561 562 563 564
        return typeToResources = value;
      });

    if (typeToResources != null) {
      // All of the delegates' resources loaded synchronously.
565
      _typeToResources = typeToResources!;
566 567 568 569 570 571
      _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.
572
      RendererBinding.instance.deferFirstFrame();
573
      typeToResourcesFuture.then<void>((Map<Type, dynamic> value) {
574 575 576 577 578 579
        if (mounted) {
          setState(() {
            _typeToResources = value;
            _locale = locale;
          });
        }
580
        RendererBinding.instance.allowFirstFrame();
581 582 583 584 585 586
      });
    }
  }

  T resourcesFor<T>(Type type) {
    assert(type != null);
587
    final T resources = _typeToResources[type] as T;
588 589 590
    return resources;
  }

591
  TextDirection get _textDirection {
592
    final WidgetsLocalizations resources = _typeToResources[WidgetsLocalizations] as WidgetsLocalizations;
593 594 595 596
    assert(resources != null);
    return resources.textDirection;
  }

597 598
  @override
  Widget build(BuildContext context) {
599
    if (_locale == null) {
600
      return const SizedBox.shrink();
601
    }
602
    return Semantics(
603
      textDirection: _textDirection,
604
      child: _LocalizationsScope(
605
        key: _localizedResourcesScopeKey,
606
        locale: _locale!,
607 608
        localizationsState: this,
        typeToResources: _typeToResources,
609
        child: Directionality(
610
          textDirection: _textDirection,
611
          child: widget.child!,
612
        ),
613
      ),
614 615 616
    );
  }
}