blob: ed3dea32b4a325f04894f634183f1b18bf0b9bb1 [file] [log] [blame]
// 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) {
// Debug-mode asserts to ensure all parameters are normalized and UTS #35
// compliant.
assert(
languageCode != null && _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(
{String languageCode, String scriptCode, String countryCode}) {
return LocaleImplementation._(
replaceDeprecatedLanguageSubtag(_normalizeLanguageCode(languageCode)),
_normalizeScriptCode(scriptCode),
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 (scriptCode == null) return null;
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 (countryCode == null) return null;
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.
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.
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.
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.
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.
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()}';