diff --git a/dev/tools/localization/bin/gen_l10n.dart b/dev/tools/localization/bin/gen_l10n.dart index ed8c2bb4265..bd145f8400a 100644 --- a/dev/tools/localization/bin/gen_l10n.dart +++ b/dev/tools/localization/bin/gen_l10n.dart @@ -63,6 +63,9 @@ Future main(List arguments) async { exit(0); } + final String flutterRoot = Platform.environment['FLUTTER_ROOT']; + final String flutterBin = Platform.isWindows ? 'flutter.bat' : 'flutter'; + final String flutterPath = flutterRoot == null ? flutterBin : path.join(flutterRoot, 'bin', flutterBin); final String arbPathString = results['arb-dir'] as String; final String outputFileString = results['output-localization-file'] as String; final String templateArbFileName = results['template-arb-file'] as String; @@ -91,13 +94,13 @@ Future main(List arguments) async { exitWithError(e.message); } - final ProcessResult pubGetResult = await Process.run('flutter', ['pub', 'get']); + final ProcessResult pubGetResult = await Process.run(flutterPath, ['pub', 'get']); if (pubGetResult.exitCode != 0) { stderr.write(pubGetResult.stderr); exit(1); } - final ProcessResult generateFromArbResult = await Process.run('flutter', [ + final ProcessResult generateFromArbResult = await Process.run(flutterPath, [ 'pub', 'run', 'intl_translation:generate_from_arb', diff --git a/dev/tools/localization/gen_l10n.dart b/dev/tools/localization/gen_l10n.dart index d28542a33b3..f75973f8053 100644 --- a/dev/tools/localization/gen_l10n.dart +++ b/dev/tools/localization/gen_l10n.dart @@ -223,7 +223,7 @@ String genSimpleMethod(Message message) { String genSimpleMethodMessage() { String messageValue = message.value; for (final Placeholder placeholder in message.placeholders) { - messageValue = messageValue.replaceAll('{${placeholder.name}}', '\$${placeholder.name}'); + messageValue = messageValue.replaceAll('{${placeholder.name}}', '\${${placeholder.name}}'); } final String rawMessage = generateString(messageValue); // "r'...'" return rawMessage.substring(1); @@ -300,9 +300,9 @@ String generatePluralMethod(Message message) { String argValue = match.group(2); for (final Placeholder placeholder in message.placeholders) { if (placeholder.requiresFormatting) { - argValue = argValue.replaceAll('#${placeholder.name}#', '\$${placeholder.name}String'); + argValue = argValue.replaceAll('#${placeholder.name}#', '\${${placeholder.name}String}'); } else { - argValue = argValue.replaceAll('#${placeholder.name}#', '\$${placeholder.name}'); + argValue = argValue.replaceAll('#${placeholder.name}#', '\${${placeholder.name}}'); } } intlMethodArgs.add("${pluralIds[pluralKey]}: '$argValue'"); diff --git a/dev/tools/localization/gen_l10n_templates.dart b/dev/tools/localization/gen_l10n_templates.dart index 23ba1216c9d..536aa3622bf 100644 --- a/dev/tools/localization/gen_l10n_templates.dart +++ b/dev/tools/localization/gen_l10n_templates.dart @@ -113,8 +113,11 @@ import 'messages_all.dart'; /// Select and expand the newly-created Localizations item then, for each /// locale your application supports, add a new item and select the locale /// you wish to add from the pop-up menu in the Value field. This list should -/// be consistent with the languages listed in the @className.supportedLocales +/// be consistent with the languages listed in the @(className).supportedLocales /// property. + +// ignore_for_file: unnecessary_brace_in_string_interps + class @(className) { @(className)(Locale locale) : _localeName = Intl.canonicalizedLocale(locale.toString()); @@ -129,7 +132,7 @@ class @(className) { return Localizations.of<@(className)>(context, @(className)); } - static const LocalizationsDelegate<@(className)> delegate = _@(classNameDelegate)(); + static const LocalizationsDelegate<@(className)> delegate = _@(className)Delegate(); /// A list of this localizations delegate along with the default localizations /// delegates. diff --git a/dev/tools/test/localization/gen_l10n_test.dart b/dev/tools/test/localization/gen_l10n_test.dart index c327e6c7763..ec88a33fb61 100644 --- a/dev/tools/test/localization/gen_l10n_test.dart +++ b/dev/tools/test/localization/gen_l10n_test.dart @@ -657,7 +657,7 @@ void main() { generator.classMethods.first, ''' String itemNumber(Object value) { return Intl.message( - \'Item \$value\', + \'Item \${value}\', locale: _localeName, name: 'itemNumber', desc: r\'Item placement in list.\', @@ -710,7 +710,7 @@ void main() { String springBegins(Object springStartDate) { return Intl.message( - \'Spring begins on \$springStartDate\', + \'Spring begins on \${springStartDate}\', locale: _localeName, name: \'springBegins\', desc: r\'The first day of spring\', @@ -837,7 +837,7 @@ void main() { String springGreetings(Object springStartDate, Object helloWorld) { return Intl.message( - \'Since it\' "\'" r\'s \$springStartDate, it\' "\'" r\'s finally spring! \$helloWorld!\', + \'Since it\' "\'" r\'s \${springStartDate}, it\' "\'" r\'s finally spring! \${helloWorld}!\', locale: _localeName, name: \'springGreetings\', desc: r\'A realization that it\' "\'" r\'s finally the spring season, followed by a greeting.\', @@ -897,7 +897,7 @@ void main() { String springRange(Object springStartDate, Object springEndDate) { return Intl.message( - \'Spring begins on \$springStartDate and ends on \$springEndDate\', + \'Spring begins on \${springStartDate} and ends on \${springEndDate}\', locale: _localeName, name: \'springRange\', desc: r\'The range of dates for spring in the year\', @@ -954,10 +954,10 @@ void main() { locale: _localeName, name: \'helloWorlds\', args: [count, currentDate], - one: \'Hello World, today is \$currentDateString\', - two: \'Hello two worlds, today is \$currentDateString\', - many: \'Hello all \$count worlds, today is \$currentDateString\', - other: \'Hello other \$count worlds, today is \$currentDateString\' + one: \'Hello World, today is \${currentDateString}\', + two: \'Hello two worlds, today is \${currentDateString}\', + many: \'Hello all \${count} worlds, today is \${currentDateString}\', + other: \'Hello other \${count} worlds, today is \${currentDateString}\' ); } return helloWorlds(count, currentDateString); @@ -1009,7 +1009,7 @@ void main() { String courseCompletion(Object progress) { return Intl.message( - \'You have completed \$progress of the course.\', + \'You have completed \${progress} of the course.\', locale: _localeName, name: \'courseCompletion\', desc: r\'The amount of progress the student has made in their class.\', @@ -1079,7 +1079,7 @@ void main() { String courseCompletion(Object progress) { return Intl.message( - \'You have completed \$progress of the course.\', + \'You have completed \${progress} of the course.\', locale: _localeName, name: \'courseCompletion\', desc: r\'The amount of progress the student has made in their class.\', @@ -1139,7 +1139,7 @@ void main() { String courseCompletion(Object progress) { return Intl.message( - \'You have completed \$progress of the course.\', + \'You have completed \${progress} of the course.\', locale: _localeName, name: \'courseCompletion\', desc: r\'The amount of progress the student has made in their class.\', @@ -1232,9 +1232,9 @@ void main() { zero: 'Hello', one: 'Hello World', two: 'Hello two worlds', - few: 'Hello \$count worlds', - many: 'Hello all \$count worlds', - other: 'Hello other \$count worlds' + few: 'Hello \${count} worlds', + many: 'Hello all \${count} worlds', + other: 'Hello other \${count} worlds' ); } ''' @@ -1280,11 +1280,11 @@ void main() { name: 'helloWorlds', args: [count, adjective], zero: 'Hello', - one: 'Hello \$adjective World', - two: 'Hello two \$adjective worlds', - few: 'Hello \$count \$adjective worlds', - many: 'Hello all \$count \$adjective worlds', - other: 'Hello other \$count \$adjective worlds' + one: 'Hello \${adjective} World', + two: 'Hello two \${adjective} worlds', + few: 'Hello \${count} \${adjective} worlds', + many: 'Hello all \${count} \${adjective} worlds', + other: 'Hello other \${count} \${adjective} worlds' ); } ''' @@ -1336,10 +1336,10 @@ void main() { locale: _localeName, name: \'helloWorlds\', args: [count, currentDate], - one: \'Hello World, today is \$currentDateString\', - two: \'Hello two worlds, today is \$currentDateString\', - many: \'Hello all \$count worlds, today is \$currentDateString\', - other: \'Hello other \$count worlds, today is \$currentDateString\' + one: \'Hello World, today is \${currentDateString}\', + two: \'Hello two worlds, today is \${currentDateString}\', + many: \'Hello all \${count} worlds, today is \${currentDateString}\', + other: \'Hello other \${count} worlds, today is \${currentDateString}\' ); } return helloWorlds(count, currentDateString); @@ -1395,10 +1395,10 @@ void main() { locale: _localeName, name: \'helloWorlds\', args: [count, population], - one: \'Hello World of \$populationString citizens\', - two: \'Hello two worlds with \$populationString total citizens\', - many: \'Hello all \$count worlds, with a total of \$populationString citizens\', - other: \'Hello other \$count worlds, with a total of \$populationString citizens\' + one: \'Hello World of \${populationString} citizens\', + two: \'Hello two worlds with \${populationString} total citizens\', + many: \'Hello all \${count} worlds, with a total of \${populationString} citizens\', + other: \'Hello other \${count} worlds, with a total of \${populationString} citizens\' ); } return helloWorlds(count, populationString); diff --git a/packages/flutter_tools/test/integration.shard/gen_l10n_test.dart b/packages/flutter_tools/test/integration.shard/gen_l10n_test.dart new file mode 100644 index 00000000000..741f563efcd --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/gen_l10n_test.dart @@ -0,0 +1,104 @@ +// 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:async'; + +import 'package:file/file.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; +import 'package:process/process.dart'; + +import '../src/common.dart'; +import 'test_data/gen_l10n_project.dart'; +import 'test_driver.dart'; +import 'test_utils.dart'; + +// Verify that the code generated by gen_l10n executes correctly. +// It can fail if gen_l10n produces a lib/l10n/app_localizations.dart that: +// - Does not analyze cleanly. +// - Can't be processed by the intl_translation:generate_from_arb tool. +// The generate_from_arb step can take close to a minute on a lightly +// loaded workstation, so the test could time out on a heavily loaded bot. +void main() { + Directory tempDir; + final GenL10nProject _project = GenL10nProject(); + FlutterRunTestDriver _flutter; + + setUp(() async { + tempDir = createResolvedTempDirectorySync('gen_l10n_test.'); + await _project.setUpIn(tempDir); + _flutter = FlutterRunTestDriver(tempDir); + }); + + tearDown(() async { + await _flutter.stop(); + tryToDelete(tempDir); + }); + + void runCommand(List command) { + final ProcessResult result = const LocalProcessManager().runSync( + command, + workingDirectory: tempDir.path, + environment: { 'FLUTTER_ROOT': getFlutterRoot() }, + ); + if (result.exitCode != 0) { + throw Exception('FAILED [${result.exitCode}]: ${command.join(' ')}\n${result.stderr}\n${result.stdout}'); + } + } + + test('generated l10n classes produce expected localized strings', () async { + // Get the intl packages before running gen_l10n. + final String flutterBin = globals.platform.isWindows ? 'flutter.bat' : 'flutter'; + final String flutterPath = globals.fs.path.join(getFlutterRoot(), 'bin', flutterBin); + runCommand([flutterPath, 'pub', 'get']); + + // Generate lib/l10n/app_localizations.dart + final String genL10nPath = globals.fs.path.join(getFlutterRoot(), 'dev', 'tools', 'localization', 'bin', 'gen_l10n.dart'); + final String dartBin = globals.platform.isWindows ? 'dart.exe' : 'dart'; + final String dartPath = globals.fs.path.join(getFlutterRoot(), 'bin', 'cache', 'dart-sdk', 'bin', dartBin); + runCommand([dartPath, genL10nPath]); + + // Run the app defined in GenL10nProject.main and wait for it to + // send '#l10n END' to its stdout. + final Completer l10nEnd = Completer(); + final StringBuffer stdout = StringBuffer(); + final StreamSubscription subscription = _flutter.stdout.listen((String line) { + if (line.contains('#l10n')) { + stdout.writeln(line.substring(line.indexOf('#l10n'))); + } + if (line.contains('#l10n END')) { + l10nEnd.complete(); + } + }); + await _flutter.run(); + await l10nEnd.future; + await subscription.cancel(); + expect(stdout.toString(), + '#l10n 0 (Hello World)\n' + '#l10n 1 (Hello World)\n' + '#l10n 2 (Hello World)\n' + '#l10n 3 (Hello World on Friday, January 1, 1960)\n' + '#l10n 4 (Hello world argument on 1/1/1960 at 00:00)\n' + '#l10n 5 (Hello World from 1960 to 2020)\n' + '#l10n 6 (Hello for 123)\n' + '#l10n 7 (Hello for price USD123.00)\n' + '#l10n 8 (Hello)\n' + '#l10n 9 (Hello World)\n' + '#l10n 10 (Hello two worlds)\n' + '#l10n 11 (Hello on Friday, January 1, 1960)\n' + '#l10n 12 (Hello World, on Friday, January 1, 1960)\n' + '#l10n 13 (Hello two worlds, on Friday, January 1, 1960)\n' + '#l10n 14 (Hello)\n' + '#l10n 15 (Hello new World)\n' + '#l10n 16 (Hello two new worlds)\n' + '#l10n 17 (Hello other 0 worlds, with a total of 100 citizens)\n' + '#l10n 18 (Hello World of 101 citizens)\n' + '#l10n 19 (Hello two worlds with 102 total citizens)\n' + '#l10n 20 ([Hello] #World#)\n' + '#l10n 21 ([Hello] -World- #123#)\n' + '#l10n END\n' + ); + }); +} diff --git a/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart b/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart new file mode 100644 index 00000000000..fb97f4f970b --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/test_data/gen_l10n_project.dart @@ -0,0 +1,249 @@ +// 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:async'; + +import 'package:file/file.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/globals.dart' as globals; + +import '../test_utils.dart'; +import 'project.dart'; + +class GenL10nProject extends Project { + @override + Future setUpIn(Directory dir) { + this.dir = dir; + writeFile(globals.fs.path.join(dir.path, 'lib', 'l10n', 'app_en.arb'), appEn); + return super.setUpIn(dir); + } + + @override + final String pubspec = ''' +name: test +environment: + sdk: ">=2.0.0-dev.68.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + intl: 0.16.1 + intl_translation: 0.17.8 +'''; + + @override + final String main = r''' +import 'package:flutter/material.dart'; + +import 'l10n/app_localizations.dart'; + +class Home extends StatelessWidget { + @override + Widget build(BuildContext context) { + try { + final AppLocalizations localizations = AppLocalizations.of(context); + final List results = [ + '${localizations.helloWorld}', + '${localizations.hello("World")}', + '${localizations.greeting("Hello", "World")}', + '${localizations.helloWorldOn(DateTime(1960))}', + '${localizations.helloOn("world argument", DateTime(1960), DateTime(1960))}', + '${localizations.helloWorldDuring(DateTime(1960), DateTime(2020))}', + '${localizations.helloFor(123)}', + '${localizations.helloCost("price", 123)}', + '${localizations.helloWorlds(0)}', + '${localizations.helloWorlds(1)}', + '${localizations.helloWorlds(2)}', + '${localizations.helloWorldsOn(0, DateTime(1960))}', + '${localizations.helloWorldsOn(1, DateTime(1960))}', + '${localizations.helloWorldsOn(2, DateTime(1960))}', + '${localizations.helloAdjectiveWorlds(0, "new")}', + '${localizations.helloAdjectiveWorlds(1, "new")}', + '${localizations.helloAdjectiveWorlds(2, "new")}', + '${localizations.helloWorldPopulation(0, 100)}', + '${localizations.helloWorldPopulation(1, 101)}', + '${localizations.helloWorldPopulation(2, 102)}', + '${localizations.helloWorldInterpolation("Hello", "World")}', + '${localizations.helloWorldsInterpolation(123, "Hello", "World")}', + ]; + int n = 0; + for (final String result in results) { + print('#l10n $n ($result)\n'); + n += 1; + } + } finally { + print('#l10n END\n'); + } + return Container(); + } +} + +void main() { + runApp( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Home(), + ), + ); +} +'''; + + final String appEn = r''' +{ + "@@locale": "en", + + "helloWorld": "Hello World", + "@helloWorld": { + "description": "The conventional newborn programmer greeting" + }, + + "hello": "Hello {world}", + "@hello": { + "description": "A message with a single parameter", + "placeholders": { + "world": {} + } + }, + + "greeting": "{hello} {world}", + "@greeting": { + "description": "A message with a two parameters", + "placeholders": { + "hello": {}, + "world": {} + } + }, + + "helloWorldOn": "Hello World on {date}", + "@helloWorldOn": { + "description": "A message with a date parameter", + "placeholders": { + "date": { + "type": "DateTime", + "format": "yMMMMEEEEd" + } + } + }, + + "helloWorldDuring": "Hello World from {startDate} to {endDate}", + "@helloWorldDuring": { + "description": "A message with two date parameters", + "placeholders": { + "startDate": { + "type": "DateTime", + "format": "y" + }, + "endDate": { + "type": "DateTime", + "format": "y" + } + } + }, + + "helloOn": "Hello {world} on {date} at {time}", + "@helloOn": { + "description": "A message with date and string parameters", + "placeholders": { + "world": { + }, + "date": { + "type": "DateTime", + "format": "yMd" + }, + "time": { + "type": "DateTime", + "format": "Hm" + } + } + }, + + "helloFor": "Hello for {value}", + "@helloFor": { + "description": "A message with a double parameter", + "placeholders": { + "value": { + "type": "double", + "format": "compact" + } + } + }, + + "helloCost": "Hello for {price} {value}", + "@helloCost": { + "description": "A message with string and int (currency) parameters", + "placeholders": { + "price": { + }, + "value": { + "type": "int", + "format": "currency" + } + } + }, + + "helloWorlds": "{count,plural, =0{Hello} =1{Hello World} =2{Hello two worlds} few{Hello {count} worlds} many{Hello all {count} worlds} other{Hello other {count} worlds}}", + "@helloWorlds": { + "description": "A plural message", + "placeholders": { + "count": {} + } + }, + + "helloAdjectiveWorlds": "{count,plural, =0{Hello} =1{Hello {adjective} World} =2{Hello two {adjective} worlds} other{Hello other {count} {adjective} worlds}}", + "@helloAdjectiveWorlds": { + "description": "A plural message with an additional parameter", + "placeholders": { + "count": {}, + "adjective": {} + } + }, + + "helloWorldsOn": "{count,plural, =0{Hello on {date}} =1{Hello World, on {date}} =2{Hello two worlds, on {date}} other{Hello other {count} worlds, on {date}}}", + "@helloWorldsOn": { + "description": "A plural message with an additional date parameter", + "placeholders": { + "count": {}, + "date": { + "type": "DateTime", + "format": "yMMMMEEEEd" + } + } + }, + + "helloWorldPopulation": "{count,plural, =1{Hello World of {population} citizens} =2{Hello two worlds with {population} total citizens} many{Hello all {count} worlds, with a total of {population} citizens} other{Hello other {count} worlds, with a total of {population} citizens}}", + "@helloWorldPopulation": { + "description": "A plural message with an additional integer parameter", + "placeholders": { + "count": {}, + "population": { + "type": "int", + "format": "compactLong" + } + } + }, + + "helloWorldInterpolation": "[{hello}] #{world}#", + "@helloWorldInterpolation": { + "description": "A message with parameters that need string interpolation braces", + "placeholders": { + "hello": {}, + "world": {} + } + }, + + "helloWorldsInterpolation": "{count,plural, other {[{hello}] -{world}- #{count}#}}", + "@helloWorldsInterpolation": { + "description": "A plural message with parameters that need string interpolation braces", + "placeholders": { + "count": {}, + "hello": {}, + "world": {} + } + } +} +'''; +}