mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Rebase ios-experimental branch onto main. This will make the PRs experimenting with newer versions of Xcode (like https://github.com/flutter/flutter/pull/173123) smaller and easier to reason about. Rebases #168860 and #170274 ``` $ git rebase main -Xtheirs ``` --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Co-authored-by: Siva <a-siva@users.noreply.github.com> Co-authored-by: engine-flutter-autoroll <engine-flutter-autoroll@skia.org> Co-authored-by: Jamil Saadeh <jssaadeh@outlook.com> Co-authored-by: Dara Adedeji <76637177+SunkenInTime@users.noreply.github.com> Co-authored-by: Greg Price <gnprice@gmail.com> Co-authored-by: Ben Konyi <bkonyi@google.com> Co-authored-by: Ricardo Dalarme <ricardodalarme@outlook.com> Co-authored-by: Flutter GitHub Bot <fluttergithubbot@gmail.com> Co-authored-by: Justin McCandless <jmccandless@google.com> Co-authored-by: Alex Talebi <31685655+SalehTZ@users.noreply.github.com> Co-authored-by: Qun Cheng <36861262+QuncCccccc@users.noreply.github.com> Co-authored-by: Mouad Debbar <mdebbar@google.com> Co-authored-by: Zuckjet <1083941774@qq.com> Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com> Co-authored-by: auto-submit[bot] <98614782+auto-submit[bot]@users.noreply.github.com> Co-authored-by: auto-submit[bot] <flutter-engprod-team@google.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: yim <ybz975218925@gmail.com> Co-authored-by: bufffun <chenmingding.cmd@alibaba-inc.com> Co-authored-by: Chinmay Garde <chinmaygarde@google.com> Co-authored-by: Hannah Jin <jhy03261997@gmail.com> Co-authored-by: Kate Lovett <katelovett@google.com> Co-authored-by: Valentin Vignal <32538273+ValentinVignal@users.noreply.github.com> Co-authored-by: Derek Xu <derekx@google.com> Co-authored-by: Yash Dhrangdhariya <72062416+Yash-Dhrangdhariya@users.noreply.github.com> Co-authored-by: bungeman <bungeman@chromium.org> Co-authored-by: Ahmed Mohamed Sameh <ahmedsameha1@gmail.com> Co-authored-by: John "codefu" McDole <codefu@google.com> Co-authored-by: Dmitry Grand <dmgr@google.com> Co-authored-by: Kostia Sokolovskyi <sokolovskyi.konstantin@gmail.com> Co-authored-by: Reid Baker <1063596+reidbaker@users.noreply.github.com> Co-authored-by: Matthew Kosarek <matt.kosarek@canonical.com> Co-authored-by: Jason Simmons <jason-simmons@users.noreply.github.com> Co-authored-by: Jim Graham <flar@google.com> Co-authored-by: Michael Goderbauer <goderbauer@google.com> Co-authored-by: Gray Mackall <34871572+gmackall@users.noreply.github.com> Co-authored-by: Gray Mackall <mackall@google.com> Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com> Co-authored-by: Jon Ihlas <jon.i@hotmail.fr> Co-authored-by: Micael Cid <micaelcid10@gmail.com> Co-authored-by: Alexander Aprelev <aam@google.com> Co-authored-by: hellohuanlin <41930132+hellohuanlin@users.noreply.github.com> Co-authored-by: Luke Memet <1598289+lukemmtt@users.noreply.github.com> Co-authored-by: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> Co-authored-by: Mairramer <50643541+Mairramer@users.noreply.github.com> Co-authored-by: Florin Malita <fmalita@gmail.com> Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com> Co-authored-by: Salem Iranloye <127918074+salemiranloye@users.noreply.github.com> Co-authored-by: Kevin Moore <kevmoo@google.com> Co-authored-by: Sydney Bao <sydneybao@google.com> Co-authored-by: Wdestroier <Wdestroier@gmail.com> Co-authored-by: Matt Boetger <matt.boetger@gmail.com> Co-authored-by: Reid Baker <reidbaker@google.com> Co-authored-by: Victor Sanni <victorsanniay@gmail.com> Co-authored-by: Jessy Yameogo <jessy.yameogo@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: romain.gyh <11901536+romaingyh@users.noreply.github.com> Co-authored-by: Robert Ancell <robert.ancell@canonical.com> Co-authored-by: TheLastFlame <131446187+TheLastFlame@users.noreply.github.com> Co-authored-by: masato <returnymgstokh@icloud.com> Co-authored-by: Albin PK <56157868+albinpk@users.noreply.github.com> Co-authored-by: Huy <huy@nevercode.io> Co-authored-by: Matan Lurey <matanlurey@users.noreply.github.com> Co-authored-by: Azat Chorekliyev <azat24680@gmail.com> Co-authored-by: EdwynZN <edwinzn9@gmail.com> Co-authored-by: Bruno Leroux <bruno.leroux@gmail.com> Co-authored-by: Dev TtangKong <ttankkeo112@gmail.com> Co-authored-by: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Co-authored-by: Houssem Eddine Fadhli <houssemeddinefadhli81@gmail.com>
222 lines
8.1 KiB
Dart
222 lines
8.1 KiB
Dart
// Copyright 2014 The Flutter 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:convert' show json;
|
|
import 'dart:io';
|
|
|
|
import 'localizations_utils.dart';
|
|
|
|
// The first suffix in kPluralSuffixes must be "Other". "Other" is special
|
|
// because it's the only one that is required.
|
|
const List<String> kPluralSuffixes = <String>['Other', 'Zero', 'One', 'Two', 'Few', 'Many'];
|
|
final RegExp kPluralRegexp = RegExp(r'(\w*)(' + kPluralSuffixes.skip(1).join(r'|') + r')$');
|
|
|
|
class ValidationError implements Exception {
|
|
ValidationError(this.message);
|
|
final String message;
|
|
@override
|
|
String toString() => message;
|
|
}
|
|
|
|
/// Sanity checking of the @foo metadata in the English translations, *_en.arb.
|
|
///
|
|
/// - For each foo, resource, there must be a corresponding @foo.
|
|
/// - For each @foo resource, there must be a corresponding foo, except
|
|
/// for plurals, for which there must be a fooOther.
|
|
/// - Each @foo resource must have a Map value with a String valued
|
|
/// description entry.
|
|
///
|
|
/// Throws an exception upon failure.
|
|
void validateEnglishLocalizations(File file) {
|
|
final StringBuffer errorMessages = StringBuffer();
|
|
|
|
if (!file.existsSync()) {
|
|
errorMessages.writeln('English localizations do not exist: $file');
|
|
throw ValidationError(errorMessages.toString());
|
|
}
|
|
|
|
final Map<String, dynamic> bundle = json.decode(file.readAsStringSync()) as Map<String, dynamic>;
|
|
|
|
for (final String resourceId in bundle.keys) {
|
|
if (resourceId.startsWith('@')) {
|
|
continue;
|
|
}
|
|
|
|
if (bundle['@$resourceId'] != null) {
|
|
continue;
|
|
}
|
|
|
|
bool checkPluralResource(String suffix) {
|
|
final int suffixIndex = resourceId.indexOf(suffix);
|
|
return suffixIndex != -1 && bundle['@${resourceId.substring(0, suffixIndex)}'] != null;
|
|
}
|
|
|
|
if (kPluralSuffixes.any(checkPluralResource)) {
|
|
continue;
|
|
}
|
|
|
|
errorMessages.writeln('A value was not specified for @$resourceId');
|
|
}
|
|
|
|
for (final String atResourceId in bundle.keys) {
|
|
if (!atResourceId.startsWith('@')) {
|
|
continue;
|
|
}
|
|
|
|
final dynamic atResourceValue = bundle[atResourceId];
|
|
final Map<String, dynamic>? atResource = atResourceValue is Map<String, dynamic>
|
|
? atResourceValue
|
|
: null;
|
|
if (atResource == null) {
|
|
errorMessages.writeln('A map value was not specified for $atResourceId');
|
|
continue;
|
|
}
|
|
|
|
final bool optional = atResource.containsKey('optional');
|
|
final String? description = atResource['description'] as String?;
|
|
if (description == null && !optional) {
|
|
errorMessages.writeln('No description specified for $atResourceId');
|
|
}
|
|
|
|
final String? plural = atResource['plural'] as String?;
|
|
final String resourceId = atResourceId.substring(1);
|
|
if (plural != null) {
|
|
final String resourceIdOther = '${resourceId}Other';
|
|
if (!bundle.containsKey(resourceIdOther)) {
|
|
errorMessages.writeln('Default plural resource $resourceIdOther undefined');
|
|
}
|
|
} else {
|
|
if (!optional && !bundle.containsKey(resourceId)) {
|
|
errorMessages.writeln('No matching $resourceId defined for $atResourceId');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (errorMessages.isNotEmpty) {
|
|
throw ValidationError(errorMessages.toString());
|
|
}
|
|
}
|
|
|
|
/// This removes undefined localizations (localizations that aren't present in
|
|
/// the canonical locale anymore) by:
|
|
///
|
|
/// 1. Looking up the canonical (English, in this case) localizations.
|
|
/// 2. For each locale, getting the resources.
|
|
/// 3. Determining the set of keys that aren't plural variations (we're only
|
|
/// interested in the base terms being translated and not their variants)
|
|
/// 4. Determining the set of invalid keys; that is those that are (non-plural)
|
|
/// keys in the resources for this locale, but which _aren't_ keys in the
|
|
/// canonical list.
|
|
/// 5. Removes the invalid mappings from this resource's locale.
|
|
void removeUndefinedLocalizations(Map<LocaleInfo, Map<String, String>> localeToResources) {
|
|
final Map<String, String> canonicalLocalizations =
|
|
localeToResources[LocaleInfo.fromString('en')]!;
|
|
final Set<String> canonicalKeys = Set<String>.from(canonicalLocalizations.keys);
|
|
|
|
localeToResources.forEach((LocaleInfo locale, Map<String, String> resources) {
|
|
bool isPluralVariation(String key) {
|
|
final Match? pluralMatch = kPluralRegexp.firstMatch(key);
|
|
if (pluralMatch == null) {
|
|
return false;
|
|
}
|
|
final String? prefix = pluralMatch[1];
|
|
return resources.containsKey('${prefix}Other');
|
|
}
|
|
|
|
final Set<String> keys = Set<String>.from(
|
|
resources.keys.where((String key) => !isPluralVariation(key)),
|
|
);
|
|
|
|
final Set<String> invalidKeys = keys.difference(canonicalKeys);
|
|
resources.removeWhere((String key, String value) => invalidKeys.contains(key));
|
|
});
|
|
}
|
|
|
|
/// 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, throws an exception.
|
|
void validateLocalizations(
|
|
Map<LocaleInfo, Map<String, String>> localeToResources,
|
|
Map<LocaleInfo, Map<String, dynamic>> localeToAttributes, {
|
|
bool removeUndefined = false,
|
|
}) {
|
|
final Map<String, String> canonicalLocalizations =
|
|
localeToResources[LocaleInfo.fromString('en')]!;
|
|
final Set<String> canonicalKeys = Set<String>.from(canonicalLocalizations.keys);
|
|
final StringBuffer errorMessages = StringBuffer();
|
|
bool explainMissingKeys = false;
|
|
for (final LocaleInfo 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 Match? pluralMatch = kPluralRegexp.firstMatch(key);
|
|
if (pluralMatch == null) {
|
|
return false;
|
|
}
|
|
final String? prefix = pluralMatch[1];
|
|
return resources.containsKey('${prefix}Other');
|
|
}
|
|
|
|
final Set<String> keys = 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 && !removeUndefined) {
|
|
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 == 1) {
|
|
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('}');
|
|
}
|
|
throw ValidationError(errorMessages.toString());
|
|
}
|
|
}
|