| // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:intl/src/locale.dart' show Locale; |
| |
| import 'locale_deprecations.dart'; |
| import 'locale_extensions.dart'; |
| |
| /// The primary implementation of the Locale interface. |
| class LocaleImplementation extends Locale { |
| /// Simple private constructor with asserts to check invariants. |
| LocaleImplementation._(this.languageCode, this.scriptCode, this.countryCode, |
| this.variants, this._extensions) { |
| ArgumentError.notNull(languageCode); |
| // Debug-mode asserts to ensure all parameters are normalized and UTS #35 |
| // compliant. |
| assert( |
| _normalizedLanguageRE.hasMatch(languageCode), |
| 'languageCode must match RegExp/${_normalizedLanguageRE.pattern}/ ' |
| 'but is "$languageCode".'); |
| assert( |
| scriptCode == null || _normalizedScriptRE.hasMatch(scriptCode!), |
| 'scriptCode must match RegExp/${_normalizedScriptRE.pattern}/ ' |
| 'but is "$scriptCode".'); |
| assert( |
| countryCode == null || _normalizedRegionRE.hasMatch(countryCode!), |
| 'countryCode must match RegExp/${_normalizedRegionRE.pattern}/ ' |
| 'but is "$countryCode".'); |
| assert( |
| variants is List<String> && |
| variants.every(_normalizedVariantRE.hasMatch), |
| 'each variant must match RegExp/${_normalizedVariantRE.pattern}/ ' |
| 'but variants are "$variants".'); |
| } |
| |
| /// For debug/assert-use only! Matches subtags considered valid for |
| /// [languageCode], does not imply subtag is valid as per Unicode LDML spec! |
| // |
| // Must be static to get tree-shaken away in production code. |
| static final _normalizedLanguageRE = RegExp(r'^[a-z]{2,3}$|^[a-z]{5,8}$'); |
| |
| /// For debug/assert-use only! Matches subtags considered valid for |
| /// [scriptCode], does not imply subtag is valid as per Unicode LDML spec! |
| // |
| // Must be static to get tree-shaken away in production code. |
| static final _normalizedScriptRE = RegExp(r'^[A-Z][a-z]{3}$'); |
| |
| /// For debug/assert-use only! Matches subtags considered valid for |
| /// [countryCode], does not imply subtags are valid as per Unicode LDML spec! |
| // |
| // Must be static to get tree-shaken away in production code. |
| static final _normalizedRegionRE = RegExp(r'^[A-Z]{2}$|^\d{3}$'); |
| |
| /// For debug/assert-use only! Matches subtags considered valid for |
| /// [variants], does not imply subtags are valid as per Unicode LDML spec! |
| // |
| // Must be static to get tree-shaken away in production code. |
| static final _normalizedVariantRE = RegExp(r'^[a-z\d]{5,8}$|^\d[a-z\d]{3}$'); |
| |
| /// Simple factory which assumes parameters are syntactically correct. |
| /// |
| /// In debug mode, incorrect use may result in an assertion failure. (In |
| /// production code, this class makes no promises regarding the consequence of |
| /// incorrect use.) |
| /// |
| /// For public APIs, see [Locale.fromSubtags] and [Locale.parse]. |
| factory LocaleImplementation.unsafe( |
| String languageCode, { |
| String? scriptCode, |
| String? countryCode, |
| Iterable<String>? variants, |
| LocaleExtensions? extensions, |
| }) { |
| variants = (variants != null && variants.isNotEmpty) |
| ? List.unmodifiable(variants.toList()..sort()) |
| : const []; |
| return LocaleImplementation._( |
| languageCode, scriptCode, countryCode, variants, extensions); |
| } |
| |
| /// Constructs a Locale instance that consists of only language, region and |
| /// country subtags. |
| /// |
| /// Throws a [FormatException] if any subtag is syntactically invalid. |
| static LocaleImplementation fromSubtags( |
| {required String languageCode, String? scriptCode, String? countryCode}) { |
| return LocaleImplementation._( |
| replaceDeprecatedLanguageSubtag(_normalizeLanguageCode(languageCode)), |
| scriptCode == null ? null : _normalizeScriptCode(scriptCode), |
| countryCode == null |
| ? null |
| : replaceDeprecatedRegionSubtag(_normalizeCountryCode(countryCode)), |
| const [], |
| null); |
| } |
| |
| /// Performs case normalization on `languageCode`. |
| /// |
| /// Throws a [FormatException] if it is syntactically invalid. |
| static String _normalizeLanguageCode(String languageCode) { |
| if (!_languageRexExp.hasMatch(languageCode)) { |
| throw FormatException('Invalid language "$languageCode"'); |
| } |
| return languageCode.toLowerCase(); |
| } |
| |
| static final _languageRexExp = RegExp(r'^[a-zA-Z]{2,3}$|^[a-zA-Z]{5,8}$'); |
| |
| /// Performs case normalization on `scriptCode`. |
| /// |
| /// Throws a [FormatException] if it is syntactically invalid. |
| static String _normalizeScriptCode(String scriptCode) { |
| if (!_scriptRegExp.hasMatch(scriptCode)) { |
| throw FormatException('Invalid script "$scriptCode"'); |
| } |
| return toCapCase(scriptCode); |
| } |
| |
| static final _scriptRegExp = RegExp(r'^[a-zA-Z]{4}$'); |
| |
| /// Performs case normalization on `countryCode`. |
| /// |
| /// Throws a [FormatException] if it is syntactically invalid. |
| static String _normalizeCountryCode(String countryCode) { |
| if (!_regionRegExp.hasMatch(countryCode)) { |
| throw FormatException('Invalid region "$countryCode"'); |
| } |
| return countryCode.toUpperCase(); |
| } |
| |
| static final _regionRegExp = RegExp(r'^[a-zA-Z]{2}$|^\d{3}$'); |
| |
| /// The language subtag of the Locale Identifier. |
| /// |
| /// It is syntactically valid, normalized (has correct case) and canonical |
| /// (deprecated tags have been replaced), but not necessarily valid (the |
| /// language might not exist) because the list of valid languages changes with |
| /// time. |
| @override |
| final String languageCode; |
| |
| /// The script subtag of the Locale Identifier, null if absent. |
| /// |
| /// It is syntactically valid, normalized (has correct case) and canonical |
| /// (deprecated tags have been replaced), but not necessarily valid (the |
| /// script might not exist) because the list of valid scripts changes with |
| /// time. |
| @override |
| final String? scriptCode; |
| |
| /// The region subtag of the Locale Identifier, null if absent. |
| /// |
| /// It is syntactically valid, normalized (has correct case) and canonical |
| /// (deprecated tags have been replaced), but not necessarily valid (the |
| /// region might not exist) because the list of valid regions changes with |
| /// time. |
| @override |
| final String? countryCode; |
| |
| /// Iterable of variant subtags, zero-length iterable if variants are absent. |
| /// |
| /// They are syntactically valid, normalized (have correct case) and canonical |
| /// (sorted alphabetically and deprecated tags have been replaced) but not |
| /// necessarily valid (variants might not exist) because the list of variants |
| /// changes with time. |
| @override |
| final Iterable<String> variants; |
| |
| /// Locale extensions, null if the locale has no extensions. |
| // TODO(hugovdm): Not yet supported: getters for extensions. |
| final LocaleExtensions? _extensions; |
| |
| /// Cache of the value returned by [toLanguageTag]. |
| String? _languageTag; |
| |
| /// Returns the canonical Unicode BCP47 Locale Identifier for this locale. |
| @override |
| String toLanguageTag() { |
| if (_languageTag == null) { |
| final out = [languageCode]; |
| if (scriptCode != null) out.add(scriptCode!); |
| if (countryCode != null) out.add(countryCode!); |
| out.addAll(variants); |
| if (_extensions != null) out.addAll(_extensions!.subtags); |
| _languageTag = out.join('-'); |
| } |
| return _languageTag!; |
| } |
| } |
| |
| /// Returns `input` with first letter capitalized and the rest lowercase. |
| String toCapCase(String input) => |
| '${input[0].toUpperCase()}${input.substring(1).toLowerCase()}'; |