🐛 Normalize generated file paths for the l10n generator (#169467)

Multiple places use non-normalized file paths when generating the
localization files.

The PR normalizes file paths when generating the file list JSON file and
the dependency file.

Fixes https://github.com/flutter/flutter/issues/163591

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] 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.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- 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
This commit is contained in:
Alex Li 2025-05-27 07:25:42 +08:00 committed by GitHub
parent 8e76fb2946
commit 4e75d56bda
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 107 additions and 16 deletions

View File

@ -58,6 +58,7 @@ class GenerateLocalizationsTarget extends Target {
final LocalizationOptions options = parseLocalizationsOptionsFromYAML(
file: configFile,
logger: environment.logger,
fileSystem: environment.fileSystem,
defaultArbDir: defaultArbDir,
defaultSyntheticPackage: !featureFlags.isExplicitPackageDependenciesEnabled,
);

View File

@ -260,6 +260,7 @@ class GenerateLocalizationsCommand extends FlutterCommand {
options = parseLocalizationsOptionsFromYAML(
file: _fileSystem.file('l10n.yaml'),
logger: _logger,
fileSystem: _fileSystem,
defaultArbDir: defaultArbDir,
defaultSyntheticPackage: !featureFlags.isExplicitPackageDependenciesEnabled,
);

View File

@ -1000,6 +1000,14 @@ class LocalizationsGenerator {
return true;
}
void _addToFileList(List<String> fileList, String path) {
fileList.add(_fs.path.normalize(path));
}
void _addAllToFileList(List<String> fileList, Iterable<String> paths) {
fileList.addAll(paths.map(_fs.path.normalize));
}
// Load _allMessages from templateArbFile and _allBundles from all of the ARB
// files in inputDirectory. Also initialized: supportedLocales.
void loadResources() {
@ -1030,7 +1038,8 @@ class LocalizationsGenerator {
.toList();
hadErrors = _allMessages.any((Message message) => message.hadErrors);
if (inputsAndOutputsListFile != null) {
_inputFileList.addAll(
_addAllToFileList(
_inputFileList,
_allBundles.bundles.map((AppResourceBundle bundle) {
return bundle.file.absolute.path;
}),
@ -1492,7 +1501,7 @@ The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", "
// Generate the required files for localizations.
_languageFileMap.forEach((File file, String contents) {
file.writeAsStringSync(useCRLF ? contents.replaceAll('\n', '\r\n') : contents);
_outputFileList.add(file.absolute.path);
_addToFileList(_outputFileList, file.absolute.path);
});
baseOutputFile.writeAsStringSync(
@ -1527,7 +1536,7 @@ The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", "
);
}
final File? inputsAndOutputsListFileLocal = inputsAndOutputsListFile;
_outputFileList.add(baseOutputFile.absolute.path);
_addToFileList(_outputFileList, baseOutputFile.absolute.path);
if (inputsAndOutputsListFileLocal != null) {
// Generate a JSON file containing the inputs and outputs of the gen_l10n script.
if (!inputsAndOutputsListFileLocal.existsSync()) {
@ -1548,7 +1557,7 @@ The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", "
void _generateUntranslatedMessagesFile(Logger logger, File untranslatedMessagesFile) {
if (_unimplementedMessages.isEmpty) {
untranslatedMessagesFile.writeAsStringSync('{}');
_outputFileList.add(untranslatedMessagesFile.absolute.path);
_addToFileList(_outputFileList, untranslatedMessagesFile.absolute.path);
return;
}
@ -1576,6 +1585,6 @@ The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", "
resultingFile += '}\n';
untranslatedMessagesFile.writeAsStringSync(resultingFile);
_outputFileList.add(untranslatedMessagesFile.absolute.path);
_addToFileList(_outputFileList, untranslatedMessagesFile.absolute.path);
}
}

View File

@ -479,6 +479,7 @@ class LocalizationOptions {
LocalizationOptions parseLocalizationsOptionsFromYAML({
required File file,
required Logger logger,
required FileSystem fileSystem,
required String defaultArbDir,
required bool defaultSyntheticPackage,
}) {
@ -497,14 +498,24 @@ LocalizationOptions parseLocalizationsOptionsFromYAML({
throw Exception();
}
return LocalizationOptions(
arbDir: _tryReadUri(yamlNode, 'arb-dir', logger)?.path ?? defaultArbDir,
outputDir: _tryReadUri(yamlNode, 'output-dir', logger)?.path,
templateArbFile: _tryReadUri(yamlNode, 'template-arb-file', logger)?.path,
outputLocalizationFile: _tryReadUri(yamlNode, 'output-localization-file', logger)?.path,
untranslatedMessagesFile: _tryReadUri(yamlNode, 'untranslated-messages-file', logger)?.path,
arbDir: _tryReadFilePath(yamlNode, 'arb-dir', logger, fileSystem) ?? defaultArbDir,
outputDir: _tryReadFilePath(yamlNode, 'output-dir', logger, fileSystem),
templateArbFile: _tryReadFilePath(yamlNode, 'template-arb-file', logger, fileSystem),
outputLocalizationFile: _tryReadFilePath(
yamlNode,
'output-localization-file',
logger,
fileSystem,
),
untranslatedMessagesFile: _tryReadFilePath(
yamlNode,
'untranslated-messages-file',
logger,
fileSystem,
),
outputClass: _tryReadString(yamlNode, 'output-class', logger),
header: _tryReadString(yamlNode, 'header', logger),
headerFile: _tryReadUri(yamlNode, 'header-file', logger)?.path,
headerFile: _tryReadFilePath(yamlNode, 'header-file', logger, fileSystem),
useDeferredLoading: _tryReadBool(yamlNode, 'use-deferred-loading', logger),
preferredSupportedLocales: _tryReadStringList(yamlNode, 'preferred-supported-locales', logger),
syntheticPackage:
@ -596,8 +607,8 @@ List<String>? _tryReadStringList(YamlMap yamlMap, String key, Logger logger) {
throw Exception();
}
// Try to read a valid `Uri` or null from `yamlMap`, otherwise throw.
Uri? _tryReadUri(YamlMap yamlMap, String key, Logger logger) {
// Try to read a valid file `Uri` or null from `yamlMap` to file path, otherwise throw.
String? _tryReadFilePath(YamlMap yamlMap, String key, Logger logger, FileSystem fileSystem) {
final String? value = _tryReadString(yamlMap, key, logger);
if (value == null) {
return null;
@ -606,5 +617,5 @@ Uri? _tryReadUri(YamlMap yamlMap, String key, Logger logger) {
if (uri == null) {
logger.printError('"$value" must be a relative file URI');
}
return uri;
return uri != null ? fileSystem.path.normalize(uri.path) : null;
}

View File

@ -7,6 +7,7 @@ import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/depfile.dart' show Depfile;
import 'package:flutter_tools/src/build_system/targets/localizations.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/generate_localizations.dart';
@ -516,6 +517,42 @@ format: true
},
);
testUsingContext('generates normalized input & output file paths', () async {
final File arbFile = fileSystem.file(fileSystem.path.join('lib', 'l10n', 'app_en.arb'))
..createSync(recursive: true);
arbFile.writeAsStringSync('''
{
"helloWorld": "Hello, World!"
}''');
final File configFile = fileSystem.file('l10n.yaml')..createSync();
// Writing both forward and backward slashes to test both cases.
configFile.writeAsStringSync(r'''
arb-dir: lib/l10n
output-dir: lib\l10n
format: false
''');
final File pubspecFile = fileSystem.file('pubspec.yaml')..createSync();
pubspecFile.writeAsStringSync(BasicProjectWithFlutterGen().pubspec);
processManager.addCommand(const FakeCommand(command: <String>[]));
final Environment environment = Environment.test(
fileSystem.currentDirectory,
artifacts: artifacts,
processManager: processManager,
fileSystem: fileSystem,
logger: BufferLogger.test(),
);
const Target buildTarget = GenerateLocalizationsTarget();
await buildTarget.build(environment);
final File dependencyFile = environment.buildDir.childFile(buildTarget.depfiles.single);
final Depfile depfile = environment.depFileService.parse(dependencyFile);
final String oppositeSeparator = fileSystem.path.separator == '/' ? r'\' : '/';
expect(depfile.inputs, everyElement(isNot(contains(oppositeSeparator))));
expect(depfile.outputs, everyElement(isNot(contains(oppositeSeparator))));
});
testUsingContext(
'nullable-getter defaults to true',
() async {

View File

@ -51,6 +51,7 @@ nullable-getter: false
final LocalizationOptions options = parseLocalizationsOptionsFromYAML(
file: configFile,
logger: BufferLogger.test(),
fileSystem: fileSystem,
defaultArbDir: fileSystem.path.join('lib', 'l10n'),
defaultSyntheticPackage: true,
);
@ -90,6 +91,7 @@ nullable-getter: false
final LocalizationOptions options = parseLocalizationsOptionsFromYAML(
file: configFile,
logger: BufferLogger.test(),
fileSystem: fileSystem,
defaultArbDir: fileSystem.path.join('lib', 'l10n'),
defaultSyntheticPackage: true,
);
@ -118,6 +120,7 @@ nullable-getter: false
final LocalizationOptions options = parseLocalizationsOptionsFromYAML(
file: configFile,
logger: BufferLogger.test(),
fileSystem: fileSystem,
defaultArbDir: fileSystem.path.join('lib', 'l10n'),
defaultSyntheticPackage: false,
);
@ -136,6 +139,7 @@ preferred-supported-locales: ['en_US', 'de']
final LocalizationOptions options = parseLocalizationsOptionsFromYAML(
file: configFile,
logger: BufferLogger.test(),
fileSystem: fileSystem,
defaultArbDir: fileSystem.path.join('lib', 'l10n'),
defaultSyntheticPackage: true,
);
@ -156,6 +160,7 @@ use-deferred-loading: string
() => parseLocalizationsOptionsFromYAML(
file: configFile,
logger: BufferLogger.test(),
fileSystem: fileSystem,
defaultArbDir: fileSystem.path.join('lib', 'l10n'),
defaultSyntheticPackage: true,
),
@ -174,6 +179,7 @@ template-arb-file: {name}_en.arb
() => parseLocalizationsOptionsFromYAML(
file: configFile,
logger: BufferLogger.test(),
fileSystem: fileSystem,
defaultArbDir: fileSystem.path.join('lib', 'l10n'),
defaultSyntheticPackage: true,
),

View File

@ -7,6 +7,7 @@ import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/globals.dart' as globals show platform;
import 'package:flutter_tools/src/localizations/gen_l10n.dart';
import 'package:flutter_tools/src/localizations/gen_l10n_types.dart';
import 'package:flutter_tools/src/localizations/localizations_utils.dart';
@ -89,8 +90,10 @@ void main() {
bool relaxSyntax = false,
bool useNamedParameters = false,
void Function(Directory)? setup,
FileSystem? fileSystem,
}) {
final Directory l10nDirectory = fs.directory(defaultL10nPath)..createSync(recursive: true);
final Directory l10nDirectory = (fileSystem ?? fs).directory(defaultL10nPath)
..createSync(recursive: true);
for (final String locale in localeToArbFile.keys) {
l10nDirectory.childFile('app_$locale.arb').writeAsStringSync(localeToArbFile[locale]!);
}
@ -98,7 +101,7 @@ void main() {
setup(l10nDirectory);
}
return LocalizationsGenerator(
fileSystem: fs,
fileSystem: fileSystem ?? fs,
inputPathString: l10nDirectory.path,
outputPathString: outputPathString ?? l10nDirectory.path,
templateArbFileName: defaultTemplateArbFileName,
@ -613,6 +616,29 @@ void main() {
);
});
testUsingContext('generates normalized input & output file paths', () {
final FileSystem fs = MemoryFileSystem.test(
style: globals.platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix,
);
setupLocalizations(
<String, String>{'en': singleMessageArbFileString, 'es': singleEsMessageArbFileString},
fileSystem: fs,
inputsAndOutputsListPath: defaultL10nPath,
);
final File inputsAndOutputsList = fs.file(
fs.path.join(defaultL10nPath, 'gen_l10n_inputs_and_outputs.json'),
);
expect(inputsAndOutputsList.existsSync(), isTrue);
final Map<String, dynamic> jsonResult =
json.decode(inputsAndOutputsList.readAsStringSync()) as Map<String, dynamic>;
final String oppositeSeparator = globals.platform.isWindows ? '/' : r'\';
final List<dynamic> inputList = jsonResult['inputs'] as List<dynamic>;
expect(inputList, everyElement(isNot(contains(oppositeSeparator))));
final List<dynamic> outputList = jsonResult['outputs'] as List<dynamic>;
expect(outputList, everyElement(isNot(contains(oppositeSeparator))));
});
testWithoutContext('setting both a headerString and a headerFile should fail', () {
expect(
() {