misc .arb fixes; localizations validator (#12197)
* misc .arb fixes; localizations validator * regenerate localizations * address comments * do not treat plural variations as invalid keys
This commit is contained in:
parent
d3d6198852
commit
f4f20c2909
@ -52,8 +52,42 @@ Future<Null> _generateDocs() async {
|
|||||||
print('${bold}DONE: test.dart does nothing in the docs shard.$reset');
|
print('${bold}DONE: test.dart does nothing in the docs shard.$reset');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Null> _verifyInternationalizations() async {
|
||||||
|
final EvalResult genResult = await _evalCommand(
|
||||||
|
dart,
|
||||||
|
<String>[
|
||||||
|
path.join('dev', 'tools', 'gen_localizations.dart'),
|
||||||
|
path.join('packages', 'flutter', 'lib', 'src', 'material', 'i18n'),
|
||||||
|
'material'
|
||||||
|
],
|
||||||
|
workingDirectory: flutterRoot,
|
||||||
|
);
|
||||||
|
|
||||||
|
final String localizationsFile = path.join('packages', 'flutter', 'lib', 'src', 'material', 'i18n', 'localizations.dart');
|
||||||
|
|
||||||
|
final EvalResult sourceContents = await _evalCommand(
|
||||||
|
'cat',
|
||||||
|
<String>[localizationsFile],
|
||||||
|
workingDirectory: flutterRoot,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (genResult.stdout.trim() != sourceContents.stdout.trim()) {
|
||||||
|
stderr
|
||||||
|
..writeln('<<<<<<< $localizationsFile')
|
||||||
|
..writeln(sourceContents.stdout.trim())
|
||||||
|
..writeln('=======')
|
||||||
|
..writeln(genResult.stdout.trim())
|
||||||
|
..writeln('>>>>>>> gen_localizations')
|
||||||
|
..writeln('The contents of $localizationsFile are different from that produced by gen_localizations.')
|
||||||
|
..writeln()
|
||||||
|
..writeln('Did you forget to run gen_localizations.dart after updating a .arb file?');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Null> _analyzeRepo() async {
|
Future<Null> _analyzeRepo() async {
|
||||||
await _verifyNoBadImports(flutterRoot);
|
await _verifyNoBadImports(flutterRoot);
|
||||||
|
await _verifyInternationalizations();
|
||||||
|
|
||||||
// Analyze all the Dart code in the repo.
|
// Analyze all the Dart code in the repo.
|
||||||
await _runFlutterAnalyze(flutterRoot,
|
await _runFlutterAnalyze(flutterRoot,
|
||||||
@ -176,6 +210,57 @@ Future<Null> _pubRunTest(
|
|||||||
return _runCommand(pub, args, workingDirectory: workingDirectory);
|
return _runCommand(pub, args, workingDirectory: workingDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EvalResult {
|
||||||
|
EvalResult({
|
||||||
|
this.stdout,
|
||||||
|
this.stderr,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String stdout;
|
||||||
|
final String stderr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<EvalResult> _evalCommand(String executable, List<String> arguments, {
|
||||||
|
String workingDirectory,
|
||||||
|
Map<String, String> environment,
|
||||||
|
bool skip: false,
|
||||||
|
}) async {
|
||||||
|
final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
|
||||||
|
final String relativeWorkingDir = path.relative(workingDirectory);
|
||||||
|
if (skip) {
|
||||||
|
_printProgress('SKIPPING', relativeWorkingDir, commandDescription);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
_printProgress('RUNNING', relativeWorkingDir, commandDescription);
|
||||||
|
|
||||||
|
final Process process = await Process.start(executable, arguments,
|
||||||
|
workingDirectory: workingDirectory,
|
||||||
|
environment: environment,
|
||||||
|
);
|
||||||
|
|
||||||
|
final Future<List<List<int>>> savedStdout = process.stdout.toList();
|
||||||
|
final Future<List<List<int>>> savedStderr = process.stderr.toList();
|
||||||
|
final int exitCode = await process.exitCode;
|
||||||
|
final EvalResult result = new EvalResult(
|
||||||
|
stdout: UTF8.decode((await savedStdout).expand((List<int> ints) => ints).toList()),
|
||||||
|
stderr: UTF8.decode((await savedStderr).expand((List<int> ints) => ints).toList()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (exitCode != 0) {
|
||||||
|
stderr.write(result.stderr);
|
||||||
|
print(
|
||||||
|
'$red━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$reset\n'
|
||||||
|
'${bold}ERROR:$red Last command exited with $exitCode.$reset\n'
|
||||||
|
'${bold}Command:$red $commandDescription$reset\n'
|
||||||
|
'${bold}Relative working directory:$red $relativeWorkingDir$reset\n'
|
||||||
|
'$red━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$reset'
|
||||||
|
);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
Future<Null> _runCommand(String executable, List<String> arguments, {
|
Future<Null> _runCommand(String executable, List<String> arguments, {
|
||||||
String workingDirectory,
|
String workingDirectory,
|
||||||
Map<String, String> environment,
|
Map<String, String> environment,
|
||||||
|
@ -26,6 +26,8 @@
|
|||||||
import 'dart:convert' show JSON;
|
import 'dart:convert' show JSON;
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'localizations_validator.dart';
|
||||||
|
|
||||||
const String outputHeader = '''
|
const String outputHeader = '''
|
||||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
@ -36,8 +38,14 @@ const String outputHeader = '''
|
|||||||
// @(regenerate)
|
// @(regenerate)
|
||||||
''';
|
''';
|
||||||
|
|
||||||
|
/// Maps locales to resource key/value pairs.
|
||||||
final Map<String, Map<String, String>> localeToResources = <String, Map<String, String>>{};
|
final Map<String, Map<String, String>> localeToResources = <String, Map<String, String>>{};
|
||||||
|
|
||||||
|
/// Maps locales to resource attributes.
|
||||||
|
///
|
||||||
|
/// See also https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
|
||||||
|
final Map<String, Map<String, dynamic>> localeToResourceAttributes = <String, Map<String, dynamic>>{};
|
||||||
|
|
||||||
// Return s as a Dart-parseable raw string in double quotes. Expand double quotes:
|
// Return s as a Dart-parseable raw string in double quotes. Expand double quotes:
|
||||||
// foo => r"foo"
|
// foo => r"foo"
|
||||||
// foo "bar" => r"foo " '"' r"bar" '"'
|
// foo "bar" => r"foo " '"' r"bar" '"'
|
||||||
@ -92,13 +100,16 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
|
|
||||||
void processBundle(File file, String locale) {
|
void processBundle(File file, String locale) {
|
||||||
localeToResources[locale] ??= <String, String>{};
|
localeToResources[locale] ??= <String, String>{};
|
||||||
|
localeToResourceAttributes[locale] ??= <String, dynamic>{};
|
||||||
final Map<String, String> resources = localeToResources[locale];
|
final Map<String, String> resources = localeToResources[locale];
|
||||||
|
final Map<String, dynamic> attributes = localeToResourceAttributes[locale];
|
||||||
final Map<String, dynamic> bundle = JSON.decode(file.readAsStringSync());
|
final Map<String, dynamic> bundle = JSON.decode(file.readAsStringSync());
|
||||||
for (String key in bundle.keys) {
|
for (String key in bundle.keys) {
|
||||||
// The ARB file resource "attributes" for foo are called @foo.
|
// The ARB file resource "attributes" for foo are called @foo.
|
||||||
if (key.startsWith('@'))
|
if (key.startsWith('@'))
|
||||||
continue;
|
attributes[key.substring(1)] = bundle[key];
|
||||||
resources[key] = bundle[key];
|
else
|
||||||
|
resources[key] = bundle[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,6 +132,7 @@ void main(List<String> args) {
|
|||||||
processBundle(new File(path), locale);
|
processBundle(new File(path), locale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
validateLocalizations(localeToResources, localeToResourceAttributes);
|
||||||
|
|
||||||
final String regenerate = 'dart dev/tools/gen_localizations.dart ${directory.path} ${args[1]}';
|
final String regenerate = 'dart dev/tools/gen_localizations.dart ${directory.path} ${args[1]}';
|
||||||
print(outputHeader.replaceFirst('@(regenerate)', regenerate));
|
print(outputHeader.replaceFirst('@(regenerate)', regenerate));
|
||||||
|
91
dev/tools/localizations_validator.dart
Normal file
91
dev/tools/localizations_validator.dart
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
/// Enforces the following invariants in our localizations:
|
||||||
|
///
|
||||||
|
/// - Resource keys are valid, i.e. they appear in the canonical list.
|
||||||
|
/// - Resource keys are complete for language-level locales, e.g. "es", "he".
|
||||||
|
///
|
||||||
|
/// Uses "en" localizations as the canonical source of locale keys that other
|
||||||
|
/// locales are compared against.
|
||||||
|
///
|
||||||
|
/// If validation fails, print an error message to STDERR and quit with exit
|
||||||
|
/// code 1.
|
||||||
|
void validateLocalizations(
|
||||||
|
Map<String, Map<String, String>> localeToResources,
|
||||||
|
Map<String, Map<String, dynamic>> localeToAttributes,
|
||||||
|
) {
|
||||||
|
final Map<String, String> canonicalLocalizations = localeToResources['en'];
|
||||||
|
final Set<String> canonicalKeys = new Set<String>.from(canonicalLocalizations.keys);
|
||||||
|
final StringBuffer errorMessages = new StringBuffer();
|
||||||
|
bool explainMissingKeys = false;
|
||||||
|
for (final String locale in localeToResources.keys) {
|
||||||
|
final Map<String, String> resources = localeToResources[locale];
|
||||||
|
|
||||||
|
// Whether `key` corresponds to one of the plural variations of a key with
|
||||||
|
// the same prefix and suffix "Other".
|
||||||
|
//
|
||||||
|
// Many languages require only a subset of these variations, so we do not
|
||||||
|
// require them so long as the "Other" variation exists.
|
||||||
|
bool isPluralVariation(String key) {
|
||||||
|
final RegExp pluralRegexp = new RegExp(r'(\w*)(Zero|One|Two|Few|Many)$');
|
||||||
|
final Match pluralMatch = pluralRegexp.firstMatch(key);
|
||||||
|
|
||||||
|
if (pluralMatch == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
final String prefix = pluralMatch[1];
|
||||||
|
return resources.containsKey('${prefix}Other');
|
||||||
|
}
|
||||||
|
|
||||||
|
final Set<String> keys = new Set<String>.from(
|
||||||
|
resources.keys.where((String key) => !isPluralVariation(key))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make sure keys are valid (i.e. they also exist in the canonical
|
||||||
|
// localizations)
|
||||||
|
final Set<String> invalidKeys = keys.difference(canonicalKeys);
|
||||||
|
if (invalidKeys.isNotEmpty)
|
||||||
|
errorMessages.writeln('Locale "$locale" contains invalid resource keys: ${invalidKeys.join(', ')}');
|
||||||
|
|
||||||
|
// For language-level locales only, check that they have a complete list of
|
||||||
|
// keys, or opted out of using certain ones.
|
||||||
|
if (locale.length == 2) {
|
||||||
|
final Map<String, dynamic> attributes = localeToAttributes[locale];
|
||||||
|
final List<String> missingKeys = <String>[];
|
||||||
|
|
||||||
|
for (final String missingKey in canonicalKeys.difference(keys)) {
|
||||||
|
final dynamic attribute = attributes[missingKey];
|
||||||
|
final bool intentionallyOmitted = attribute is Map && attribute.containsKey('notUsed');
|
||||||
|
if (!intentionallyOmitted && !isPluralVariation(missingKey))
|
||||||
|
missingKeys.add(missingKey);
|
||||||
|
}
|
||||||
|
if (missingKeys.isNotEmpty) {
|
||||||
|
explainMissingKeys = true;
|
||||||
|
errorMessages.writeln('Locale "$locale" is missing the following resource keys: ${missingKeys.join(', ')}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMessages.isNotEmpty) {
|
||||||
|
if (explainMissingKeys) {
|
||||||
|
errorMessages
|
||||||
|
..writeln()
|
||||||
|
..writeln(
|
||||||
|
'If a resource key is intentionally omitted, add an attribute corresponding '
|
||||||
|
'to the key name with a "notUsed" property explaining why. Example:'
|
||||||
|
)
|
||||||
|
..writeln()
|
||||||
|
..writeln('"@anteMeridiemAbbreviation": {')
|
||||||
|
..writeln(' "notUsed": "Sindhi time format does not use a.m. indicator"')
|
||||||
|
..writeln('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr.writeln('ERROR:');
|
||||||
|
stderr.writeln(errorMessages);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
// This file has been automatically generated. Please do not edit it manually.
|
// This file has been automatically generated. Please do not edit it manually.
|
||||||
// To regenerate the file, use:
|
// To regenerate the file, use:
|
||||||
// dart dev/tools/gen_localizations.dart lib/src/material/i18n material
|
// dart dev/tools/gen_localizations.dart packages/flutter/lib/src/material/i18n material
|
||||||
|
|
||||||
/// Maps from [Locale.languageCode] to a map that contains the localized strings
|
/// Maps from [Locale.languageCode] to a map that contains the localized strings
|
||||||
/// for that locale.
|
/// for that locale.
|
||||||
@ -136,6 +136,8 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
},
|
},
|
||||||
"es_US": const <String, String>{
|
"es_US": const <String, String>{
|
||||||
"timeOfDayFormat": r"h:mm a",
|
"timeOfDayFormat": r"h:mm a",
|
||||||
|
"anteMeridiemAbbreviation": r"AM",
|
||||||
|
"postMeridiemAbbreviation": r"PM",
|
||||||
},
|
},
|
||||||
"fa": const <String, String>{
|
"fa": const <String, String>{
|
||||||
"timeOfDayFormat": r"H:mm",
|
"timeOfDayFormat": r"H:mm",
|
||||||
@ -339,7 +341,9 @@ const Map<String, Map<String, String>> localizations = const <String, Map<String
|
|||||||
"rowsPerPageTitle": r"Строки на страницу:",
|
"rowsPerPageTitle": r"Строки на страницу:",
|
||||||
"aboutListTileTitle": r"O $applicationName",
|
"aboutListTileTitle": r"O $applicationName",
|
||||||
"licensesPageTitle": r"Лицензии",
|
"licensesPageTitle": r"Лицензии",
|
||||||
"selectedRowCountTitleOther": r"Выбранно $selectedRowCount строк",
|
"selectedRowCountTitleZero": r"Строки не выбраны",
|
||||||
|
"selectedRowCountTitleOne": r"Выбрана 1 строка",
|
||||||
|
"selectedRowCountTitleOther": r"Выбрано $selectedRowCount строк",
|
||||||
"cancelButtonLabel": r"ОТМЕНИТЬ",
|
"cancelButtonLabel": r"ОТМЕНИТЬ",
|
||||||
"closeButtonLabel": r"ЗАКРЫТЬ",
|
"closeButtonLabel": r"ЗАКРЫТЬ",
|
||||||
"continueButtonLabel": r"ПРОДОЛЖИТЬ",
|
"continueButtonLabel": r"ПРОДОЛЖИТЬ",
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"timeOfDayFormat": "HH:mm",
|
"timeOfDayFormat": "HH:mm",
|
||||||
|
"@anteMeridiemAbbreviation": { "notUsed": "German time format does not use a.m. indicator" },
|
||||||
|
"@postMeridiemAbbreviation": { "notUsed": "German time format does not use p.m. indicator" },
|
||||||
"openAppDrawerTooltip": "Navigationsmenü öffnen",
|
"openAppDrawerTooltip": "Navigationsmenü öffnen",
|
||||||
"backButtonTooltip": "Zurück",
|
"backButtonTooltip": "Zurück",
|
||||||
"closeButtonTooltip": "Schließen",
|
"closeButtonTooltip": "Schließen",
|
||||||
|
@ -67,7 +67,7 @@
|
|||||||
|
|
||||||
"pageRowsInfoTitle": "$firstRow–$lastRow of $rowCount",
|
"pageRowsInfoTitle": "$firstRow–$lastRow of $rowCount",
|
||||||
"pageRowsInfoTitleApproximate": "$firstRow–$lastRow of about $rowCount",
|
"pageRowsInfoTitleApproximate": "$firstRow–$lastRow of about $rowCount",
|
||||||
"@pageRowInfoTitle": {
|
"@pageRowsInfoTitle": {
|
||||||
"description": "Title for the [PaginatedDataTable]'s row info footer",
|
"description": "Title for the [PaginatedDataTable]'s row info footer",
|
||||||
"type": "text"
|
"type": "text"
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"timeOfDayFormat": "H:mm",
|
"timeOfDayFormat": "H:mm",
|
||||||
|
"@anteMeridiemAbbreviation": { "notUsed": "Standard Spanish time format does not use a.m. indicator" },
|
||||||
|
"@postMeridiemAbbreviation": { "notUsed": "Standard Spanish time format does not use p.m. indicator" },
|
||||||
"openAppDrawerTooltip": "Abrir el menú de navegación",
|
"openAppDrawerTooltip": "Abrir el menú de navegación",
|
||||||
"backButtonTooltip": "Espalda",
|
"backButtonTooltip": "Espalda",
|
||||||
"closeButtonTooltip": "Cerrar",
|
"closeButtonTooltip": "Cerrar",
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"timeOfDayFormat": "h:mm a"
|
"timeOfDayFormat": "h:mm a",
|
||||||
|
"anteMeridiemAbbreviation": "AM",
|
||||||
|
"postMeridiemAbbreviation": "PM"
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"timeOfDayFormat": "H:mm",
|
"timeOfDayFormat": "H:mm",
|
||||||
|
"@anteMeridiemAbbreviation": { "notUsed": "Farsi time format does not use a.m. indicator" },
|
||||||
|
"@postMeridiemAbbreviation": { "notUsed": "Farsi time format does not use p.m. indicator" },
|
||||||
"openAppDrawerTooltip": "منوی ناوبری را باز کنید",
|
"openAppDrawerTooltip": "منوی ناوبری را باز کنید",
|
||||||
"backButtonTooltip": "بازگشت",
|
"backButtonTooltip": "بازگشت",
|
||||||
"closeButtonTooltip": "بستن",
|
"closeButtonTooltip": "بستن",
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"timeOfDayFormat": "HH:mm",
|
"timeOfDayFormat": "HH:mm",
|
||||||
|
"@anteMeridiemAbbreviation": { "notUsed": "French time format does not use a.m. indicator" },
|
||||||
|
"@postMeridiemAbbreviation": { "notUsed": "French time format does not use p.m. indicator" },
|
||||||
"openAppDrawerTooltip": "Ouvrir le menu de navigation",
|
"openAppDrawerTooltip": "Ouvrir le menu de navigation",
|
||||||
"backButtonTooltip": "Retour",
|
"backButtonTooltip": "Retour",
|
||||||
"closeButtonTooltip": "Fermer",
|
"closeButtonTooltip": "Fermer",
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"timeOfDayFormat": "H:mm",
|
"timeOfDayFormat": "H:mm",
|
||||||
|
"@anteMeridiemAbbreviation": { "notUsed": "Hebrew time format does not use a.m. indicator" },
|
||||||
|
"@postMeridiemAbbreviation": { "notUsed": "Hebrew time format does not use p.m. indicator" },
|
||||||
"openAppDrawerTooltip": "פתח תפריט ניווט",
|
"openAppDrawerTooltip": "פתח תפריט ניווט",
|
||||||
"backButtonTooltip": "אחורה",
|
"backButtonTooltip": "אחורה",
|
||||||
"closeButtonTooltip": "סגור",
|
"closeButtonTooltip": "סגור",
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"timeOfDayFormat": "HH:mm",
|
"timeOfDayFormat": "HH:mm",
|
||||||
|
"@anteMeridiemAbbreviation": { "notUsed": "Italian time format does not use a.m. indicator" },
|
||||||
|
"@postMeridiemAbbreviation": { "notUsed": "Italian time format does not use p.m. indicator" },
|
||||||
"openAppDrawerTooltip": "Apri il menu di navigazione",
|
"openAppDrawerTooltip": "Apri il menu di navigazione",
|
||||||
"backButtonTooltip": "Indietro",
|
"backButtonTooltip": "Indietro",
|
||||||
"closeButtonTooltip": "Chiudi",
|
"closeButtonTooltip": "Chiudi",
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"timeOfDayFormat": "H:mm",
|
"timeOfDayFormat": "H:mm",
|
||||||
|
"@anteMeridiemAbbreviation": { "notUsed": "Japanese time format does not use a.m. indicator" },
|
||||||
|
"@postMeridiemAbbreviation": { "notUsed": "Japanese time format does not use p.m. indicator" },
|
||||||
"openAppDrawerTooltip": "ナビゲーションメニューを開く",
|
"openAppDrawerTooltip": "ナビゲーションメニューを開く",
|
||||||
"backButtonTooltip": "戻る",
|
"backButtonTooltip": "戻る",
|
||||||
"closeButtonTooltip": "閉じる",
|
"closeButtonTooltip": "閉じる",
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"timeOfDayFormat": "HH:mm",
|
"timeOfDayFormat": "HH:mm",
|
||||||
|
"@anteMeridiemAbbreviation": { "notUsed": "Pashto time format does not use a.m. indicator" },
|
||||||
|
"@postMeridiemAbbreviation": { "notUsed": "Pashto time format does not use p.m. indicator" },
|
||||||
"openAppDrawerTooltip": "د پرانیستی نیینګ مینو",
|
"openAppDrawerTooltip": "د پرانیستی نیینګ مینو",
|
||||||
"backButtonTooltip": "شاته",
|
"backButtonTooltip": "شاته",
|
||||||
"closeButtonTooltip": "بنده",
|
"closeButtonTooltip": "بنده",
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"timeOfDayFormat": "HH:mm",
|
"timeOfDayFormat": "HH:mm",
|
||||||
|
"@anteMeridiemAbbreviation": { "notUsed": "Portuguese time format does not use a.m. indicator" },
|
||||||
|
"@postMeridiemAbbreviation": { "notUsed": "Portuguese time format does not use p.m. indicator" },
|
||||||
"openAppDrawerTooltip": "Abrir menu de navegação",
|
"openAppDrawerTooltip": "Abrir menu de navegação",
|
||||||
"backButtonTooltip": "Costas",
|
"backButtonTooltip": "Costas",
|
||||||
"closeButtonTooltip": "Fechar",
|
"closeButtonTooltip": "Fechar",
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"timeOfDayFormat": "H:mm",
|
"timeOfDayFormat": "H:mm",
|
||||||
|
"@anteMeridiemAbbreviation": { "notUsed": "Russian time format does not use a.m. indicator" },
|
||||||
|
"@postMeridiemAbbreviation": { "notUsed": "Russian time format does not use p.m. indicator" },
|
||||||
"openAppDrawerTooltip": "Открыть меню навигации",
|
"openAppDrawerTooltip": "Открыть меню навигации",
|
||||||
"backButtonTooltip": "Назад",
|
"backButtonTooltip": "Назад",
|
||||||
"closeButtonTooltip": "Закрыть",
|
"closeButtonTooltip": "Закрыть",
|
||||||
@ -13,7 +15,9 @@
|
|||||||
"rowsPerPageTitle": "Строки на страницу:",
|
"rowsPerPageTitle": "Строки на страницу:",
|
||||||
"aboutListTileTitle": "O $applicationName",
|
"aboutListTileTitle": "O $applicationName",
|
||||||
"licensesPageTitle": "Лицензии",
|
"licensesPageTitle": "Лицензии",
|
||||||
"selectedRowCountTitleOther": "Выбранно $selectedRowCount строк",
|
"selectedRowCountTitleZero": "Строки не выбраны",
|
||||||
|
"selectedRowCountTitleOne": "Выбрана 1 строка",
|
||||||
|
"selectedRowCountTitleOther": "Выбрано $selectedRowCount строк",
|
||||||
"cancelButtonLabel": "ОТМЕНИТЬ",
|
"cancelButtonLabel": "ОТМЕНИТЬ",
|
||||||
"closeButtonLabel": "ЗАКРЫТЬ",
|
"closeButtonLabel": "ЗАКРЫТЬ",
|
||||||
"continueButtonLabel": "ПРОДОЛЖИТЬ",
|
"continueButtonLabel": "ПРОДОЛЖИТЬ",
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"timeOfDayFormat": "HH:mm",
|
"timeOfDayFormat": "HH:mm",
|
||||||
|
"@anteMeridiemAbbreviation": { "notUsed": "Sindhi time format does not use a.m. indicator" },
|
||||||
|
"@postMeridiemAbbreviation": { "notUsed": "Sindhi time format does not use p.m. indicator" },
|
||||||
"openAppDrawerTooltip": "اوپن جي مينڊيٽ مينيو",
|
"openAppDrawerTooltip": "اوپن جي مينڊيٽ مينيو",
|
||||||
"backButtonTooltip": "پوئتي",
|
"backButtonTooltip": "پوئتي",
|
||||||
"closeButtonTooltip": "بند ڪريو",
|
"closeButtonTooltip": "بند ڪريو",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user