cupertino_localizations.dart 20.2 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 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart' as intl;

9
import 'l10n/generated_cupertino_localizations.dart';
10 11 12 13 14 15 16
import 'utils/date_localizations.dart' as util;
import 'widgets_localizations.dart';

/// Implementation of localized strings for Cupertino widgets using the `intl`
/// package for date and time formatting.
///
/// Further localization of strings beyond date time formatting are provided
17 18 19 20 21 22 23 24
/// by language specific subclasses of [GlobalCupertinoLocalizations].
///
/// ## Supported languages
///
/// This class supports locales with the following [Locale.languageCode]s:
///
/// {@macro flutter.localizations.cupertino.languages}
///
25
/// This list is available programmatically via [kCupertinoSupportedLanguages].
26 27 28 29 30 31 32 33 34
///
/// ## Sample code
///
/// To include the localizations provided by this class in a [CupertinoApp],
/// add [GlobalCupertinoLocalizations.delegates] to
/// [CupertinoApp.localizationsDelegates], and specify the locales your
/// app supports with [CupertinoApp.supportedLocales]:
///
/// ```dart
35
/// CupertinoApp(
36 37 38 39 40 41 42 43 44
///   localizationsDelegates: GlobalCupertinoLocalizations.delegates,
///   supportedLocales: [
///     const Locale('en', 'US'), // American English
///     const Locale('he', 'IL'), // Israeli Hebrew
///     // ...
///   ],
///   // ...
/// )
/// ```
45 46 47 48 49 50 51 52 53 54 55 56
///
/// See also:
///
///  * [DefaultCupertinoLocalizations], which provides US English localizations
///    for Cupertino widgets.
abstract class GlobalCupertinoLocalizations implements CupertinoLocalizations {
  /// Initializes an object that defines the Cupertino widgets' localized
  /// strings for the given `localeName`.
  ///
  /// The remaining '*Format' arguments uses the intl package to provide
  /// [DateFormat] configurations for the `localeName`.
  const GlobalCupertinoLocalizations({
57 58 59 60 61 62 63 64 65
    required String localeName,
    required intl.DateFormat fullYearFormat,
    required intl.DateFormat dayFormat,
    required intl.DateFormat mediumDateFormat,
    required intl.DateFormat singleDigitHourFormat,
    required intl.DateFormat singleDigitMinuteFormat,
    required intl.DateFormat doubleDigitMinuteFormat,
    required intl.DateFormat singleDigitSecondFormat,
    required intl.NumberFormat decimalFormat,
66
  }) : _localeName = localeName,
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
       _fullYearFormat = fullYearFormat,
       _dayFormat = dayFormat,
       _mediumDateFormat = mediumDateFormat,
       _singleDigitHourFormat = singleDigitHourFormat,
       _singleDigitMinuteFormat = singleDigitMinuteFormat,
       _doubleDigitMinuteFormat = doubleDigitMinuteFormat,
       _singleDigitSecondFormat = singleDigitSecondFormat,
       _decimalFormat =decimalFormat;

  final String _localeName;
  final intl.DateFormat _fullYearFormat;
  final intl.DateFormat _dayFormat;
  final intl.DateFormat _mediumDateFormat;
  final intl.DateFormat _singleDigitHourFormat;
  final intl.DateFormat _singleDigitMinuteFormat;
  final intl.DateFormat _doubleDigitMinuteFormat;
  final intl.DateFormat _singleDigitSecondFormat;
  final intl.NumberFormat _decimalFormat;

  @override
  String datePickerYear(int yearIndex) {
    return _fullYearFormat.format(DateTime.utc(yearIndex));
  }

  @override
  String datePickerMonth(int monthIndex) {
    // It doesn't actually have anything to do with _fullYearFormat. It's just
    // taking advantage of the fact that _fullYearFormat loaded the needed
    // locale's symbols.
    return _fullYearFormat.dateSymbols.MONTHS[monthIndex - 1];
  }

  @override
  String datePickerDayOfMonth(int dayIndex) {
    // Year and month doesn't matter since we just want to day formatted.
    return _dayFormat.format(DateTime.utc(0, 0, dayIndex));
  }

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

  @override
  String datePickerHour(int hour) {
    return _singleDigitHourFormat.format(DateTime.utc(0, 0, 0, hour));
  }

  @override
  String datePickerMinute(int minute) {
    return _doubleDigitMinuteFormat.format(DateTime.utc(0, 0, 0, 0, minute));
  }

  /// Subclasses should provide the optional zero pluralization of [datePickerHourSemanticsLabel] based on the ARB file.
121 122
  @protected
  String? get datePickerHourSemanticsLabelZero => null;
123
  /// Subclasses should provide the optional one pluralization of [datePickerHourSemanticsLabel] based on the ARB file.
124 125
  @protected
  String? get datePickerHourSemanticsLabelOne => null;
126
  /// Subclasses should provide the optional two pluralization of [datePickerHourSemanticsLabel] based on the ARB file.
127 128
  @protected
  String? get datePickerHourSemanticsLabelTwo => null;
129
  /// Subclasses should provide the optional few pluralization of [datePickerHourSemanticsLabel] based on the ARB file.
130 131
  @protected
  String? get datePickerHourSemanticsLabelFew => null;
132
  /// Subclasses should provide the optional many pluralization of [datePickerHourSemanticsLabel] based on the ARB file.
133 134
  @protected
  String? get datePickerHourSemanticsLabelMany => null;
135
  /// Subclasses should provide the required other pluralization of [datePickerHourSemanticsLabel] based on the ARB file.
136 137
  @protected
  String? get datePickerHourSemanticsLabelOther;
138 139

  @override
140
  String? datePickerHourSemanticsLabel(int hour) {
141 142 143 144 145 146 147 148 149
    return intl.Intl.pluralLogic(
      hour,
      zero: datePickerHourSemanticsLabelZero,
      one: datePickerHourSemanticsLabelOne,
      two: datePickerHourSemanticsLabelTwo,
      few: datePickerHourSemanticsLabelFew,
      many: datePickerHourSemanticsLabelMany,
      other: datePickerHourSemanticsLabelOther,
      locale: _localeName,
150
    )?.replaceFirst(r'$hour', _decimalFormat.format(hour));
151 152 153
  }

  /// Subclasses should provide the optional zero pluralization of [datePickerMinuteSemanticsLabel] based on the ARB file.
154 155
  @protected
  String? get datePickerMinuteSemanticsLabelZero => null;
156
  /// Subclasses should provide the optional one pluralization of [datePickerMinuteSemanticsLabel] based on the ARB file.
157 158
  @protected
  String? get datePickerMinuteSemanticsLabelOne => null;
159
  /// Subclasses should provide the optional two pluralization of [datePickerMinuteSemanticsLabel] based on the ARB file.
160 161
  @protected
  String? get datePickerMinuteSemanticsLabelTwo => null;
162
  /// Subclasses should provide the optional few pluralization of [datePickerMinuteSemanticsLabel] based on the ARB file.
163 164
  @protected
  String? get datePickerMinuteSemanticsLabelFew => null;
165
  /// Subclasses should provide the optional many pluralization of [datePickerMinuteSemanticsLabel] based on the ARB file.
166 167
  @protected
  String? get datePickerMinuteSemanticsLabelMany => null;
168
  /// Subclasses should provide the required other pluralization of [datePickerMinuteSemanticsLabel] based on the ARB file.
169 170
  @protected
  String? get datePickerMinuteSemanticsLabelOther;
171 172

  @override
173
  String? datePickerMinuteSemanticsLabel(int minute) {
174 175 176 177 178 179 180 181 182
    return intl.Intl.pluralLogic(
      minute,
      zero: datePickerMinuteSemanticsLabelZero,
      one: datePickerMinuteSemanticsLabelOne,
      two: datePickerMinuteSemanticsLabelTwo,
      few: datePickerMinuteSemanticsLabelFew,
      many: datePickerMinuteSemanticsLabelMany,
      other: datePickerMinuteSemanticsLabelOther,
      locale: _localeName,
183
    )?.replaceFirst(r'$minute', _decimalFormat.format(minute));
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
  }

  /// A string describing the [DatePickerDateOrder] enum value.
  ///
  /// Subclasses should provide this string value based on the ARB file for
  /// the locale.
  ///
  /// See also:
  ///
  ///  * [datePickerDateOrder], which provides the [DatePickerDateOrder]
  ///    enum value for [CupertinoLocalizations] based on this string value
  @protected
  String get datePickerDateOrderString;

  @override
  DatePickerDateOrder get datePickerDateOrder {
    switch (datePickerDateOrderString) {
      case 'dmy':
        return DatePickerDateOrder.dmy;
      case 'mdy':
        return DatePickerDateOrder.mdy;
      case 'ymd':
        return DatePickerDateOrder.ymd;
      case 'ydm':
        return DatePickerDateOrder.ydm;
      default:
        assert(
          false,
          'Failed to load DatePickerDateOrder $datePickerDateOrderString for '
213
          "locale $_localeName.\nNon conforming string for $_localeName's "
214 215
          '.arb file',
        );
216
        return DatePickerDateOrder.mdy;
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
    }
  }

  /// A string describing the [DatePickerDateTimeOrder] enum value.
  ///
  /// Subclasses should provide this string value based on the ARB file for
  /// the locale.
  ///
  /// See also:
  ///
  ///  * [datePickerDateTimeOrder], which provides the [DatePickerDateTimeOrder]
  ///    enum value for [CupertinoLocalizations] based on this string value.
  @protected
  String get datePickerDateTimeOrderString;

  @override
  DatePickerDateTimeOrder get datePickerDateTimeOrder {
    switch (datePickerDateTimeOrderString) {
      case 'date_time_dayPeriod':
        return DatePickerDateTimeOrder.date_time_dayPeriod;
      case 'date_dayPeriod_time':
        return DatePickerDateTimeOrder.date_dayPeriod_time;
      case 'time_dayPeriod_date':
        return DatePickerDateTimeOrder.time_dayPeriod_date;
      case 'dayPeriod_time_date':
        return DatePickerDateTimeOrder.dayPeriod_time_date;
      default:
        assert(
          false,
          'Failed to load DatePickerDateTimeOrder $datePickerDateTimeOrderString '
247
          "for locale $_localeName.\nNon conforming string for $_localeName's "
248 249
          '.arb file',
        );
250
        return DatePickerDateTimeOrder.date_time_dayPeriod;
251 252 253
    }
  }

254 255 256 257 258 259
  /// The raw version of [tabSemanticsLabel], with `$tabIndex` and `$tabCount` verbatim
  /// in the string.
  @protected
  String get tabSemanticsLabelRaw;

  @override
260
  String tabSemanticsLabel({ required int tabIndex, required int tabCount }) {
261 262 263 264 265 266 267 268
    assert(tabIndex >= 1);
    assert(tabCount >= 1);
    final String template = tabSemanticsLabelRaw;
    return template
      .replaceFirst(r'$tabIndex', _decimalFormat.format(tabIndex))
      .replaceFirst(r'$tabCount', _decimalFormat.format(tabCount));
  }

269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284
  @override
  String timerPickerHour(int hour) {
    return _singleDigitHourFormat.format(DateTime.utc(0, 0, 0, hour));
  }

  @override
  String timerPickerMinute(int minute) {
    return _singleDigitMinuteFormat.format(DateTime.utc(0, 0, 0, 0, minute));
  }

  @override
  String timerPickerSecond(int second) {
    return _singleDigitSecondFormat.format(DateTime.utc(0, 0, 0, 0, 0, second));
  }

  /// Subclasses should provide the optional zero pluralization of [timerPickerHourLabel] based on the ARB file.
285 286
  @protected
  String? get timerPickerHourLabelZero => null;
287
  /// Subclasses should provide the optional one pluralization of [timerPickerHourLabel] based on the ARB file.
288 289
  @protected
  String? get timerPickerHourLabelOne => null;
290
  /// Subclasses should provide the optional two pluralization of [timerPickerHourLabel] based on the ARB file.
291 292
  @protected
  String? get timerPickerHourLabelTwo => null;
293
  /// Subclasses should provide the optional few pluralization of [timerPickerHourLabel] based on the ARB file.
294 295
  @protected
  String? get timerPickerHourLabelFew => null;
296
  /// Subclasses should provide the optional many pluralization of [timerPickerHourLabel] based on the ARB file.
297 298
  @protected
  String? get timerPickerHourLabelMany => null;
299
  /// Subclasses should provide the required other pluralization of [timerPickerHourLabel] based on the ARB file.
300 301
  @protected
  String? get timerPickerHourLabelOther;
302 303

  @override
304
  String? timerPickerHourLabel(int hour) {
305 306 307 308 309 310 311 312 313
    return intl.Intl.pluralLogic(
      hour,
      zero: timerPickerHourLabelZero,
      one: timerPickerHourLabelOne,
      two: timerPickerHourLabelTwo,
      few: timerPickerHourLabelFew,
      many: timerPickerHourLabelMany,
      other: timerPickerHourLabelOther,
      locale: _localeName,
314
    )?.replaceFirst(r'$hour', _decimalFormat.format(hour));
315 316
  }

317 318
  @override
  List<String> get timerPickerHourLabels => <String>[
319 320 321 322 323 324
    if (timerPickerHourLabelZero != null) timerPickerHourLabelZero!,
    if (timerPickerHourLabelOne != null) timerPickerHourLabelOne!,
    if (timerPickerHourLabelTwo != null) timerPickerHourLabelTwo!,
    if (timerPickerHourLabelFew != null) timerPickerHourLabelFew!,
    if (timerPickerHourLabelMany != null) timerPickerHourLabelMany!,
    if (timerPickerHourLabelOther != null) timerPickerHourLabelOther!,
325 326
  ];

327
  /// Subclasses should provide the optional zero pluralization of [timerPickerMinuteLabel] based on the ARB file.
328 329
  @protected
  String? get timerPickerMinuteLabelZero => null;
330
  /// Subclasses should provide the optional one pluralization of [timerPickerMinuteLabel] based on the ARB file.
331 332
  @protected
  String? get timerPickerMinuteLabelOne => null;
333
  /// Subclasses should provide the optional two pluralization of [timerPickerMinuteLabel] based on the ARB file.
334 335
  @protected
  String? get timerPickerMinuteLabelTwo => null;
336
  /// Subclasses should provide the optional few pluralization of [timerPickerMinuteLabel] based on the ARB file.
337 338
  @protected
  String? get timerPickerMinuteLabelFew => null;
339
  /// Subclasses should provide the optional many pluralization of [timerPickerMinuteLabel] based on the ARB file.
340 341
  @protected
  String? get timerPickerMinuteLabelMany => null;
342
  /// Subclasses should provide the required other pluralization of [timerPickerMinuteLabel] based on the ARB file.
343 344
  @protected
  String? get timerPickerMinuteLabelOther;
345 346

  @override
347
  String? timerPickerMinuteLabel(int minute) {
348 349 350 351 352 353 354 355 356
    return intl.Intl.pluralLogic(
      minute,
      zero: timerPickerMinuteLabelZero,
      one: timerPickerMinuteLabelOne,
      two: timerPickerMinuteLabelTwo,
      few: timerPickerMinuteLabelFew,
      many: timerPickerMinuteLabelMany,
      other: timerPickerMinuteLabelOther,
      locale: _localeName,
357
    )?.replaceFirst(r'$minute', _decimalFormat.format(minute));
358 359
  }

360 361
  @override
  List<String> get timerPickerMinuteLabels => <String>[
362 363 364 365 366 367
    if (timerPickerMinuteLabelZero != null) timerPickerMinuteLabelZero!,
    if (timerPickerMinuteLabelOne != null) timerPickerMinuteLabelOne!,
    if (timerPickerMinuteLabelTwo != null) timerPickerMinuteLabelTwo!,
    if (timerPickerMinuteLabelFew != null) timerPickerMinuteLabelFew!,
    if (timerPickerMinuteLabelMany != null) timerPickerMinuteLabelMany!,
    if (timerPickerMinuteLabelOther != null) timerPickerMinuteLabelOther!,
368 369
  ];

370
  /// Subclasses should provide the optional zero pluralization of [timerPickerSecondLabel] based on the ARB file.
371 372
  @protected
  String? get timerPickerSecondLabelZero => null;
373
  /// Subclasses should provide the optional one pluralization of [timerPickerSecondLabel] based on the ARB file.
374 375
  @protected
  String? get timerPickerSecondLabelOne => null;
376
  /// Subclasses should provide the optional two pluralization of [timerPickerSecondLabel] based on the ARB file.
377 378
  @protected
  String? get timerPickerSecondLabelTwo => null;
379
  /// Subclasses should provide the optional few pluralization of [timerPickerSecondLabel] based on the ARB file.
380 381
  @protected
  String? get timerPickerSecondLabelFew => null;
382
  /// Subclasses should provide the optional many pluralization of [timerPickerSecondLabel] based on the ARB file.
383 384
  @protected
  String? get timerPickerSecondLabelMany => null;
385
  /// Subclasses should provide the required other pluralization of [timerPickerSecondLabel] based on the ARB file.
386 387
  @protected
  String? get timerPickerSecondLabelOther;
388 389

  @override
390
  String? timerPickerSecondLabel(int second) {
391 392 393 394 395 396 397 398 399
    return intl.Intl.pluralLogic(
      second,
      zero: timerPickerSecondLabelZero,
      one: timerPickerSecondLabelOne,
      two: timerPickerSecondLabelTwo,
      few: timerPickerSecondLabelFew,
      many: timerPickerSecondLabelMany,
      other: timerPickerSecondLabelOther,
      locale: _localeName,
400
    )?.replaceFirst(r'$second', _decimalFormat.format(second));
401 402
  }

403 404
  @override
  List<String> get timerPickerSecondLabels => <String>[
405 406 407 408 409 410
    if (timerPickerSecondLabelZero != null) timerPickerSecondLabelZero!,
    if (timerPickerSecondLabelOne != null) timerPickerSecondLabelOne!,
    if (timerPickerSecondLabelTwo != null) timerPickerSecondLabelTwo!,
    if (timerPickerSecondLabelFew != null) timerPickerSecondLabelFew!,
    if (timerPickerSecondLabelMany != null) timerPickerSecondLabelMany!,
    if (timerPickerSecondLabelOther != null) timerPickerSecondLabelOther!,
411 412
  ];

413
  /// A [LocalizationsDelegate] for [CupertinoLocalizations].
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431
  ///
  /// Most internationalized apps will use [GlobalCupertinoLocalizations.delegates]
  /// as the value of [CupertinoApp.localizationsDelegates] to include
  /// the localizations for both the cupertino and widget libraries.
  static const LocalizationsDelegate<CupertinoLocalizations> delegate = _GlobalCupertinoLocalizationsDelegate();

  /// A value for [CupertinoApp.localizationsDelegates] that's typically used by
  /// internationalized apps.
  ///
  /// ## Sample code
  ///
  /// To include the localizations provided by this class and by
  /// [GlobalWidgetsLocalizations] in a [CupertinoApp],
  /// use [GlobalCupertinoLocalizations.delegates] as the value of
  /// [CupertinoApp.localizationsDelegates], and specify the locales your
  /// app supports with [CupertinoApp.supportedLocales]:
  ///
  /// ```dart
432
  /// CupertinoApp(
433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
  ///   localizationsDelegates: GlobalCupertinoLocalizations.delegates,
  ///   supportedLocales: [
  ///     const Locale('en', 'US'), // English
  ///     const Locale('he', 'IL'), // Hebrew
  ///   ],
  ///   // ...
  /// )
  /// ```
  static const List<LocalizationsDelegate<dynamic>> delegates = <LocalizationsDelegate<dynamic>>[
    GlobalCupertinoLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
  ];
}

class _GlobalCupertinoLocalizationsDelegate extends LocalizationsDelegate<CupertinoLocalizations> {
  const _GlobalCupertinoLocalizationsDelegate();

  @override
451
  bool isSupported(Locale locale) => kCupertinoSupportedLanguages.contains(locale.languageCode);
452 453 454 455 456

  static final Map<Locale, Future<CupertinoLocalizations>> _loadedTranslations = <Locale, Future<CupertinoLocalizations>>{};

  @override
  Future<CupertinoLocalizations> load(Locale locale) {
457
    assert(isSupported(locale));
458 459 460 461 462 463 464 465 466 467
    return _loadedTranslations.putIfAbsent(locale, () {
      util.loadDateIntlDataIfNotLoaded();

      final String localeName = intl.Intl.canonicalizedLocale(locale.toString());
      assert(
        locale.toString() == localeName,
        'Flutter does not support the non-standard locale form $locale (which '
        'might be $localeName',
      );

468 469 470
      late intl.DateFormat fullYearFormat;
      late intl.DateFormat dayFormat;
      late intl.DateFormat mediumDateFormat;
471 472
      // We don't want any additional decoration here. The am/pm is handled in
      // the date picker. We just want an hour number localized.
473 474 475 476 477
      late intl.DateFormat singleDigitHourFormat;
      late intl.DateFormat singleDigitMinuteFormat;
      late intl.DateFormat doubleDigitMinuteFormat;
      late intl.DateFormat singleDigitSecondFormat;
      late intl.NumberFormat decimalFormat;
478

479
      void loadFormats(String? locale) {
480 481 482 483 484 485 486 487
        fullYearFormat = intl.DateFormat.y(locale);
        dayFormat = intl.DateFormat.d(locale);
        mediumDateFormat = intl.DateFormat.MMMEd(locale);
        // TODO(xster): fix when https://github.com/dart-lang/intl/issues/207 is resolved.
        singleDigitHourFormat = intl.DateFormat('HH', locale);
        singleDigitMinuteFormat = intl.DateFormat.m(locale);
        doubleDigitMinuteFormat = intl.DateFormat('mm', locale);
        singleDigitSecondFormat = intl.DateFormat.s(locale);
488
        decimalFormat = intl.NumberFormat.decimalPattern(locale);
489 490 491 492 493 494 495 496 497 498
      }

      if (intl.DateFormat.localeExists(localeName)) {
        loadFormats(localeName);
      } else if (intl.DateFormat.localeExists(locale.languageCode)) {
        loadFormats(locale.languageCode);
      } else {
        loadFormats(null);
      }

499 500
      return SynchronousFuture<CupertinoLocalizations>(getCupertinoTranslation(
        locale,
501 502 503 504 505 506 507 508
        fullYearFormat,
        dayFormat,
        mediumDateFormat,
        singleDigitHourFormat,
        singleDigitMinuteFormat,
        doubleDigitMinuteFormat,
        singleDigitSecondFormat,
        decimalFormat,
509
      )!);
510 511 512 513 514 515
    });
  }

  @override
  bool shouldReload(_GlobalCupertinoLocalizationsDelegate old) => false;

516 517
  @override
  String toString() => 'GlobalCupertinoLocalizations.delegate(${kCupertinoSupportedLanguages.length} locales)';
518
}