| // 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. |
| |
| // `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 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 |
| final 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. |
| final 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(r"'([{}#].*?)'"); |
| |
| /// Pattern for looking for '' in the message. |
| static final Pattern _patternDoubleApostrophe = "''"; |
| |
| /// 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; |
| |
| switch (patternType) { |
| case _BlockType.string: |
| result.add(patternValue as String); |
| break; |
| case _BlockType.simple: |
| _formatSimplePlaceholder( |
| patternValue as String, namedParameters, result); |
| break; |
| case _BlockType.select: |
| _checkAndThrow(patternValue is Map<String, Object>, |
| 'The value should be a map: $patternValue'); |
| var mapPattern = patternValue as Map<String, Object>; |
| _formatSelectBlock(mapPattern, namedParameters, ignorePound, result); |
| break; |
| case _BlockType.plural: |
| _formatPluralOrdinalBlock(patternValue as Map<String, Object>, |
| namedParameters, _PluralRules.select, ignorePound, result); |
| break; |
| case _BlockType.ordinal: |
| _formatPluralOrdinalBlock(patternValue as Map<String, Object>, |
| 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]] as Queue<_BlockTypeAndVal>?; |
| if (!_isDef(option)) { |
| option = parsedBlocks[_other] as Queue<_BlockTypeAndVal>?; |
| _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, |
| dynamic 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 as String); |
| 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]] as Queue<_BlockTypeAndVal>?; |
| if (!_isDef(option)) { |
| option = parsedBlocks[namedParameters[argumentName].toString()] |
| as Queue<_BlockTypeAndVal>?; |
| } |
| if (!_isDef(option)) { |
| var item = pluralSelector(diff.abs(), _locale); |
| _checkAndThrow(item is String, 'Invalid plural key.'); |
| |
| option = parsedBlocks[item] as Queue<_BlockTypeAndVal>?; |
| |
| // If option is not provided fall back to "other". |
| if (!_isDef(option)) { |
| option = parsedBlocks[_other] as Queue<_BlockTypeAndVal>?; |
| } |
| |
| _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(''); |
| 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(_patternDoubleApostrophe, (match) { |
| literals.add("'"); |
| return buildPlaceholder(literals); |
| }); |
| |
| pattern = pattern.replaceAllMapped(_regexLiteral, (match) { |
| // match, text |
| var text = match[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(r'[{}]'); |
| |
| 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(r'^\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(r'^\s*(\w+)\s*,\s*selectordinal\s*,'); |
| |
| /// A regular expression to parse the select block. |
| /// |
| /// It extracts the argument index. |
| static final RegExp _selectBlockRe = RegExp(r'^\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(r'^\s*\w').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) { |
| 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[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); |
| 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(r'\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[1]!; |
| if (_isDef(match[2])) { |
| argumentOffset = int.parse(match[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); |
| 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(r'\s*=?(\w+)\s*'), (match) { |
| return match[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[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); |
| 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(r'\s*=?(\w+)\s*'), (match) { |
| return match[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 one 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 ordinal 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, |
| useExplicitNumberCases: false); |
| } |
| } |
| |
| // Pairs a value and information about its type. |
| class _TypeAndVal<T, V> { |
| final T _type; |
| final V _value; |
| |
| _TypeAndVal(this._type, 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(_ElementType type, String value) : super(type, value); |
| } |
| |
| /// Block type. |
| enum _BlockType { plural, ordinal, select, simple, string, unknown } |
| |
| class _BlockTypeAndVal extends _TypeAndVal<_BlockType, Object> { |
| _BlockTypeAndVal(_BlockType type, Object value) : super(type, value); |
| } |