blob: be010fade21ddb79268611e46f3d6f25af41d205 [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.
// @dart=2.9
/// `MessageFormat` is a "locale aware printf", with plural / gender support.
///
/// `MessageFormat` prepares strings for display to users, with optional
/// arguments (variables/placeholders). The arguments can occur in any order,
/// which is necessary for translation into languages with different grammars.
/// It supports syntax to represent plurals and select options.
library message_format;
import 'dart:collection';
import 'intl.dart';
/// **MessageFormat grammar:**
/// ```
/// message := messageText (argument messageText)*
/// argument := simpleArg | pluralArg | selectArg
///
/// simpleArg := "#" | "{" argNameOrNumber "}"
/// pluralArg := "{" argNameOrNumber "," "plural" "," pluralStyle "}"
/// selectArg := "{" argNameOrNumber "," "select" "," selectStyle "}"
///
/// argNameOrNumber := identifier | number
///
/// pluralStyle := [offsetValue] (pluralSelector "{" message "}")+
/// offsetValue := "offset:" number
/// pluralSelector := explicitValue | pluralKeyword
/// explicitValue := "=" number // adjacent, no white space in between
/// pluralKeyword := "zero" | "one" | "two" | "few" | "many" | "other"
///
/// selectStyle := (selectSelector "{" message "}")+
/// selectSelector := keyword
///
/// identifier := [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+
/// number := "0" | ("1".."9" ("0".."9")*)
/// ```
///
/// **NOTE:** "#" has special meaning only inside a plural block.
/// It is "connected" to the argument of the plural, but the value of #
/// is the value of the plural argument minus the offset.
///
/// **Quoting/Escaping:** if syntax characters occur in the text portions,
/// then they need to be quoted by enclosing the syntax in pairs of ASCII
/// apostrophes.
///
/// A pair of ASCII apostrophes always represents one ASCII apostrophe,
/// similar to %% in printf representing one %, although this rule still
/// applies inside quoted text.
///
/// ("This '{isn''t}' obvious" → "This {isn't} obvious")
///
/// An ASCII apostrophe only starts quoted text if it immediately precedes
/// a character that requires quoting (that is, "only where needed"), and
/// works the same in nested messages as on the top level of the pattern.
///
/// **Recommendation:** Use the real apostrophe (single quote) character ’
/// (U+2019) for human-readable text, and use the ASCII apostrophe ' (U+0027)
/// only in program syntax, like escaping.
///
/// This is a subset of the ICU MessageFormat syntax:
/// http://userguide.icu-project.org/formatparse/messages.
///
/// **Message example:**
/// ```
/// I see {NUM_PEOPLE, plural, offset:1
/// =0 {no one at all}
/// =1 {{WHO}}
/// one {{WHO} and one other person}
/// other {{WHO} and # other people}}
/// in {PLACE}.
/// ```
///
/// Calling `format({'NUM_PEOPLE': 2, 'WHO': 'Mark', 'PLACE': 'Athens'})` would
/// produce `"I see Mark and one other person in Athens."` as output.
///
/// Calling `format({'NUM_PEOPLE': 5, 'WHO': 'Mark', 'PLACE': 'Athens'})` would
/// produce `"I see Mark and one 4 other people in Athens."` as output.
/// Notice how the "#" is the value of `NUM_PEOPLE` - 1 (the offset).
///
/// Another important thing to notice is the existence of both `"=1"` and
/// `"one"`. You should think of the plural keywords as names for "buckets of
/// numbers" which have only a loose connection to the numerical value.
///
/// In English there is no difference, but for example in Russian all the
/// numbers that end with `"1"` but not with `"11"` are mapped to `"one"`
///
/// For more information please visit:
/// http://cldr.unicode.org/index/cldr-spec/plural-rules and
/// http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
// The implementation is based on the
// [Closure goog.i18n.MessageFormat](https://google.github.io/closure-library/api/goog.i18n.MessageFormat.html)
// sources at https://github.com/google/closure-library/blob/master/closure/goog/i18n/messageformat.js,
// so we should try to keep potential fixes in sync.
//
// The initial parsing done by [_extractParts] breaks the pattern into top
// level strings and {...} blocks [_ElementTypeAndVal]
//
// The values are all strings, but the ones that contain curly brackets
// are classified as `blocks` and will be parsed again (recursively)
//
// The second round of parsing takes the parts from above and refines them
// into _BlockTypeAndVal. After this point we have different types of blocks
// (string, plural, ordinal, select, ...)
class MessageFormat {
/// The locale to use for plural, ordinal, decisions,
/// number / date / time formatting
String _locale;
/// The pattern we parse and apply positional parameters to.
String _pattern;
/// All encountered literals during parse stage.
Queue<String> _initialLiterals;
/// Working list with all encountered literals during parse and format stages.
Queue<String> _literals;
/// Input pattern gets parsed into objects for faster formatting.
Queue<_BlockTypeAndVal> _parsedPattern;
/// Locale aware number formatter.
NumberFormat _numberFormat;
/// Literal strings, including '', are replaced with \uFDDF_x_ for parsing.
///
/// They are recovered during format phase.
/// \uFDDF is a Unicode nonprinting character, not expected to be found in the
/// typical message.
static const String _literalPlaceholder = '\uFDDF_';
/// Mandatory option in both select and plural form.
static const String _other = 'other';
/// Regular expression for looking for string literals.
static final RegExp _regexLiteral = RegExp("'([{}#].*?)'");
/// Regular expression for looking for '' in the message.
static final RegExp _regexDoubleApostrophe = RegExp("''");
/// Create a MessageFormat for the ICU message string [pattern].
/// It does parameter substitutions in a locale-aware way.
/// The syntax is similar to the one used by ICU and is described in the
/// grammar above.
MessageFormat(String pattern, {String locale = 'en'}) {
_locale = locale;
_pattern = pattern;
_numberFormat = NumberFormat.decimalPattern(locale);
}
/// Returns a formatted message, treating '#' as a special placeholder.
///
/// It represents the number (plural_variable - offset).
///
/// The [namedParameters] either influence the formatting or are used as
/// actual data.
/// I.e. in call to `fmt.format({'NUM_PEOPLE': 5, 'NAME': 'Angela'})`, the
/// map `{'NUM_PEOPLE': 5, 'NAME': 'Angela'}` holds parameters.
/// `NUM_PEOPLE` parameter could mean 5 people, which could influence plural
/// format, and `NAME` parameter is just a data to be printed out in proper
/// position.
String format([Map<String, Object> namedParameters]) {
return _format(false, namedParameters);
}
/// Returns a formatted message, treating '#' as literal character.
///
/// The [namedParameters] either influence the formatting or are used as
/// actual data.
/// I.e. in call to `fmt.format({'NUM_PEOPLE': 5, 'NAME': 'Angela'})`, the
/// map `{'NUM_PEOPLE': 5, 'NAME': 'Angela'}` holds positional parameters.
/// `NUM_PEOPLE` parameter could mean 5 people, which could influence plural
/// format, and `NAME` parameter is just a data to be printed out in proper
/// position.
String formatIgnoringPound([Map<String, Object> namedParameters]) {
return _format(true, namedParameters);
}
/// Returns a formatted message.
///
/// The [namedParameters] either influence the formatting or are used as
/// actual data.
/// I.e. in call to `fmt.format({'NUM_PEOPLE': 5, 'NAME': 'Angela'})`, the
/// map `{'NUM_PEOPLE': 5, 'NAME': 'Angela'}` holds positional parameters.
/// `NUM_PEOPLE` parameter could mean 5 people, which could influence plural
/// format, and `NAME` parameter is just a data to be printed out in proper
/// position.
/// If [ignorePound] is true, treat '#' in plural messages as a
/// literal character, else treat it as an ICU syntax character, resolving
/// to the number (plural_variable - offset).
String _format(bool ignorePound, [Map<String, Object> namedParameters]) {
_init();
if (_parsedPattern == null || _parsedPattern.isEmpty) {
return '';
}
// Clone, we don't want to damage the original
_literals = Queue<String>()..addAll(_initialLiterals);
// Implementation notes: this seems inefficient, we could in theory do the
// replace + join in one go.
// But would make the code even more unreadable than it is.
//
// `_formatBlock` replaces "full blocks"
// For example replaces this:
// `... {count, plural, =1 {one file} few {...} many {...} other {# files} ...`
// with
// `... one file ...`
//
// The replace after that (with `message.replaceFirst`) is only replacing
// simple parameters (`...{expDate} ... {count}...`)
//
// So `_formatBlock` is ugly, potentially recursive.
// `message.replaceFirst` is very simple, flat.
//
// I agree that there might be some performance loss.
// But in real use the messages don't have that many arguments.
// If we think printf, how many arguments are common?
// Probably less than 5 or so.
var messageParts = Queue<String>();
_formatBlock(_parsedPattern, namedParameters, ignorePound, messageParts);
var message = messageParts.join('');
if (!ignorePound) {
_checkAndThrow(!message.contains('#'), 'Not all # were replaced.');
}
while (_literals.isNotEmpty) {
message = message.replaceFirst(
_buildPlaceholder(_literals), _literals.removeLast());
}
return message;
}
/// Takes the parsed tree and the parameters, appending to result.
///
/// The [parsedBlocks] parameter holds parsed tree.
/// [namedParameters] are parameters that either influence the formatting
/// or are used as actual data.
/// If [ignorePound] is true, treat '#' in plural messages as a
/// literal character, else treat it as an ICU syntax character, resolving
/// to the number (plural_variable - offset).
/// Each formatting stage appends its product to the [result].
/// It can be recursive, as plural / select contain full message patterns.
void _formatBlock(
Queue<_BlockTypeAndVal> parsedBlocks,
Map<String, Object> namedParameters,
bool ignorePound,
Queue<String> result) {
for (var currentPattern in parsedBlocks) {
var patternValue = currentPattern._value;
var patternType = currentPattern._type;
_checkAndThrow(patternType is _BlockType,
'The type should be a block type: $patternType');
switch (patternType) {
case _BlockType.string:
result.add(patternValue);
break;
case _BlockType.simple:
_formatSimplePlaceholder(patternValue, namedParameters, result);
break;
case _BlockType.select:
_checkAndThrow(patternValue is Map<String, Object>,
'The value should be a map: $patternValue');
Map<String, Object> mapPattern = patternValue;
_formatSelectBlock(mapPattern, namedParameters, ignorePound, result);
break;
case _BlockType.plural:
_formatPluralOrdinalBlock(patternValue, namedParameters,
_PluralRules.select, ignorePound, result);
break;
case _BlockType.ordinal:
_formatPluralOrdinalBlock(patternValue, namedParameters,
_OrdinalRules.select, ignorePound, result);
break;
default:
_checkAndThrow(false, 'Unrecognized block type: $patternType');
}
}
}
/// Formats a simple placeholder.
///
/// [parsedBlocks] is an object containing placeholder info.
/// The [namedParameters] that are used as actual data.
/// Each formatting stage appends its product to the [result].
void _formatSimplePlaceholder(String parsedBlocks,
Map<String, Object> namedParameters, Queue<String> result) {
var value = namedParameters[parsedBlocks];
if (!_isDef(value)) {
result.add('Undefined parameter - $parsedBlocks');
return;
}
// Don't push the value yet, it may contain any of # { } in it which
// will break formatter. Insert a placeholder and replace at the end.
String strValue;
if (value is int) {
strValue = _numberFormat.format(value);
} else if (value is String) {
strValue = value;
} else {
strValue = value.toString();
}
_literals.add(strValue);
result.add(_buildPlaceholder(_literals));
}
/// Formats select block. Only one option is selected.
///
/// [parsedBlocks] is an object containing select block info.
/// [namedParameters] are parameters that either influence the formatting
/// or are used as actual data.
/// If [ignorePound] is true, treat '#' in plural messages as a
/// literal character, else treat it as an ICU syntax character, resolving
/// to the number (plural_variable - offset).
/// Each formatting stage appends its product to the [result].
void _formatSelectBlock(
Map<String, Object> parsedBlocks,
Map<String, Object> namedParameters,
bool ignorePound,
Queue<String> result) {
var argumentName = parsedBlocks['argumentName'];
if (!_isDef(namedParameters[argumentName])) {
result.add('Undefined parameter - $argumentName');
return;
}
var option = parsedBlocks[namedParameters[argumentName]];
if (!_isDef(option)) {
option = parsedBlocks[_other];
_checkAndThrow(option != null,
'Invalid option or missing other option for select block.');
}
_formatBlock(option, namedParameters, ignorePound, result);
}
/// Formats `plural` / `selectordinal` block, selects an option, replaces `#`
///
/// [parsedBlocks] is an object containing plural block info.
/// [namedParameters] are parameters that either influence the formatting
/// or are used as actual data.
/// The [pluralSelector] is a select function from pluralRules or ordinalRules
/// which determines which plural/ordinal form to use based on the input
/// number's cardinality.
/// If [ignorePound] is true, treat '#' in plural messages as a
/// literal character, else treat it as an ICU syntax character, resolving
/// to the number (plural_variable - offset).
/// Each formatting stage appends its product to the [result].
void _formatPluralOrdinalBlock(
Map<String, Object> parsedBlocks,
var namedParameters,
Function(num, String) pluralSelector,
bool ignorePound,
Queue<String> result) {
var argumentName = parsedBlocks['argumentName'];
var argumentOffset = parsedBlocks['argumentOffset'];
var pluralValue = namedParameters[argumentName];
if (!_isDef(pluralValue)) {
result.add('Undefined parameter - $argumentName');
return;
}
var numPluralValue =
pluralValue is num ? pluralValue : double.tryParse(pluralValue);
if (numPluralValue == null) {
result.add('Invalid parameter - $argumentName');
return;
}
var numArgumentOffset = argumentOffset is num
? argumentOffset
: double.tryParse(argumentOffset);
if (numArgumentOffset == null) {
result.add('Invalid offset - $argumentOffset');
return;
}
var diff = numPluralValue - numArgumentOffset;
// Check if there is an exact match.
var option = parsedBlocks[namedParameters[argumentName]];
if (!_isDef(option)) {
option = parsedBlocks[namedParameters[argumentName].toString()];
}
if (!_isDef(option)) {
var item = pluralSelector(diff.abs(), _locale);
_checkAndThrow(item is String, 'Invalid plural key.');
option = parsedBlocks[item];
// If option is not provided fall back to "other".
if (!_isDef(option)) {
option = parsedBlocks[_other];
}
_checkAndThrow(option != null,
'Invalid option or missing other option for plural block.');
}
var pluralResult = Queue<String>();
_formatBlock(option, namedParameters, ignorePound, pluralResult);
var plural = pluralResult.join('');
_checkAndThrow(plural is String, 'Empty block in plural.');
if (ignorePound) {
result.add(plural);
} else {
var localeAwareDiff = _numberFormat.format(diff);
result.add(plural.replaceAll('#', localeAwareDiff));
}
}
/// Set up the MessageFormat.
///
/// Parses input pattern into an array, for faster reformatting with
/// different input parameters.
/// Parsing is locale independent.
void _init() {
if (_pattern != null) {
_initialLiterals = Queue<String>();
var pattern = _insertPlaceholders(_pattern);
_parsedPattern = _parseBlock(pattern);
_pattern = null;
}
}
/// Replaces string literals with literal placeholders in [pattern].
///
/// Literals are string of the form '}...', '{...' and '#...' where ... is
/// set of characters not containing '
/// Builds a dictionary so we can recover literals during format phase.
String _insertPlaceholders(String pattern) {
var literals = _initialLiterals;
var buildPlaceholder = _buildPlaceholder;
// First replace '' with single quote placeholder since they can be found
// inside other literals.
pattern = pattern.replaceAllMapped(_regexDoubleApostrophe, (match) {
literals.add("'");
return buildPlaceholder(literals);
});
pattern = pattern.replaceAllMapped(_regexLiteral, (match) {
// match, text
var text = match.group(1);
literals.add(text);
return buildPlaceholder(literals);
});
return pattern;
}
/// Breaks [pattern] into strings and top level {...} blocks.
Queue<_ElementTypeAndVal> _extractParts(String pattern) {
var prevPos = 0;
var braceStack = Queue<String>();
var results = Queue<_ElementTypeAndVal>();
var braces = RegExp('[{}]');
Match match;
for (match in braces.allMatches(pattern)) {
var pos = match.start;
if (match[0] == '}') {
String brace;
try {
brace = braceStack.removeLast();
} on StateError {
_checkAndThrow(brace != '}', 'No matching } for {.');
}
_checkAndThrow(brace == '{', 'No matching { for }.');
if (braceStack.isEmpty) {
// End of the block.
var part = _ElementTypeAndVal(
_ElementType.block, pattern.substring(prevPos, pos));
results.add(part);
prevPos = pos + 1;
}
} else {
if (braceStack.isEmpty) {
var substring = pattern.substring(prevPos, pos);
if (substring != '') {
results.add(_ElementTypeAndVal(_ElementType.string, substring));
}
prevPos = pos + 1;
}
braceStack.add('{');
}
}
// Take care of the final string, and check if the braceStack is empty.
_checkAndThrow(
braceStack.isEmpty, 'There are mismatched { or } in the pattern.');
var substring = pattern.substring(prevPos);
if (substring != '') {
results.add(_ElementTypeAndVal(_ElementType.string, substring));
}
return results;
}
/// A regular expression to parse the plural block.
///
/// It extracts the argument index and offset (if any).
static final RegExp _pluralBlockRe =
RegExp('^\\s*(\\w+)\\s*,\\s*plural\\s*,(?:\\s*offset:(\\d+))?');
/// A regular expression to parse the ordinal block.
///
/// It extracts the argument index.
static final RegExp _ordinalBlockRe =
RegExp('^\\s*(\\w+)\\s*,\\s*selectordinal\\s*,');
/// A regular expression to parse the select block.
///
/// It extracts the argument index.
static final RegExp _selectBlockRe =
RegExp('^\\s*(\\w+)\\s*,\\s*select\\s*,');
/// Detects the block type of the [pattern].
_BlockType _parseBlockType(String pattern) {
if (_pluralBlockRe.hasMatch(pattern)) {
return _BlockType.plural;
}
if (_ordinalBlockRe.hasMatch(pattern)) {
return _BlockType.ordinal;
}
if (_selectBlockRe.hasMatch(pattern)) {
return _BlockType.select;
}
if (RegExp('^\\s*\\w+\\s*').hasMatch(pattern)) {
return _BlockType.simple;
}
return _BlockType.unknown;
}
/// Parses generic block.
///
/// Takes the [pattern], which is the content of the block to parse,
/// and returns sub-blocks marked as strings, select, plural, ...
Queue<_BlockTypeAndVal> _parseBlock(String pattern) {
var result = Queue<_BlockTypeAndVal>();
var parts = _extractParts(pattern);
for (var thePart in parts) {
_BlockTypeAndVal block;
if (_ElementType.string == thePart._type) {
block = _BlockTypeAndVal(_BlockType.string, thePart._value);
} else if (_ElementType.block == thePart._type) {
_checkAndThrow(thePart._value is String,
'The value should be a string: ${thePart._value}');
var blockType = _parseBlockType(thePart._value);
switch (blockType) {
case _BlockType.select:
block = _BlockTypeAndVal(
_BlockType.select, _parseSelectBlock(thePart._value));
break;
case _BlockType.plural:
block = _BlockTypeAndVal(
_BlockType.plural, _parsePluralBlock(thePart._value));
break;
case _BlockType.ordinal:
block = _BlockTypeAndVal(
_BlockType.ordinal, _parseOrdinalBlock(thePart._value));
break;
case _BlockType.simple:
block = _BlockTypeAndVal(_BlockType.simple, thePart._value);
break;
default:
_checkAndThrow(
false, 'Unknown block type for pattern: ${thePart._value}');
}
} else {
_checkAndThrow(false, 'Unknown part of the pattern.');
}
result.add(block);
}
return result;
}
/// Parses a select type of a block and produces an object for it.
///
/// The [pattern] is the sub-pattern that needs to be parsed as select,
/// and returns an object with select block info.
Map<String, Object> _parseSelectBlock(String pattern) {
var argumentName = '';
var replaceRegex = _selectBlockRe;
pattern = pattern.replaceFirstMapped(replaceRegex, (match) {
// string, name
argumentName = match.group(1);
return '';
});
var result = <String, Object>{'argumentName': argumentName};
var parts = _extractParts(pattern);
// Looking for (key block)+ sequence. One of the keys has to be "other".
var pos = 0;
while (pos < parts.length) {
var thePart = parts.elementAt(pos);
_checkAndThrow(thePart._value is String, 'Missing select key element.');
var key = thePart._value;
pos++;
_checkAndThrow(
pos < parts.length, 'Missing or invalid select value element.');
thePart = parts.elementAt(pos);
Queue<_BlockTypeAndVal> value;
if (_ElementType.block == thePart._type) {
value = _parseBlock(thePart._value);
} else {
_checkAndThrow(false, 'Expected block type.');
}
result[key.replaceAll(RegExp('\\s'), '')] = value;
pos++;
}
_checkAndThrow(
result.containsKey(_other), 'Missing other key in select statement.');
return result;
}
/// Parses a plural type of a block and produces an object for it.
///
/// The [pattern] is the sub-pattern that needs to be parsed as plural.
/// and returns an bject with plural block info.
Map<String, Object> _parsePluralBlock(String pattern) {
var argumentName = '';
var argumentOffset = 0;
var replaceRegex = _pluralBlockRe;
pattern = pattern.replaceFirstMapped(replaceRegex, (match) {
// string, name, offset
argumentName = match.group(1);
if (_isDef(match.group(2))) {
argumentOffset = int.parse(match.group(2));
}
return '';
});
var result = {
'argumentName': argumentName,
'argumentOffset': argumentOffset
};
var parts = _extractParts(pattern);
// Looking for (key block)+ sequence.
var pos = 0;
while (pos < parts.length) {
var thePart = parts.elementAt(pos);
_checkAndThrow(thePart._value is String, 'Missing plural key element.');
var key = thePart._value;
pos++;
_checkAndThrow(
pos < parts.length, 'Missing or invalid plural value element.');
thePart = parts.elementAt(pos);
Queue<_BlockTypeAndVal> value;
if (_ElementType.block == thePart._type) {
value = _parseBlock(thePart._value);
} else {
_checkAndThrow(false, 'Expected block type.');
}
key = key.replaceFirstMapped(RegExp('\\s*(?:=)?(\\w+)\\s*'), (match) {
return match.group(1).toString();
});
result[key] = value;
pos++;
}
_checkAndThrow(
result.containsKey(_other), 'Missing other key in plural statement.');
return result;
}
/// Parses an ordinal type of a block and produces an object for it.
///
/// For example the input string:
/// `{FOO, selectordinal, one {Message A}other {Message B}}`
/// Should result in the output object:
/// ```
/// {
/// argumentName: 'FOO',
/// argumentOffest: 0,
/// one: [ { type: 4, value: 'Message A' } ],
/// other: [ { type: 4, value: 'Message B' } ]
/// }
/// ```
/// The [pattern] is the sub-pattern that needs to be parsed as ordinal,
/// and returns an bject with ordinal block info.
Map<String, Object> _parseOrdinalBlock(String pattern) {
var argumentName = '';
var replaceRegex = _ordinalBlockRe;
pattern = pattern.replaceFirstMapped(replaceRegex, (match) {
// string, name
argumentName = match.group(1);
return '';
});
var result = {'argumentName': argumentName, 'argumentOffset': 0};
var parts = _extractParts(pattern);
// Looking for (key block)+ sequence.
var pos = 0;
while (pos < parts.length) {
var thePart = parts.elementAt(pos);
_checkAndThrow(thePart._value is String, 'Missing ordinal key element.');
var key = thePart._value;
pos++;
_checkAndThrow(
pos < parts.length, 'Missing or invalid ordinal value element.');
thePart = parts.elementAt(pos);
Queue<_BlockTypeAndVal> value;
if (_ElementType.block == thePart._type) {
value = _parseBlock(thePart._value);
} else {
_checkAndThrow(false, 'Expected block type.');
}
key = key.replaceFirstMapped(RegExp('\\s*(?:=)?(\\w+)\\s*'), (match) {
return match.group(1).toString();
});
result[key] = value;
pos++;
}
_checkAndThrow(result.containsKey(_other),
'Missing other key in selectordinal statement.');
return result;
}
/// Builds a placeholder from the last index of the array.
///
/// using all the [literals] encountered during parse.
/// It returns a string that looks like this: `"\uFDDF_" + last index + "_"`.
String _buildPlaceholder(Queue<String> literals) {
_checkAndThrow(literals.isNotEmpty, 'Literal array is empty.');
var index = (literals.length - 1).toString();
return '$_literalPlaceholder${index}_';
}
}
//========== EXTRAS: temporary, to help the move from JS to Dart ==========
// Simple goog.isDef replacement, will probably remove it
bool _isDef(Object obj) {
return obj != null;
}
// Closure calls assert, which actually ends up with an exception on can catch.
// In Dart assert is only for debug, so I am using this small wrapper method.
void _checkAndThrow(bool condition, String message) {
if (!condition) {
throw AssertionError(message);
}
}
// Dart has no support for ordinals
// TODO(b/142132665): add ordial rules to intl, then fix this
class _OrdinalRules {
static String select(num n, String locale) {
return _PluralRules.select(n, locale);
}
}
// Simple mapping from Intl.pluralLogic to _PluralRules, to change later
class _PluralRules {
static String select(num n, String locale) {
return Intl.pluralLogic(n,
zero: 'zero',
one: 'one',
two: 'two',
few: 'few',
many: 'many',
other: 'other',
locale: locale);
}
}
// Pairs a value and information about its type.
class _TypeAndVal<T, V> {
final T _type;
final V _value;
_TypeAndVal(var this._type, var this._value);
@override
String toString() {
return '{type:$_type, value:$_value}';
}
}
/// Marks a string and block during parsing.
enum _ElementType { string, block }
class _ElementTypeAndVal extends _TypeAndVal<_ElementType, String> {
_ElementTypeAndVal(var _type, var _value) : super(_type, _value);
}
/// Block type.
enum _BlockType { plural, ordinal, select, simple, string, unknown }
class _BlockTypeAndVal extends _TypeAndVal<_BlockType, Object> {
_BlockTypeAndVal(var _type, var _value) : super(_type, _value);
}