Skip to content

Conversation

@shihaohong
Copy link
Contributor

@shihaohong shihaohong commented Oct 31, 2019

Description

Builds on #41432 to simplify and improve the i18n process. This assumes that the developer has the necessary .arb files.

It can be test run on https://github.com/shihaohong/i18n_exploration.
Steps:

  1. Be on the root directory of the i18n_exploration project.
  2. Run dart ${FLUTTER_DIR}/dev/tools/localization/gen_l10n.dart.

In its current state, this proposal should generate the following:

  1. app_localizations.dart:
  • AppLocalizations.localizationsDelegate - Helper that returns list containing the demo localizations' delegates, along with the other default delegates needed for the app (GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate and GlobalWidgetsLocalizations.delegate)
MaterialApp(
  localizationsDelegates: AppLocalizations.localizationsDelegates,
  // ...
),
  • AppLocalizations.supportedLocales - Helper that returns a list of all the supported locales:
MaterialApp(
  supportedLocales: AppLocalizations.supportedLocales,
  // ...
),
  1. messages_en.dart, messages_es.dart, messages_zh_CN.dart, and messages_zh_TW.dart, which provide the messages for their respective locales
  2. messages_all.dart, which looks up the desired locale's translations.

Specs

  • The code generator is dev/tools/localization/gen_l10n.dart. It assumes that the Flutter app has provided ARB format files that contain localized messages. One ARB file per locale. It produces a Dart source code file that contains a single class which encapsulates all of the localized messages. The app will import this file and use the generated class to lookup localized messages.
  • ARB files can indicate the locale they represent by using a filename that matches *_.arb" or by specifying the locale at the top of the ARB file with "@@Locale": "" (or both).
  • The format of a valid locale name is defined by LocaleInfo.fromString in dev/tools/localization/localizations_utils.dart.
  • By default, an app's ARB files will be lib/l10n/*.arb. The directory can be overridden, or an explicit list of ARB files can be provided, or both.
  • If an explicit list of files is not provided and the ARB directory does not exist, the code generator will exit with an error.
  • If directory exists but does not allow read/write, the code generator will exit with an error.
  • If an ARB file contains an "@@Locale" entry, the tool will confirm that the filename's locale component, if any, is consistent.
  • By default the generated localizations class will be called "AppLocalizations" and will be saved in the ARB directory as app_localizations.dart.
  • By default, the files generated by the dart intl package, which app_localizations.dart depends on, will also be saved to the ARB directory.
  • The name of the generated class, and the directory where generated code is saved, can be overridden.

Related Issues

#41437

Checklist

Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes ([x]). This will ensure a smooth and quick review process.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I signed the CLA.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I updated/added relevant documentation (doc comments with ///).
  • All existing and new tests are passing.
  • The analyzer (flutter analyze --flutter-repo) does not report any problems on my PR.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Does your PR require Flutter developers to manually update their apps to accommodate your change?

  • Yes, this is a breaking change (Please read Handling breaking changes). Replace this with a link to the e-mail where you asked for input on this proposed change.
  • No, this is not a breaking change.

@fluttergithubbot fluttergithubbot added c: contributor-productivity Team-specific productivity, code health, technical debt. work in progress; do not review labels Oct 31, 2019
@shihaohong shihaohong added a: internationalization Supporting other languages or locales. (aka i18n) framework flutter/packages/flutter repository. See also f: labels. labels Oct 31, 2019
Copy link
Contributor

@HansMuller HansMuller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good. Mostly just some suggestions about error detection.

parser.addOption('dir-path', defaultsTo: path.join('lib', 'l10n'));
parser.addOption('input-arb-file', defaultsTo: 'app_en.arb');
parser.addOption('output-file-prefix', defaultsTo: 'app');
parser.addOption('output-class-prefix', defaultsTo: 'App');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better to just make this 'output-class' and default to 'AppLocalizations'. No reason to require developers to use 'Localizations' as a class name suffix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


Future<void> main(List<String> args) async {
final argslib.ArgParser parser = argslib.ArgParser();
parser.addOption('dir-path', defaultsTo: path.join('lib', 'l10n'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most flutter commands options that name a directory use "-dir" as a suffix. Maybe just 'arb-dir' here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

final File inputArbFile = File(path.join(l10nDirectory.path, results['input-arb-file']));
final File outputFile = File(path.join(l10nDirectory.path, '${results['output-file-prefix']}_localizations.dart'));
final String stringsClassName = '${results['output-class-prefix']}Localizations';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be a good place to check everything:

  • The l10nDirectory exists and supports read/write.
  • The inputArbFile is readable
  • The outputFile is writable or doesn't exist.
  • The stringClassName is a legit Dart class name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I managed to add checks for the following:

  • If l10nDirectory exists
  • The stringClassName is a legit Dart class name (check for first character being uppercase alphabet and for non alphanumeric characters in the string.

I'm not quite sure how to check if a file is readable/writable. Is there an example I can reference?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you can use the FileSytemEntity stat method to check the value of the FileSystemStat's mode string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Let me know if it looks good, I'm not terribly familiar with the stat() method and file system permissions, so let me know if there's something we should change here.

final String entityPath = entity.path;

if (FileSystemEntity.isFileSync(entityPath)) {
// TODO: what if there are multiple files with the exact same locale?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NICE. If we detect as much, we might as well abort, since it's likely to be a mistake.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a check here to abort if there is two arb files with the same locale

Future<void> main(List<String> args) async {
final argslib.ArgParser parser = argslib.ArgParser();
parser.addOption('dir-path', defaultsTo: path.join('lib', 'l10n'));
parser.addOption('input-arb-file', defaultsTo: 'app_en.arb');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little (just a little) odd to call this the input arb file, since they're all input files. Maybe we should call this one template-arb-file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

final RegExp arbFilenameRE = RegExp(r'(\w+)\.arb$');
if (arbFilenameRE.hasMatch(entityPath)) {
final File arbFile = File(entityPath);
final Map<String, dynamic> arbContents = json.decode(arbFile.readAsStringSync());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably this can fail. If it does we should probably abort.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if (arbFilenameRE.hasMatch(entityPath)) {
final File arbFile = File(entityPath);
final Map<String, dynamic> arbContents = json.decode(arbFile.readAsStringSync());
final RegExp arbFilenameLocaleRE = RegExp(r'^[^_]*_(\w+)\.arb$');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest only RE matching the filename if that's needed:

String localeString = arbContents['@@locale'];
if (localeString == null) {
  final RegExp arbFilenameLocaleRE = RegExp(r'^[^_]*_(\w+)\.arb$');
  localeString = arbFilenameLocaleRE.firstMatch(entityPath)[1];
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


if (arbFilenameLocaleRE.hasMatch(entityPath) && localeString == null)
localeString = arbFilenameLocaleRE.firstMatch(entityPath)[1];

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If localeString is null at this point then we found an arb file but couldn't determine what locale it was for. That seems bad; probably abort here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

parser.addOption('input-arb-file', defaultsTo: 'app_en.arb');
parser.addOption('output-file-prefix', defaultsTo: 'app');
parser.addOption('output-class-prefix', defaultsTo: 'App');
final argslib.ArgResults results = parser.parse(args);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually we should add support for a --verbose option.

/// supportedLocales list. For example:
///
/// ```
/// return MaterialApp(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe show the import here as well (assuming that the app is defined in lib/).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}
''';

const String simpleMethodTemplate = '''
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could map no-parameter lookups to a getter. Would need a getterMethodTemplate .. and some other changes :-).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

@shihaohong shihaohong marked this pull request as ready for review November 4, 2019 18:05
@fluttergithubbot
Copy link
Contributor

It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Copy link
Contributor

@HansMuller HansMuller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM; just needs a few small changes

'Please ensure that the user has read permissions.'
);
final String outputFileStatModeString = outputFile.statSync().modeString();
if (outputFileStatModeString[1] == '-')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's OK if the file doesn't exist (we're generating it for the first time). It looks like we would fail if the output file doesn't exist yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that makes sense. I realized this a few minutes ago and removed this check

final String l10nDirectoryStatModeString = l10nDirectory.statSync().modeString();
if (!_isDirectoryReadableAndWritable(l10nDirectoryStatModeString))
exitWithError(
"The 'arb-dir' directory, $l10nDirectory, is not readable or writable.\n"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is not readable or writable => doesn't allow reading and writing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

final argslib.ArgParser parser = argslib.ArgParser();
parser.addOption('arb-dir', defaultsTo: path.join('lib', 'l10n'));
parser.addOption('template-arb-file', defaultsTo: 'app_en.arb');
parser.addOption('output-localization-file', defaultsTo: 'app_localizations');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be less confusing if this defaulted to app_localizations.dart; if we didn't add the .dart suffix to the output file parameter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@shihaohong shihaohong merged commit ede637a into flutter:master Nov 5, 2019
@shihaohong shihaohong deleted the l10n-s12n branch November 5, 2019 02:27
Inconnu08 pushed a commit to Inconnu08/flutter that referenced this pull request Nov 26, 2019
…#43934)

*Starting with arb files in lib/l10n, the tool creates the following by default:
1. An AppLocalizations class containing:
- an `AppLocalizationsDelegate`
- a `supportedLocales` property that returns a list of all supported locales.
- a `localizationsDelegate` property that returns a list containing this delegate along with `GlobalMaterialLocalizations.delegate`, `GlobalCupertinoLocalizations.delegate`, and `GlobalWidgetsLocalizations.delegate`.
2. One Dart `message_<locale>.dart` file for each arb file, as well as a `messages_all.dart` file that performs the locale message lookup. The tool infers the locale of each arb file from the @@Locale property or the arb file's name.
@shihaohong shihaohong changed the title L10n Simplification New Simplified Localization Workflow Feb 27, 2020
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 1, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

a: internationalization Supporting other languages or locales. (aka i18n) c: contributor-productivity Team-specific productivity, code health, technical debt. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants