// Copyright (c) 2012, 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. /// A library for general helper code associated with the intl library /// rather than confined to specific parts of it. library; import 'global_state.dart' as global_state; /// Type for the callback action when a message translation is not found. typedef MessageIfAbsent = String? Function( String? messageText, List? args); /// This is used as a marker for a locale data map that hasn't been initialized, /// and will throw an exception on any usage that isn't the fallback /// patterns/symbols provided. class UninitializedLocaleData implements MessageLookup { final String message; final F fallbackData; UninitializedLocaleData(this.message, this.fallbackData); bool _isFallback(String key) => canonicalizedLocale(key) == 'en_US'; F operator [](String key) => _isFallback(key) ? fallbackData : _throwException(); /// If a message is looked up before any locale initialization, record it, /// and throw an exception with that information once the locale is /// initialized. /// /// Set this during development to find issues with race conditions between /// message caching and locale initialization. If the results of Intl.message /// calls aren't being cached, then this won't help. /// /// There's nothing that actually sets this, so checking this requires /// patching the code here. static final bool throwOnFallback = false; /// The messages that were called before the locale was initialized. final List _badMessages = []; void _reportErrors() { if (throwOnFallback && _badMessages.isNotEmpty) { throw StateError( 'The following messages were called before locale initialization:' ' $_uninitializedMessages'); } } String get _uninitializedMessages => (_badMessages.toSet().toList()..sort()).join('\n '); @override String? lookupMessage(String? messageText, String? locale, String? name, List? args, String? meaning, {MessageIfAbsent? ifAbsent}) { if (throwOnFallback) { _badMessages.add((name ?? messageText)!); } return messageText; } /// Given an initial locale or null, returns the locale that will be used /// for messages. String findLocale(String? locale) => locale ?? global_state.getCurrentLocale(); List get keys => _throwException() as List; bool containsKey(String key) { if (!_isFallback(key)) { _throwException(); } return true; } F _throwException() { throw LocaleDataException('Locale data has not been initialized' ', call $message.'); } @override void addLocale(String localeName, Function findLocale) => _throwException(); } abstract class MessageLookup { String? lookupMessage(String? messageText, String? locale, String? name, List? args, String? meaning, {MessageIfAbsent? ifAbsent}); void addLocale(String localeName, Function findLocale); } class LocaleDataException implements Exception { final String message; LocaleDataException(this.message); @override String toString() => 'LocaleDataException: $message'; } /// An abstract superclass for data readers to keep the type system happy. abstract class LocaleDataReader { Future read(String locale); } /// The internal mechanism for looking up messages. We expect this to be set /// by the implementing package so that we're not dependent on its /// implementation. MessageLookup messageLookup = UninitializedLocaleData('initializeMessages()', null); /// Initialize the message lookup mechanism. This is for internal use only. /// User applications should import `message_lookup_by_library.dart` and call /// `initializeMessages` void initializeInternalMessageLookup(Function lookupFunction) { if (messageLookup is UninitializedLocaleData) { // This line has to be precisely this way to work around an analyzer crash. (messageLookup as UninitializedLocaleData)._reportErrors(); messageLookup = lookupFunction(); } } /// If a message is a string literal without interpolation, compute /// a name based on that and the meaning, if present. // NOTE: THIS LOGIC IS DUPLICATED IN intl_translation AND THE TWO MUST MATCH. String? computeMessageName(String? name, String? text, String? meaning) { if (name != null && name != '') return name; return meaning == null ? text : '${text}_$meaning'; } /// Returns an index of a separator between language and region. /// /// Assumes that language length can be only 2 or 3. int _separatorIndex(String locale) { if (locale.length < 3) { return -1; } if (locale[2] == '-' || locale[2] == '_') { return 2; } if (locale.length < 4) { return -1; } if (locale[3] == '-' || locale[3] == '_') { return 3; } return -1; } String canonicalizedLocale(String? aLocale) { // Locales of length < 5 are presumably two-letter forms, or else malformed. // We return them unmodified and if correct they will be found. // Locales longer than 6 might be malformed, but also do occur. Do as // little as possible to them, but make the '-' be an '_' if it's there. // We treat C as a special case, and assume it wants en_ISO for formatting. // TODO(alanknight): en_ISO is probably not quite right for the C/Posix // locale for formatting. Consider adding C to the formats database. if (aLocale == null) return global_state.getCurrentLocale(); if (aLocale == 'C') return 'en_ISO'; if (aLocale.length < 5) return aLocale; var separatorIndex = _separatorIndex(aLocale); if (separatorIndex == -1) { return aLocale; } var language = aLocale.substring(0, separatorIndex); var region = aLocale.substring(separatorIndex + 1); // If it's longer than three it's something odd, so don't touch it. if (region.length <= 3) region = region.toUpperCase(); return '${language}_$region'; } String? verifiedLocale(String? newLocale, bool Function(String) localeExists, String? Function(String)? onFailure) { // TODO(alanknight): Previously we kept a single verified locale on the Intl // object, but with different verification for different uses, that's more // difficult. As a result, we call this more often. Consider keeping // verified locales for each purpose if it turns out to be a performance // issue. if (newLocale == null) { return verifiedLocale( global_state.getCurrentLocale(), localeExists, onFailure); } if (localeExists(newLocale)) { return newLocale; } final fallbackOptions = [ canonicalizedLocale, shortLocale, deprecatedLocale, (locale) => deprecatedLocale(shortLocale(locale)), (locale) => deprecatedLocale(canonicalizedLocale(locale)), (_) => 'fallback' ]; for (var option in fallbackOptions) { var localeFallback = option(newLocale); if (localeExists(localeFallback)) { return localeFallback; } } return (onFailure ?? _throwLocaleError)(newLocale); } /// The default action if a locale isn't found in verifiedLocale. Throw /// an exception indicating the locale isn't correct. String _throwLocaleError(String localeName) { throw ArgumentError('Invalid locale "$localeName"'); } /// Return the other code for a current-deprecated locale pair. This helps in /// situations where, for example, the user has a `he.arb` file, but gets passed /// the `iw` locale code. String deprecatedLocale(String aLocale) { switch (aLocale) { case 'iw': return 'he'; case 'he': return 'iw'; case 'fil': return 'tl'; case 'tl': return 'fil'; case 'id': return 'in'; case 'in': return 'id'; case 'no': return 'nb'; case 'nb': return 'no'; } return aLocale; } /// Return the short version of a locale name, e.g. 'en_US' => 'en' String shortLocale(String aLocale) { // TODO(b/241094372): Remove this check. if (aLocale == 'invalid') { return 'in'; } if (aLocale.length < 2) { return aLocale; } var separatorIndex = _separatorIndex(aLocale); if (separatorIndex == -1) { if (aLocale.length < 4) { // aLocale is already only a language code. return aLocale.toLowerCase(); } else { // Something weird, returning as is. return aLocale; } } return aLocale.substring(0, separatorIndex).toLowerCase(); }