mirror of
https://github.com/flutter/flutter.git
synced 2026-01-09 07:51:35 +08:00
WIP Commits separated as follows: - Update lints in analysis_options files - Run `dart fix --apply` - Clean up leftover analysis issues - Run `dart format .` in the right places. Local analysis and testing passes. Checking CI now. Part of https://github.com/flutter/flutter/issues/178827 - Adoption of flutter_lints in examples/api coming in a separate change (cc @loic-sharma) ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
218 lines
7.9 KiB
Dart
218 lines
7.9 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 errorMessages = StringBuffer();
|
|
|
|
if (!file.existsSync()) {
|
|
errorMessages.writeln('English localizations do not exist: $file');
|
|
throw ValidationError(errorMessages.toString());
|
|
}
|
|
|
|
final 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 description = atResource['description'] as String?;
|
|
if (description == null && !optional) {
|
|
errorMessages.writeln('No description specified for $atResourceId');
|
|
}
|
|
|
|
final plural = atResource['plural'] as String?;
|
|
final String resourceId = atResourceId.substring(1);
|
|
if (plural != null) {
|
|
final 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 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 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 canonicalKeys = Set<String>.from(canonicalLocalizations.keys);
|
|
final errorMessages = StringBuffer();
|
|
var 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 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 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());
|
|
}
|
|
}
|