Add locale-specific DateTime formatting syntax (#129573)
Based on the [message format syntax](https://unicode-org.github.io/icu/userguide/format_parse/messages/#examples) for [ICU4J](https://unicode-org.github.io/icu-docs/apidoc/released/icu4j/com/ibm/icu/text/MessageFormat.html). This adds new syntax to the current Flutter messageFormat parser which should allow developers to add locale-specific date formatting. ## Usage example ``` "datetimeTest": "Today is {today, date, ::yMd}", "@datetimeTest": { "placeholders": { "today": { "description": "The date placeholder", "type": "DateTime" } } } ``` compiles to ``` String datetimeTest(DateTime today) { String _temp0 = intl.DateFormat.yMd(localeName).format(today); return 'Today is $_temp0'; } ``` Fixes https://github.com/flutter/flutter/issues/127304.
This commit is contained in:
parent
f3a7485b2c
commit
ff838bca89
@ -1157,6 +1157,7 @@ class LocalizationsGenerator {
|
|||||||
// When traversing through a placeholderExpr node, return "$placeholderName".
|
// When traversing through a placeholderExpr node, return "$placeholderName".
|
||||||
// When traversing through a pluralExpr node, return "$tempVarN" and add variable declaration in "tempVariables".
|
// When traversing through a pluralExpr node, return "$tempVarN" and add variable declaration in "tempVariables".
|
||||||
// When traversing through a selectExpr node, return "$tempVarN" and add variable declaration in "tempVariables".
|
// When traversing through a selectExpr node, return "$tempVarN" and add variable declaration in "tempVariables".
|
||||||
|
// When traversing through an argumentExpr node, return "$tempVarN" and add variable declaration in "tempVariables".
|
||||||
// When traversing through a message node, return concatenation of all of "generateVariables(child)" for each child.
|
// When traversing through a message node, return concatenation of all of "generateVariables(child)" for each child.
|
||||||
String generateVariables(Node node, { bool isRoot = false }) {
|
String generateVariables(Node node, { bool isRoot = false }) {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
@ -1259,6 +1260,34 @@ The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", "
|
|||||||
.replaceAll('@(selectCases)', selectLogicArgs.join('\n'))
|
.replaceAll('@(selectCases)', selectLogicArgs.join('\n'))
|
||||||
);
|
);
|
||||||
return '\$$tempVarName';
|
return '\$$tempVarName';
|
||||||
|
case ST.argumentExpr:
|
||||||
|
requiresIntlImport = true;
|
||||||
|
assert(node.children[1].type == ST.identifier);
|
||||||
|
assert(node.children[3].type == ST.argType);
|
||||||
|
assert(node.children[7].type == ST.identifier);
|
||||||
|
final String identifierName = node.children[1].value!;
|
||||||
|
final Node formatType = node.children[7];
|
||||||
|
// Check that formatType is a valid intl.DateFormat.
|
||||||
|
if (!validDateFormats.contains(formatType.value)) {
|
||||||
|
throw L10nParserException(
|
||||||
|
'Date format "${formatType.value!}" for placeholder '
|
||||||
|
'$identifierName does not have a corresponding DateFormat '
|
||||||
|
"constructor\n. Check the intl library's DateFormat class "
|
||||||
|
'constructors for allowed date formats, or set "isCustomDateFormat" attribute '
|
||||||
|
'to "true".',
|
||||||
|
_inputFileNames[locale]!,
|
||||||
|
message.resourceId,
|
||||||
|
translationForMessage,
|
||||||
|
formatType.positionInMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final String tempVarName = getTempVariableName();
|
||||||
|
tempVariables.add(dateVariableTemplate
|
||||||
|
.replaceAll('@(varName)', tempVarName)
|
||||||
|
.replaceAll('@(formatType)', formatType.value!)
|
||||||
|
.replaceAll('@(argument)', identifierName)
|
||||||
|
);
|
||||||
|
return '\$$tempVarName';
|
||||||
// ignore: no_default_cases
|
// ignore: no_default_cases
|
||||||
default:
|
default:
|
||||||
throw Exception('Cannot call "generateHelperMethod" on node type ${node.type}');
|
throw Exception('Cannot call "generateHelperMethod" on node type ${node.type}');
|
||||||
|
@ -157,6 +157,9 @@ const String selectVariableTemplate = '''
|
|||||||
},
|
},
|
||||||
);''';
|
);''';
|
||||||
|
|
||||||
|
const String dateVariableTemplate = '''
|
||||||
|
String @(varName) = intl.DateFormat.@(formatType)(localeName).format(@(argument));''';
|
||||||
|
|
||||||
const String classFileTemplate = '''
|
const String classFileTemplate = '''
|
||||||
@(header)@(requiresIntlImport)import '@(fileName)';
|
@(header)@(requiresIntlImport)import '@(fileName)';
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ import 'message_parser.dart';
|
|||||||
// * <https://pub.dev/packages/intl>
|
// * <https://pub.dev/packages/intl>
|
||||||
// * <https://pub.dev/documentation/intl/latest/intl/DateFormat-class.html>
|
// * <https://pub.dev/documentation/intl/latest/intl/DateFormat-class.html>
|
||||||
// * <https://api.dartlang.org/stable/2.7.0/dart-core/DateTime-class.html>
|
// * <https://api.dartlang.org/stable/2.7.0/dart-core/DateTime-class.html>
|
||||||
const Set<String> _validDateFormats = <String>{
|
const Set<String> validDateFormats = <String>{
|
||||||
'd',
|
'd',
|
||||||
'E',
|
'E',
|
||||||
'EEEE',
|
'EEEE',
|
||||||
@ -244,13 +244,14 @@ class Placeholder {
|
|||||||
String? type;
|
String? type;
|
||||||
bool isPlural = false;
|
bool isPlural = false;
|
||||||
bool isSelect = false;
|
bool isSelect = false;
|
||||||
|
bool isDateTime = false;
|
||||||
|
bool requiresDateFormatting = false;
|
||||||
|
|
||||||
bool get requiresFormatting => requiresDateFormatting || requiresNumFormatting;
|
bool get requiresFormatting => requiresDateFormatting || requiresNumFormatting;
|
||||||
bool get requiresDateFormatting => type == 'DateTime';
|
|
||||||
bool get requiresNumFormatting => <String>['int', 'num', 'double'].contains(type) && format != null;
|
bool get requiresNumFormatting => <String>['int', 'num', 'double'].contains(type) && format != null;
|
||||||
bool get hasValidNumberFormat => _validNumberFormats.contains(format);
|
bool get hasValidNumberFormat => _validNumberFormats.contains(format);
|
||||||
bool get hasNumberFormatWithParameters => _numberFormatsWithNamedParameters.contains(format);
|
bool get hasNumberFormatWithParameters => _numberFormatsWithNamedParameters.contains(format);
|
||||||
bool get hasValidDateFormat => _validDateFormats.contains(format);
|
bool get hasValidDateFormat => validDateFormats.contains(format);
|
||||||
|
|
||||||
static String? _stringAttribute(
|
static String? _stringAttribute(
|
||||||
String resourceId,
|
String resourceId,
|
||||||
@ -488,7 +489,12 @@ class Message {
|
|||||||
final List<Node> traversalStack = <Node>[parsedMessages[locale]!];
|
final List<Node> traversalStack = <Node>[parsedMessages[locale]!];
|
||||||
while (traversalStack.isNotEmpty) {
|
while (traversalStack.isNotEmpty) {
|
||||||
final Node node = traversalStack.removeLast();
|
final Node node = traversalStack.removeLast();
|
||||||
if (<ST>[ST.placeholderExpr, ST.pluralExpr, ST.selectExpr].contains(node.type)) {
|
if (<ST>[
|
||||||
|
ST.placeholderExpr,
|
||||||
|
ST.pluralExpr,
|
||||||
|
ST.selectExpr,
|
||||||
|
ST.argumentExpr
|
||||||
|
].contains(node.type)) {
|
||||||
final String identifier = node.children[1].value!;
|
final String identifier = node.children[1].value!;
|
||||||
Placeholder? placeholder = getPlaceholder(identifier);
|
Placeholder? placeholder = getPlaceholder(identifier);
|
||||||
if (placeholder == null) {
|
if (placeholder == null) {
|
||||||
@ -499,6 +505,14 @@ class Message {
|
|||||||
placeholder.isPlural = true;
|
placeholder.isPlural = true;
|
||||||
} else if (node.type == ST.selectExpr) {
|
} else if (node.type == ST.selectExpr) {
|
||||||
placeholder.isSelect = true;
|
placeholder.isSelect = true;
|
||||||
|
} else if (node.type == ST.argumentExpr) {
|
||||||
|
placeholder.isDateTime = true;
|
||||||
|
} else {
|
||||||
|
// Here the node type must be ST.placeholderExpr.
|
||||||
|
// A DateTime placeholder must require date formatting.
|
||||||
|
if (placeholder.type == 'DateTime') {
|
||||||
|
placeholder.requiresDateFormatting = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
traversalStack.addAll(node.children);
|
traversalStack.addAll(node.children);
|
||||||
@ -510,9 +524,16 @@ class Message {
|
|||||||
..sort((MapEntry<String, Placeholder> p1, MapEntry<String, Placeholder> p2) => p1.key.compareTo(p2.key))
|
..sort((MapEntry<String, Placeholder> p1, MapEntry<String, Placeholder> p2) => p1.key.compareTo(p2.key))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
bool atMostOneOf(bool x, bool y, bool z) {
|
||||||
|
return x && !y && !z
|
||||||
|
|| !x && y && !z
|
||||||
|
|| !x && !y && z
|
||||||
|
|| !x && !y && !z;
|
||||||
|
}
|
||||||
|
|
||||||
for (final Placeholder placeholder in placeholders.values) {
|
for (final Placeholder placeholder in placeholders.values) {
|
||||||
if (placeholder.isPlural && placeholder.isSelect) {
|
if (!atMostOneOf(placeholder.isPlural, placeholder.isDateTime, placeholder.isSelect)) {
|
||||||
throw L10nException('Placeholder is used as both a plural and select in certain languages.');
|
throw L10nException('Placeholder is used as plural/select/datetime in certain languages.');
|
||||||
} else if (placeholder.isPlural) {
|
} else if (placeholder.isPlural) {
|
||||||
if (placeholder.type == null) {
|
if (placeholder.type == null) {
|
||||||
placeholder.type = 'num';
|
placeholder.type = 'num';
|
||||||
@ -526,6 +547,12 @@ class Message {
|
|||||||
} else if (placeholder.type != 'String') {
|
} else if (placeholder.type != 'String') {
|
||||||
throw L10nException("Placeholders used in selects must be of type 'String'");
|
throw L10nException("Placeholders used in selects must be of type 'String'");
|
||||||
}
|
}
|
||||||
|
} else if (placeholder.isDateTime) {
|
||||||
|
if (placeholder.type == null) {
|
||||||
|
placeholder.type = 'DateTime';
|
||||||
|
} else if (placeholder.type != 'DateTime') {
|
||||||
|
throw L10nException("Placeholders used in datetime expressions much be of type 'DateTime'");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
placeholder.type ??= 'Object';
|
placeholder.type ??= 'Object';
|
||||||
}
|
}
|
||||||
|
@ -22,11 +22,16 @@ enum ST {
|
|||||||
number,
|
number,
|
||||||
identifier,
|
identifier,
|
||||||
empty,
|
empty,
|
||||||
|
colon,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
// Nonterminal Types
|
// Nonterminal Types
|
||||||
message,
|
message,
|
||||||
|
|
||||||
placeholderExpr,
|
placeholderExpr,
|
||||||
|
|
||||||
|
argumentExpr,
|
||||||
|
|
||||||
pluralExpr,
|
pluralExpr,
|
||||||
pluralParts,
|
pluralParts,
|
||||||
pluralPart,
|
pluralPart,
|
||||||
@ -34,6 +39,8 @@ enum ST {
|
|||||||
selectExpr,
|
selectExpr,
|
||||||
selectParts,
|
selectParts,
|
||||||
selectPart,
|
selectPart,
|
||||||
|
|
||||||
|
argType,
|
||||||
}
|
}
|
||||||
|
|
||||||
// The grammar of the syntax.
|
// The grammar of the syntax.
|
||||||
@ -43,6 +50,7 @@ Map<ST, List<List<ST>>> grammar = <ST, List<List<ST>>>{
|
|||||||
<ST>[ST.placeholderExpr, ST.message],
|
<ST>[ST.placeholderExpr, ST.message],
|
||||||
<ST>[ST.pluralExpr, ST.message],
|
<ST>[ST.pluralExpr, ST.message],
|
||||||
<ST>[ST.selectExpr, ST.message],
|
<ST>[ST.selectExpr, ST.message],
|
||||||
|
<ST>[ST.argumentExpr, ST.message],
|
||||||
<ST>[ST.empty],
|
<ST>[ST.empty],
|
||||||
],
|
],
|
||||||
ST.placeholderExpr: <List<ST>>[
|
ST.placeholderExpr: <List<ST>>[
|
||||||
@ -73,6 +81,13 @@ Map<ST, List<List<ST>>> grammar = <ST, List<List<ST>>>{
|
|||||||
<ST>[ST.number, ST.openBrace, ST.message, ST.closeBrace],
|
<ST>[ST.number, ST.openBrace, ST.message, ST.closeBrace],
|
||||||
<ST>[ST.other, ST.openBrace, ST.message, ST.closeBrace],
|
<ST>[ST.other, ST.openBrace, ST.message, ST.closeBrace],
|
||||||
],
|
],
|
||||||
|
ST.argumentExpr: <List<ST>>[
|
||||||
|
<ST>[ST.openBrace, ST.identifier, ST.comma, ST.argType, ST.comma, ST.colon, ST.colon, ST.identifier, ST.closeBrace],
|
||||||
|
],
|
||||||
|
ST.argType: <List<ST>>[
|
||||||
|
<ST>[ST.date],
|
||||||
|
<ST>[ST.time],
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
class Node {
|
class Node {
|
||||||
@ -100,6 +115,8 @@ class Node {
|
|||||||
Node.selectKeyword(this.positionInMessage): type = ST.select, value = 'select';
|
Node.selectKeyword(this.positionInMessage): type = ST.select, value = 'select';
|
||||||
Node.otherKeyword(this.positionInMessage): type = ST.other, value = 'other';
|
Node.otherKeyword(this.positionInMessage): type = ST.other, value = 'other';
|
||||||
Node.empty(this.positionInMessage): type = ST.empty, value = '';
|
Node.empty(this.positionInMessage): type = ST.empty, value = '';
|
||||||
|
Node.dateKeyword(this.positionInMessage): type = ST.date, value = 'date';
|
||||||
|
Node.timeKeyword(this.positionInMessage): type = ST.time, value = 'time';
|
||||||
|
|
||||||
String? value;
|
String? value;
|
||||||
late ST type;
|
late ST type;
|
||||||
@ -162,6 +179,7 @@ RegExp numeric = RegExp(r'[0-9]+');
|
|||||||
RegExp alphanumeric = RegExp(r'[a-zA-Z0-9|_]+');
|
RegExp alphanumeric = RegExp(r'[a-zA-Z0-9|_]+');
|
||||||
RegExp comma = RegExp(r',');
|
RegExp comma = RegExp(r',');
|
||||||
RegExp equalSign = RegExp(r'=');
|
RegExp equalSign = RegExp(r'=');
|
||||||
|
RegExp colon = RegExp(r':');
|
||||||
|
|
||||||
// List of token matchers ordered by precedence
|
// List of token matchers ordered by precedence
|
||||||
Map<ST, RegExp> matchers = <ST, RegExp>{
|
Map<ST, RegExp> matchers = <ST, RegExp>{
|
||||||
@ -169,6 +187,7 @@ Map<ST, RegExp> matchers = <ST, RegExp>{
|
|||||||
ST.number: numeric,
|
ST.number: numeric,
|
||||||
ST.comma: comma,
|
ST.comma: comma,
|
||||||
ST.equalSign: equalSign,
|
ST.equalSign: equalSign,
|
||||||
|
ST.colon: colon,
|
||||||
ST.identifier: alphanumeric,
|
ST.identifier: alphanumeric,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -312,6 +331,10 @@ class Parser {
|
|||||||
matchedType = ST.select;
|
matchedType = ST.select;
|
||||||
case 'other':
|
case 'other':
|
||||||
matchedType = ST.other;
|
matchedType = ST.other;
|
||||||
|
case 'date':
|
||||||
|
matchedType = ST.date;
|
||||||
|
case 'time':
|
||||||
|
matchedType = ST.time;
|
||||||
}
|
}
|
||||||
tokens.add(Node(matchedType!, startIndex, value: match.group(0)));
|
tokens.add(Node(matchedType!, startIndex, value: match.group(0)));
|
||||||
startIndex = match.end;
|
startIndex = match.end;
|
||||||
@ -354,9 +377,9 @@ class Parser {
|
|||||||
switch (symbol) {
|
switch (symbol) {
|
||||||
case ST.message:
|
case ST.message:
|
||||||
if (tokens.isEmpty) {
|
if (tokens.isEmpty) {
|
||||||
parseAndConstructNode(ST.message, 4);
|
parseAndConstructNode(ST.message, 5);
|
||||||
} else if (tokens[0].type == ST.closeBrace) {
|
} else if (tokens[0].type == ST.closeBrace) {
|
||||||
parseAndConstructNode(ST.message, 4);
|
parseAndConstructNode(ST.message, 5);
|
||||||
} else if (tokens[0].type == ST.string) {
|
} else if (tokens[0].type == ST.string) {
|
||||||
parseAndConstructNode(ST.message, 0);
|
parseAndConstructNode(ST.message, 0);
|
||||||
} else if (tokens[0].type == ST.openBrace) {
|
} else if (tokens[0].type == ST.openBrace) {
|
||||||
@ -364,6 +387,8 @@ class Parser {
|
|||||||
parseAndConstructNode(ST.message, 2);
|
parseAndConstructNode(ST.message, 2);
|
||||||
} else if (3 < tokens.length && tokens[3].type == ST.select) {
|
} else if (3 < tokens.length && tokens[3].type == ST.select) {
|
||||||
parseAndConstructNode(ST.message, 3);
|
parseAndConstructNode(ST.message, 3);
|
||||||
|
} else if (3 < tokens.length && (tokens[3].type == ST.date || tokens[3].type == ST.time)) {
|
||||||
|
parseAndConstructNode(ST.message, 4);
|
||||||
} else {
|
} else {
|
||||||
parseAndConstructNode(ST.message, 1);
|
parseAndConstructNode(ST.message, 1);
|
||||||
}
|
}
|
||||||
@ -373,6 +398,16 @@ class Parser {
|
|||||||
}
|
}
|
||||||
case ST.placeholderExpr:
|
case ST.placeholderExpr:
|
||||||
parseAndConstructNode(ST.placeholderExpr, 0);
|
parseAndConstructNode(ST.placeholderExpr, 0);
|
||||||
|
case ST.argumentExpr:
|
||||||
|
parseAndConstructNode(ST.argumentExpr, 0);
|
||||||
|
case ST.argType:
|
||||||
|
if (tokens.isNotEmpty && tokens[0].type == ST.date) {
|
||||||
|
parseAndConstructNode(ST.argType, 0);
|
||||||
|
} else if (tokens.isNotEmpty && tokens[0].type == ST.time) {
|
||||||
|
parseAndConstructNode(ST.argType, 1);
|
||||||
|
} else {
|
||||||
|
throw L10nException('ICU Syntax Error. Found unknown argument type.');
|
||||||
|
}
|
||||||
case ST.pluralExpr:
|
case ST.pluralExpr:
|
||||||
parseAndConstructNode(ST.pluralExpr, 0);
|
parseAndConstructNode(ST.pluralExpr, 0);
|
||||||
case ST.pluralParts:
|
case ST.pluralParts:
|
||||||
|
@ -1759,6 +1759,67 @@ import 'output-localization-file_en.dart' deferred as output-localization-file_e
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('argument messages', () {
|
||||||
|
testWithoutContext('should generate proper calls to intl.DateFormat', () {
|
||||||
|
setupLocalizations(<String, String>{
|
||||||
|
'en': '''
|
||||||
|
{
|
||||||
|
"datetime": "{today, date, ::yMd}"
|
||||||
|
}'''
|
||||||
|
});
|
||||||
|
expect(getGeneratedFileContent(locale: 'en'), contains('intl.DateFormat.yMd(localeName).format(today)'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('should generate proper calls to intl.DateFormat when using time', () {
|
||||||
|
setupLocalizations(<String, String>{
|
||||||
|
'en': '''
|
||||||
|
{
|
||||||
|
"datetime": "{current, time, ::jms}"
|
||||||
|
}'''
|
||||||
|
});
|
||||||
|
expect(getGeneratedFileContent(locale: 'en'), contains('intl.DateFormat.jms(localeName).format(current)'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('should not complain when placeholders are explicitly typed to DateTime', () {
|
||||||
|
setupLocalizations(<String, String>{
|
||||||
|
'en': '''
|
||||||
|
{
|
||||||
|
"datetime": "{today, date, ::yMd}",
|
||||||
|
"@datetime": {
|
||||||
|
"placeholders": {
|
||||||
|
"today": { "type": "DateTime" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'''
|
||||||
|
});
|
||||||
|
expect(getGeneratedFileContent(locale: 'en'), contains('String datetime(DateTime today) {'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('should automatically infer date time placeholders that are not explicitly defined', () {
|
||||||
|
setupLocalizations(<String, String>{
|
||||||
|
'en': '''
|
||||||
|
{
|
||||||
|
"datetime": "{today, date, ::yMd}"
|
||||||
|
}'''
|
||||||
|
});
|
||||||
|
expect(getGeneratedFileContent(locale: 'en'), contains('String datetime(DateTime today) {'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('should throw on invalid DateFormat', () {
|
||||||
|
try {
|
||||||
|
setupLocalizations(<String, String>{
|
||||||
|
'en': '''
|
||||||
|
{
|
||||||
|
"datetime": "{today, date, ::yMMMMMd}"
|
||||||
|
}'''
|
||||||
|
});
|
||||||
|
assert(false);
|
||||||
|
} on L10nException {
|
||||||
|
expect(logger.errorText, contains('Date format "yMMMMMd" for placeholder today does not have a corresponding DateFormat constructor'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// All error handling for messages should collect errors on a per-error
|
// All error handling for messages should collect errors on a per-error
|
||||||
// basis and log them out individually. Then, it will throw an L10nException.
|
// basis and log them out individually. Then, it will throw an L10nException.
|
||||||
group('error handling tests', () {
|
group('error handling tests', () {
|
||||||
|
@ -306,6 +306,25 @@ void main() {
|
|||||||
])
|
])
|
||||||
));
|
));
|
||||||
|
|
||||||
|
expect(Parser('argumentTest', 'app_en.arb', 'Today is {date, date, ::yMMd}').parse(), equals(
|
||||||
|
Node(ST.message, 0, children: <Node>[
|
||||||
|
Node(ST.string, 0, value: 'Today is '),
|
||||||
|
Node(ST.argumentExpr, 9, children: <Node>[
|
||||||
|
Node(ST.openBrace, 9, value: '{'),
|
||||||
|
Node(ST.identifier, 10, value: 'date'),
|
||||||
|
Node(ST.comma, 14, value: ','),
|
||||||
|
Node(ST.argType, 16, children: <Node>[
|
||||||
|
Node(ST.date, 16, value: 'date'),
|
||||||
|
]),
|
||||||
|
Node(ST.comma, 20, value: ','),
|
||||||
|
Node(ST.colon, 22, value: ':'),
|
||||||
|
Node(ST.colon, 23, value: ':'),
|
||||||
|
Node(ST.identifier, 24, value: 'yMMd'),
|
||||||
|
Node(ST.closeBrace, 28, value: '}'),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
));
|
||||||
|
|
||||||
expect(Parser(
|
expect(Parser(
|
||||||
'plural',
|
'plural',
|
||||||
'app_en.arb',
|
'app_en.arb',
|
||||||
|
@ -126,46 +126,49 @@ void main() {
|
|||||||
'#l10n 73 (he)\n'
|
'#l10n 73 (he)\n'
|
||||||
'#l10n 74 (they)\n'
|
'#l10n 74 (they)\n'
|
||||||
'#l10n 75 (she)\n'
|
'#l10n 75 (she)\n'
|
||||||
'#l10n 76 (--- es ---)\n'
|
'#l10n 76 (6/26/2023)\n'
|
||||||
'#l10n 77 (ES - Hello world)\n'
|
'#l10n 77 (5:23:00 AM)\n'
|
||||||
'#l10n 78 (ES - Hello _NEWLINE_ World)\n'
|
'#l10n 78 (--- es ---)\n'
|
||||||
'#l10n 79 (ES - Hola \$ Mundo)\n'
|
'#l10n 79 (ES - Hello world)\n'
|
||||||
'#l10n 80 (ES - Hello Mundo)\n'
|
'#l10n 80 (ES - Hello _NEWLINE_ World)\n'
|
||||||
'#l10n 81 (ES - Hola Mundo)\n'
|
'#l10n 81 (ES - Hola \$ Mundo)\n'
|
||||||
'#l10n 82 (ES - Hello World on viernes, 1 de enero de 1960)\n'
|
'#l10n 82 (ES - Hello Mundo)\n'
|
||||||
'#l10n 83 (ES - Hello world argument on 1/1/1960 at 0:00)\n'
|
'#l10n 83 (ES - Hola Mundo)\n'
|
||||||
'#l10n 84 (ES - Hello World from 1960 to 2020)\n'
|
'#l10n 84 (ES - Hello World on viernes, 1 de enero de 1960)\n'
|
||||||
'#l10n 85 (ES - Hello for 123)\n'
|
'#l10n 85 (ES - Hello world argument on 1/1/1960 at 0:00)\n'
|
||||||
'#l10n 86 (ES - Hello)\n'
|
'#l10n 86 (ES - Hello World from 1960 to 2020)\n'
|
||||||
'#l10n 87 (ES - Hello World)\n'
|
'#l10n 87 (ES - Hello for 123)\n'
|
||||||
'#l10n 88 (ES - Hello two worlds)\n'
|
'#l10n 88 (ES - Hello)\n'
|
||||||
'#l10n 89 (ES - Hello)\n'
|
'#l10n 89 (ES - Hello World)\n'
|
||||||
'#l10n 90 (ES - Hello nuevo World)\n'
|
'#l10n 90 (ES - Hello two worlds)\n'
|
||||||
'#l10n 91 (ES - Hello two nuevo worlds)\n'
|
'#l10n 91 (ES - Hello)\n'
|
||||||
'#l10n 92 (ES - Hello on viernes, 1 de enero de 1960)\n'
|
'#l10n 92 (ES - Hello nuevo World)\n'
|
||||||
'#l10n 93 (ES - Hello World, on viernes, 1 de enero de 1960)\n'
|
'#l10n 93 (ES - Hello two nuevo worlds)\n'
|
||||||
'#l10n 94 (ES - Hello two worlds, on viernes, 1 de enero de 1960)\n'
|
'#l10n 94 (ES - Hello on viernes, 1 de enero de 1960)\n'
|
||||||
'#l10n 95 (ES - Hello other 0 worlds, with a total of 100 citizens)\n'
|
'#l10n 95 (ES - Hello World, on viernes, 1 de enero de 1960)\n'
|
||||||
'#l10n 96 (ES - Hello World of 101 citizens)\n'
|
'#l10n 96 (ES - Hello two worlds, on viernes, 1 de enero de 1960)\n'
|
||||||
'#l10n 97 (ES - Hello two worlds with 102 total citizens)\n'
|
'#l10n 97 (ES - Hello other 0 worlds, with a total of 100 citizens)\n'
|
||||||
'#l10n 98 (ES - [Hola] -Mundo- #123#)\n'
|
'#l10n 98 (ES - Hello World of 101 citizens)\n'
|
||||||
'#l10n 99 (ES - \$!)\n'
|
'#l10n 99 (ES - Hello two worlds with 102 total citizens)\n'
|
||||||
'#l10n 100 (ES - One \$)\n'
|
'#l10n 100 (ES - [Hola] -Mundo- #123#)\n'
|
||||||
"#l10n 101 (ES - Flutter's amazing!)\n"
|
'#l10n 101 (ES - \$!)\n'
|
||||||
"#l10n 102 (ES - Flutter's amazing, times 2!)\n"
|
'#l10n 102 (ES - One \$)\n'
|
||||||
'#l10n 103 (ES - Flutter is "amazing"!)\n'
|
"#l10n 103 (ES - Flutter's amazing!)\n"
|
||||||
'#l10n 104 (ES - Flutter is "amazing", times 2!)\n'
|
"#l10n 104 (ES - Flutter's amazing, times 2!)\n"
|
||||||
'#l10n 105 (ES - 16 wheel truck)\n'
|
'#l10n 105 (ES - Flutter is "amazing"!)\n'
|
||||||
"#l10n 106 (ES - Sedan's elegance)\n"
|
'#l10n 106 (ES - Flutter is "amazing", times 2!)\n'
|
||||||
'#l10n 107 (ES - Cabriolet has "acceleration")\n'
|
'#l10n 107 (ES - 16 wheel truck)\n'
|
||||||
'#l10n 108 (ES - Oh, she found ES - 1 itemES - !)\n'
|
"#l10n 108 (ES - Sedan's elegance)\n"
|
||||||
'#l10n 109 (ES - Indeed, ES - they like ES - Flutter!)\n'
|
'#l10n 109 (ES - Cabriolet has "acceleration")\n'
|
||||||
'#l10n 110 (--- es_419 ---)\n'
|
'#l10n 110 (ES - Oh, she found ES - 1 itemES - !)\n'
|
||||||
'#l10n 111 (ES 419 - Hello World)\n'
|
'#l10n 111 (ES - Indeed, ES - they like ES - Flutter!)\n'
|
||||||
'#l10n 112 (ES 419 - Hello)\n'
|
'#l10n 112 (--- es_419 ---)\n'
|
||||||
'#l10n 113 (ES 419 - Hello World)\n'
|
'#l10n 113 (ES 419 - Hello World)\n'
|
||||||
'#l10n 114 (ES 419 - Hello two worlds)\n'
|
'#l10n 114 (ES 419 - Hello)\n'
|
||||||
|
'#l10n 115 (ES 419 - Hello World)\n'
|
||||||
|
'#l10n 116 (ES 419 - Hello two worlds)\n'
|
||||||
'#l10n END\n'
|
'#l10n END\n'
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,6 +232,8 @@ class Home extends StatelessWidget {
|
|||||||
"${localizations.selectInPlural('male', 1)}",
|
"${localizations.selectInPlural('male', 1)}",
|
||||||
"${localizations.selectInPlural('male', 2)}",
|
"${localizations.selectInPlural('male', 2)}",
|
||||||
"${localizations.selectInPlural('female', 1)}",
|
"${localizations.selectInPlural('female', 1)}",
|
||||||
|
'${localizations.datetime1(DateTime(2023, 6, 26))}',
|
||||||
|
'${localizations.datetime2(DateTime(2023, 6, 26, 5, 23))}',
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -682,7 +684,9 @@ void main() {
|
|||||||
"type": "num"
|
"type": "num"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"datetime1": "{today, date, ::yMd}",
|
||||||
|
"datetime2": "{current, time, ::jms}"
|
||||||
}
|
}
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user