material_localizations.dart 18.4 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 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart' as intl;
10
import 'package:intl/date_symbols.dart' as intl;
11
import 'package:intl/date_symbol_data_custom.dart' as date_symbol_data_custom;
12
import 'l10n/date_localizations.dart' as date_localizations;
13

14
import 'l10n/localizations.dart' show TranslationBundle, translationBundleForLocale;
15 16
import 'widgets_localizations.dart';

17 18
// Watch out: the supported locales list in the doc comment below must be kept
// in sync with the list we test, see test/translations_test.dart, and of course
19
// the actual list of supported locales in _MaterialLocalizationsDelegate.
20

21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
/// Localized strings for the material widgets.
///
/// To include the localizations provided by this class in a [MaterialApp],
/// add [GlobalMaterialLocalizations.delegates] to
/// [MaterialApp.localizationsDelegates], and specify the locales your
/// app supports with [MaterialApp.supportedLocales]:
///
/// ```dart
/// new MaterialApp(
///   localizationsDelegates: GlobalMaterialLocalizations.delegates,
///   supportedLocales: [
///     const Locale('en', 'US'), // English
///     const Locale('he', 'IL'), // Hebrew
///     // ...
///   ],
///   // ...
/// )
/// ```
///
/// This class supports locales with the following [Locale.languageCode]s:
///
///   * ar - Arabic
///   * de - German
///   * en - English
///   * es - Spanish
///   * fa - Farsi
///   * fr - French
///   * he - Hebrew
49
///   * id - Indonesian
50 51
///   * it - Italian
///   * ja - Japanese
52
///   * ko - Korean
53
///   * ms - Malay
54
///   * nl - Dutch
55
///   * nb - Norwegian
56
///   * pl - Polish
57
///   * ps - Pashto
58
///   * pt - Portuguese
59
///   * ro - Romanian
60
///   * ru - Russian
61 62
///   * th - Thai
///   * tr - Turkish
63
///   * ur - Urdu
64
///   * vi - Vietnamese
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
///   * zh - Simplified Chinese
///
/// See also:
///
///  * The Flutter Internationalization Tutorial,
///    <https://flutter.io/tutorials/internationalization/>.
///  * [DefaultMaterialLocalizations], which only provides US English translations.
class GlobalMaterialLocalizations implements MaterialLocalizations {
  /// Constructs an object that defines the material widgets' localized strings
  /// for the given `locale`.
  ///
  /// [LocalizationsDelegate] implementations typically call the static [load]
  /// function, rather than constructing this class directly.
  GlobalMaterialLocalizations(this.locale)
      : assert(locale != null),
80
        _localeName = _computeLocaleName(locale) {
81 82
    _loadDateIntlDataIfNotLoaded();

83 84
    _translationBundle = translationBundleForLocale(locale);
    assert(_translationBundle != null);
85 86 87 88 89

    const String kMediumDatePattern = 'E, MMM\u00a0d';
    if (intl.DateFormat.localeExists(_localeName)) {
      _fullYearFormat = new intl.DateFormat.y(_localeName);
      _mediumDateFormat = new intl.DateFormat(kMediumDatePattern, _localeName);
90
      _longDateFormat = new intl.DateFormat.yMMMMEEEEd(_localeName);
91 92 93 94
      _yearMonthFormat = new intl.DateFormat('yMMMM', _localeName);
    } else if (intl.DateFormat.localeExists(locale.languageCode)) {
      _fullYearFormat = new intl.DateFormat.y(locale.languageCode);
      _mediumDateFormat = new intl.DateFormat(kMediumDatePattern, locale.languageCode);
95 96

      _longDateFormat = new intl.DateFormat.yMMMMEEEEd(locale.languageCode);
97 98 99 100
      _yearMonthFormat = new intl.DateFormat('yMMMM', locale.languageCode);
    } else {
      _fullYearFormat = new intl.DateFormat.y();
      _mediumDateFormat = new intl.DateFormat(kMediumDatePattern);
101
      _longDateFormat = new intl.DateFormat.yMMMMEEEEd();
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
      _yearMonthFormat = new intl.DateFormat('yMMMM');
    }

    if (intl.NumberFormat.localeExists(_localeName)) {
      _decimalFormat = new intl.NumberFormat.decimalPattern(_localeName);
      _twoDigitZeroPaddedFormat = new intl.NumberFormat('00', _localeName);
    } else if (intl.NumberFormat.localeExists(locale.languageCode)) {
      _decimalFormat = new intl.NumberFormat.decimalPattern(locale.languageCode);
      _twoDigitZeroPaddedFormat = new intl.NumberFormat('00', locale.languageCode);
    } else {
      _decimalFormat = new intl.NumberFormat.decimalPattern();
      _twoDigitZeroPaddedFormat = new intl.NumberFormat('00');
    }
  }

  /// The locale for which the values of this class's localized resources
  /// have been translated.
  final Locale locale;

  final String _localeName;

123
  TranslationBundle _translationBundle;
124 125 126 127 128 129 130 131 132

  intl.NumberFormat _decimalFormat;

  intl.NumberFormat _twoDigitZeroPaddedFormat;

  intl.DateFormat _fullYearFormat;

  intl.DateFormat _mediumDateFormat;

133 134
  intl.DateFormat _longDateFormat;

135 136 137
  intl.DateFormat _yearMonthFormat;

  static String _computeLocaleName(Locale locale) {
138
    return intl.Intl.canonicalizedLocale(locale.toString());
139 140 141
  }

  @override
142
  String formatHour(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat = false }) {
143
    switch (hourFormat(of: timeOfDayFormat(alwaysUse24HourFormat: alwaysUse24HourFormat))) {
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
      case HourFormat.HH:
        return _twoDigitZeroPaddedFormat.format(timeOfDay.hour);
      case HourFormat.H:
        return formatDecimal(timeOfDay.hour);
      case HourFormat.h:
        final int hour = timeOfDay.hourOfPeriod;
        return formatDecimal(hour == 0 ? 12 : hour);
    }
    return null;
  }

  @override
  String formatMinute(TimeOfDay timeOfDay) {
    return _twoDigitZeroPaddedFormat.format(timeOfDay.minute);
  }

  @override
  String formatYear(DateTime date) {
    return _fullYearFormat.format(date);
  }

  @override
  String formatMediumDate(DateTime date) {
    return _mediumDateFormat.format(date);
  }

170 171 172 173 174
  @override
  String formatFullDate(DateTime date) {
    return _longDateFormat.format(date);
  }

175 176 177 178 179 180 181 182 183 184 185 186 187
  @override
  String formatMonthYear(DateTime date) {
    return _yearMonthFormat.format(date);
  }

  @override
  List<String> get narrowWeekdays {
    return _fullYearFormat.dateSymbols.NARROWWEEKDAYS;
  }

  @override
  int get firstDayOfWeekIndex => (_fullYearFormat.dateSymbols.FIRSTDAYOFWEEK + 1) % 7;

188
  @override
189 190 191 192 193
  String formatDecimal(int number) {
    return _decimalFormat.format(number);
  }

  @override
194
  String formatTimeOfDay(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat = false }) {
195 196 197 198 199 200 201 202
    // Not using intl.DateFormat for two reasons:
    //
    // - DateFormat supports more formats than our material time picker does,
    //   and we want to be consistent across time picker format and the string
    //   formatting of the time of day.
    // - DateFormat operates on DateTime, which is sensitive to time eras and
    //   time zones, while here we want to format hour and minute within one day
    //   no matter what date the day falls on.
203 204 205
    final String hour = formatHour(timeOfDay, alwaysUse24HourFormat: alwaysUse24HourFormat);
    final String minute = formatMinute(timeOfDay);
    switch (timeOfDayFormat(alwaysUse24HourFormat: alwaysUse24HourFormat)) {
206
      case TimeOfDayFormat.h_colon_mm_space_a:
207
        return '$hour:$minute ${_formatDayPeriod(timeOfDay)}';
208 209
      case TimeOfDayFormat.H_colon_mm:
      case TimeOfDayFormat.HH_colon_mm:
210
        return '$hour:$minute';
211
      case TimeOfDayFormat.HH_dot_mm:
212
        return '$hour.$minute';
213
      case TimeOfDayFormat.a_space_h_colon_mm:
214
        return '${_formatDayPeriod(timeOfDay)} $hour:$minute';
215
      case TimeOfDayFormat.frenchCanadian:
216
        return '$hour h $minute';
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
    }

    return null;
  }

  String _formatDayPeriod(TimeOfDay timeOfDay) {
    switch (timeOfDay.period) {
      case DayPeriod.am:
        return anteMeridiemAbbreviation;
      case DayPeriod.pm:
        return postMeridiemAbbreviation;
    }
    return null;
  }

  @override
233
  String get openAppDrawerTooltip => _translationBundle.openAppDrawerTooltip;
234 235

  @override
236
  String get backButtonTooltip => _translationBundle.backButtonTooltip;
237 238

  @override
239
  String get closeButtonTooltip => _translationBundle.closeButtonTooltip;
240

241
  @override
242
  String get deleteButtonTooltip => _translationBundle.deleteButtonTooltip;
243

244
  @override
245
  String get nextMonthTooltip => _translationBundle.nextMonthTooltip;
246 247

  @override
248
  String get previousMonthTooltip => _translationBundle.previousMonthTooltip;
249 250

  @override
251
  String get nextPageTooltip => _translationBundle.nextPageTooltip;
252 253

  @override
254
  String get previousPageTooltip => _translationBundle.previousPageTooltip;
255 256

  @override
257
  String get showMenuTooltip => _translationBundle.showMenuTooltip;
258

259 260 261 262 263 264 265 266 267 268 269 270
  @override
  String get drawerLabel => _translationBundle.alertDialogLabel;

  @override
  String get popupMenuLabel => _translationBundle.popupMenuLabel;

  @override
  String get dialogLabel => _translationBundle.dialogLabel;

  @override
  String get alertDialogLabel => _translationBundle.alertDialogLabel;

271 272 273
  @override
  String get searchFieldLabel => _translationBundle.searchFieldLabel;

274 275
  @override
  String aboutListTileTitle(String applicationName) {
276
    final String text = _translationBundle.aboutListTileTitle;
277 278 279 280
    return text.replaceFirst(r'$applicationName', applicationName);
  }

  @override
281
  String get licensesPageTitle => _translationBundle.licensesPageTitle;
282 283 284

  @override
  String pageRowsInfoTitle(int firstRow, int lastRow, int rowCount, bool rowCountIsApproximate) {
285 286
    String text = rowCountIsApproximate ? _translationBundle.pageRowsInfoTitleApproximate : null;
    text ??= _translationBundle.pageRowsInfoTitle;
287 288 289 290 291 292 293 294 295
    assert(text != null, 'A $locale localization was not found for pageRowsInfoTitle or pageRowsInfoTitleApproximate');
    // TODO(hansmuller): this could be more efficient.
    return text
      .replaceFirst(r'$firstRow', formatDecimal(firstRow))
      .replaceFirst(r'$lastRow', formatDecimal(lastRow))
      .replaceFirst(r'$rowCount', formatDecimal(rowCount));
  }

  @override
296
  String get rowsPerPageTitle => _translationBundle.rowsPerPageTitle;
297

298 299 300 301 302 303 304
  @override
  String tabLabel({int tabIndex, int tabCount}) {
    assert(tabIndex >= 1);
    assert(tabCount >= 1);
    final String template = _translationBundle.tabLabel;
    return template
      .replaceFirst(r'$tabIndex', formatDecimal(tabIndex))
305
      .replaceFirst(r'$tabCount', formatDecimal(tabCount));
306 307
  }

308 309
  @override
  String selectedRowCountTitle(int selectedRowCount) {
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
    // TODO(hmuller): the rules for mapping from an integer value to
    // "one" or "two" etc. are locale specific and an additional "few" category
    // is needed. See http://cldr.unicode.org/index/cldr-spec/plural-rules
    String text;
    if (selectedRowCount == 0)
      text = _translationBundle.selectedRowCountTitleZero;
    else if (selectedRowCount == 1)
      text = _translationBundle.selectedRowCountTitleOne;
    else if (selectedRowCount == 2)
      text = _translationBundle.selectedRowCountTitleTwo;
    else if (selectedRowCount > 2)
      text = _translationBundle.selectedRowCountTitleMany;
    text ??= _translationBundle.selectedRowCountTitleOther;
    assert(text != null);

    return text.replaceFirst(r'$selectedRowCount', formatDecimal(selectedRowCount));
326 327 328
  }

  @override
329
  String get cancelButtonLabel => _translationBundle.cancelButtonLabel;
330 331

  @override
332
  String get closeButtonLabel => _translationBundle.closeButtonLabel;
333 334

  @override
335
  String get continueButtonLabel => _translationBundle.continueButtonLabel;
336 337

  @override
338
  String get copyButtonLabel => _translationBundle.copyButtonLabel;
339 340

  @override
341
  String get cutButtonLabel => _translationBundle.cutButtonLabel;
342 343

  @override
344
  String get okButtonLabel => _translationBundle.okButtonLabel;
345 346

  @override
347
  String get pasteButtonLabel => _translationBundle.pasteButtonLabel;
348 349

  @override
350
  String get selectAllButtonLabel => _translationBundle.selectAllButtonLabel;
351 352

  @override
353
  String get viewLicensesButtonLabel => _translationBundle.viewLicensesButtonLabel;
354 355

  @override
356
  String get anteMeridiemAbbreviation => _translationBundle.anteMeridiemAbbreviation;
357 358

  @override
359
  String get postMeridiemAbbreviation => _translationBundle.postMeridiemAbbreviation;
360

361
  @override
362
  String get timePickerHourModeAnnouncement => _translationBundle.timePickerHourModeAnnouncement;
363 364

  @override
365
  String get timePickerMinuteModeAnnouncement => _translationBundle.timePickerMinuteModeAnnouncement;
366

367
  @override
368
  String get modalBarrierDismissLabel => _translationBundle.modalBarrierDismissLabel;
369

370 371 372 373 374 375 376 377 378
  @override
  String get signedInLabel => _translationBundle.signedInLabel;

  @override
  String get hideAccountsLabel => _translationBundle.hideAccountsLabel;

  @override
  String get showAccountsLabel => _translationBundle.showAccountsLabel;

379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395
  /// The [TimeOfDayFormat] corresponding to one of the following supported
  /// patterns:
  ///
  ///  * `HH:mm`
  ///  * `HH.mm`
  ///  * `HH 'h' mm`
  ///  * `HH:mm น.`
  ///  * `H:mm`
  ///  * `h:mm a`
  ///  * `a h:mm`
  ///  * `ah:mm`
  ///
  /// See also:
  ///
  ///  * http://demo.icu-project.org/icu-bin/locexp?d_=en&_=en_US shows the
  ///    short time pattern used in locale en_US
  @override
396
  TimeOfDayFormat timeOfDayFormat({ bool alwaysUse24HourFormat = false }) {
397
    final String icuShortTimePattern = _translationBundle.timeOfDayFormat;
398 399 400 401 402 403 404 405 406 407 408 409 410

    assert(() {
      if (!_icuTimeOfDayToEnum.containsKey(icuShortTimePattern)) {
        throw new FlutterError(
          '"$icuShortTimePattern" is not one of the ICU short time patterns '
          'supported by the material library. Here is the list of supported '
          'patterns:\n  ' +
          _icuTimeOfDayToEnum.keys.join('\n  ')
        );
      }
      return true;
    }());

411 412 413 414 415 416
    final TimeOfDayFormat icuFormat = _icuTimeOfDayToEnum[icuShortTimePattern];

    if (alwaysUse24HourFormat)
      return _get24HourVersionOf(icuFormat);

    return icuFormat;
417 418 419 420
  }

  /// Looks up text geometry defined in [MaterialTextGeometry].
  @override
421
  TextTheme get localTextGeometry => MaterialTextGeometry.forScriptCategory(_translationBundle.scriptCategory);
422 423 424 425 426 427 428 429 430 431 432 433 434

  /// Creates an object that provides localized resource values for the
  /// for the widgets of the material library.
  ///
  /// This method is typically used to create a [LocalizationsDelegate].
  /// The [MaterialApp] does so by default.
  static Future<MaterialLocalizations> load(Locale locale) {
    return new SynchronousFuture<MaterialLocalizations>(new GlobalMaterialLocalizations(locale));
  }

  /// A [LocalizationsDelegate] that uses [GlobalMaterialLocalizations.load]
  /// to create an instance of this class.
  ///
435
  /// Most internationalized apps will use [GlobalMaterialLocalizations.delegates]
436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475
  /// as the value of [MaterialApp.localizationsDelegates] to include
  /// the localizations for both the material and widget libraries.
  static const LocalizationsDelegate<MaterialLocalizations> delegate = const _MaterialLocalizationsDelegate();

  /// A value for [MaterialApp.localizationsDelegates] that's typically used by
  /// internationalized apps.
  ///
  /// To include the localizations provided by this class and by
  /// [GlobalWidgetsLocalizations] in a [MaterialApp],
  /// use [GlobalMaterialLocalizations.delegates] as the value of
  /// [MaterialApp.localizationsDelegates], and specify the locales your
  /// app supports with [MaterialApp.supportedLocales]:
  ///
  /// ```dart
  /// new MaterialApp(
  ///   localizationsDelegates: GlobalMaterialLocalizations.delegates,
  ///   supportedLocales: [
  ///     const Locale('en', 'US'), // English
  ///     const Locale('he', 'IL'), // Hebrew
  ///   ],
  ///   // ...
  /// )
  /// ```
  static const List<LocalizationsDelegate<dynamic>> delegates = const <LocalizationsDelegate<dynamic>>[
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
  ];
}

const Map<String, TimeOfDayFormat> _icuTimeOfDayToEnum = const <String, TimeOfDayFormat>{
  'HH:mm': TimeOfDayFormat.HH_colon_mm,
  'HH.mm': TimeOfDayFormat.HH_dot_mm,
  "HH 'h' mm": TimeOfDayFormat.frenchCanadian,
  'HH:mm น.': TimeOfDayFormat.HH_colon_mm,
  'H:mm': TimeOfDayFormat.H_colon_mm,
  'h:mm a': TimeOfDayFormat.h_colon_mm_space_a,
  'a h:mm': TimeOfDayFormat.a_space_h_colon_mm,
  'ah:mm': TimeOfDayFormat.a_space_h_colon_mm,
};

476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492
/// Finds the [TimeOfDayFormat] to use instead of the `original` when the
/// `original` uses 12-hour format and [MediaQueryData.alwaysUse24HourFormat]
/// is true.
TimeOfDayFormat _get24HourVersionOf(TimeOfDayFormat original) {
  switch (original) {
    case TimeOfDayFormat.HH_colon_mm:
    case TimeOfDayFormat.HH_dot_mm:
    case TimeOfDayFormat.frenchCanadian:
    case TimeOfDayFormat.H_colon_mm:
      return original;
    case TimeOfDayFormat.h_colon_mm_space_a:
    case TimeOfDayFormat.a_space_h_colon_mm:
      return TimeOfDayFormat.HH_colon_mm;
  }
  return TimeOfDayFormat.HH_colon_mm;
}

493 494 495 496 497 498 499 500 501
/// Tracks if date i18n data has been loaded.
bool _dateIntlDataInitialized = false;

/// Loads i18n data for dates if it hasn't be loaded yet.
///
/// Only the first invocation of this function has the effect of loading the
/// data. Subsequent invocations have no effect.
void _loadDateIntlDataIfNotLoaded() {
  if (!_dateIntlDataInitialized) {
502 503 504 505 506 507 508 509
    date_localizations.dateSymbols.forEach((String locale, dynamic data) {
      assert(date_localizations.datePatterns.containsKey(locale));
      final intl.DateSymbols symbols = new intl.DateSymbols.deserializeFromMap(data);
      date_symbol_data_custom.initializeDateFormattingCustom(
        locale: locale,
        symbols: symbols,
        patterns: date_localizations.datePatterns[locale],
      );
510
    });
511 512 513 514 515 516 517
    _dateIntlDataInitialized = true;
  }
}

class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
  const _MaterialLocalizationsDelegate();

518 519
  // Watch out: this list must match the one in the GlobalMaterialLocalizations
  // class doc and the list we test, see test/translations_test.dart.
520
  static const List<String> _supportedLanguages = const <String>[
521 522 523 524
    'ar', // Arabic
    'de', // German
    'en', // English
    'es', // Spanish
525
    'fa', // Farsi (Persian)
526 527
    'fr', // French
    'he', // Hebrew
528
    'id', // Indonesian
529 530
    'it', // Italian
    'ja', // Japanese
531
    'ko', // Korean
532
    'ms', // Malay
533
    'nl', // Dutch
534
    'nb', // Norwegian
535
    'pl', // Polish
536 537 538 539
    'ps', // Pashto
    'pt', // Portugese
    'ro', // Romanian
    'ru', // Russian
540 541
    'th', // Thai
    'tr', // Turkish
542
    'ur', // Urdu
543
    'vi', // Vietnamese
544
    'zh', // Chinese (simplified)
545 546 547 548 549
  ];

  @override
  bool isSupported(Locale locale) => _supportedLanguages.contains(locale.languageCode);

550 551 552 553 554 555
  @override
  Future<MaterialLocalizations> load(Locale locale) => GlobalMaterialLocalizations.load(locale);

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