Unverified Commit 134aa8e9 authored by Shi-Hao Hong's avatar Shi-Hao Hong Committed by GitHub

[gen-l10n] Add `nullable-getter` flag (#79263)

parent cf6d4a35
...@@ -172,6 +172,17 @@ class GenerateLocalizationsCommand extends FlutterCommand { ...@@ -172,6 +172,17 @@ class GenerateLocalizationsCommand extends FlutterCommand {
'\n' '\n'
'Resource attributes are still required for plural messages.' 'Resource attributes are still required for plural messages.'
); );
argParser.addFlag(
'nullable-getter',
help: 'Whether or not the localizations class getter is nullable.\n'
'\n'
'By default, this value is set to true so that '
'Localizations.of(context) returns a nullable value '
'for backwards compatibility. If this value is set to true, then '
'a null check is performed on the returned value of '
'Localizations.of(context), removing the need for null checking in '
'user code.'
);
} }
final FileSystem _fileSystem; final FileSystem _fileSystem;
...@@ -220,6 +231,7 @@ class GenerateLocalizationsCommand extends FlutterCommand { ...@@ -220,6 +231,7 @@ class GenerateLocalizationsCommand extends FlutterCommand {
final bool useSyntheticPackage = boolArg('synthetic-package'); final bool useSyntheticPackage = boolArg('synthetic-package');
final String projectPathString = stringArg('project-dir'); final String projectPathString = stringArg('project-dir');
final bool areResourceAttributesRequired = boolArg('required-resource-attributes'); final bool areResourceAttributesRequired = boolArg('required-resource-attributes');
final bool usesNullableGetter = boolArg('nullable-getter');
final LocalizationsGenerator localizationsGenerator = LocalizationsGenerator(_fileSystem); final LocalizationsGenerator localizationsGenerator = LocalizationsGenerator(_fileSystem);
...@@ -242,6 +254,7 @@ class GenerateLocalizationsCommand extends FlutterCommand { ...@@ -242,6 +254,7 @@ class GenerateLocalizationsCommand extends FlutterCommand {
projectPathString: projectPathString, projectPathString: projectPathString,
areResourceAttributesRequired: areResourceAttributesRequired, areResourceAttributesRequired: areResourceAttributesRequired,
untranslatedMessagesFile: untranslatedMessagesFile, untranslatedMessagesFile: untranslatedMessagesFile,
usesNullableGetter: usesNullableGetter,
) )
..loadResources() ..loadResources()
..writeOutputFiles(_logger); ..writeOutputFiles(_logger);
......
...@@ -66,6 +66,7 @@ void generateLocalizations({ ...@@ -66,6 +66,7 @@ void generateLocalizations({
useSyntheticPackage: options.useSyntheticPackage ?? true, useSyntheticPackage: options.useSyntheticPackage ?? true,
areResourceAttributesRequired: options.areResourceAttributesRequired ?? false, areResourceAttributesRequired: options.areResourceAttributesRequired ?? false,
untranslatedMessagesFile: options?.untranslatedMessagesFile?.toFilePath(), untranslatedMessagesFile: options?.untranslatedMessagesFile?.toFilePath(),
usesNullableGetter: options?.usesNullableGetter ?? true,
) )
..loadResources() ..loadResources()
..writeOutputFiles(logger, isFromYaml: true); ..writeOutputFiles(logger, isFromYaml: true);
...@@ -545,6 +546,10 @@ class LocalizationsGenerator { ...@@ -545,6 +546,10 @@ class LocalizationsGenerator {
AppResourceBundleCollection _allBundles; AppResourceBundleCollection _allBundles;
LocaleInfo _templateArbLocale; LocaleInfo _templateArbLocale;
bool _useSyntheticPackage = true; bool _useSyntheticPackage = true;
// Used to decide if the generated code is nullable or not
// (whether AppLocalizations? or AppLocalizations is returned from
// `static {name}Localizations{?} of (BuildContext context))`
bool _usesNullableGetter = true;
/// The directory that contains the project's arb files, as well as the /// The directory that contains the project's arb files, as well as the
/// header file, if specified. /// header file, if specified.
...@@ -689,8 +694,10 @@ class LocalizationsGenerator { ...@@ -689,8 +694,10 @@ class LocalizationsGenerator {
String projectPathString, String projectPathString,
bool areResourceAttributesRequired = false, bool areResourceAttributesRequired = false,
String untranslatedMessagesFile, String untranslatedMessagesFile,
bool usesNullableGetter = true,
}) { }) {
_useSyntheticPackage = useSyntheticPackage; _useSyntheticPackage = useSyntheticPackage;
_usesNullableGetter = usesNullableGetter;
setProjectDir(projectPathString); setProjectDir(projectPathString);
setInputDirectory(inputPathString); setInputDirectory(inputPathString);
setOutputDirectory(outputPathString ?? inputPathString); setOutputDirectory(outputPathString ?? inputPathString);
...@@ -1162,7 +1169,9 @@ class LocalizationsGenerator { ...@@ -1162,7 +1169,9 @@ class LocalizationsGenerator {
.replaceAll('@(supportedLanguageCodes)', supportedLanguageCodes.join(', ')) .replaceAll('@(supportedLanguageCodes)', supportedLanguageCodes.join(', '))
.replaceAll('@(messageClassImports)', sortedClassImports.join('\n')) .replaceAll('@(messageClassImports)', sortedClassImports.join('\n'))
.replaceAll('@(delegateClass)', delegateClass) .replaceAll('@(delegateClass)', delegateClass)
.replaceAll('@(requiresIntlImport)', _containsPluralMessage() ? "import 'package:intl/intl.dart' as intl;" : ''); .replaceAll('@(requiresIntlImport)', _containsPluralMessage() ? "import 'package:intl/intl.dart' as intl;" : '')
.replaceAll('@(canBeNullable)', _usesNullableGetter ? '?' : '')
.replaceAll('@(needsNullCheck)', _usesNullableGetter ? '' : '!');
} }
bool _containsPluralMessage() => _allMessages.any((Message message) => message.isPlural); bool _containsPluralMessage() => _allMessages.any((Message message) => message.isPlural);
......
...@@ -77,8 +77,8 @@ abstract class @(class) { ...@@ -77,8 +77,8 @@ abstract class @(class) {
// ignore: unused_field // ignore: unused_field
final String localeName; final String localeName;
static @(class)? of(BuildContext context) { static @(class)@(canBeNullable) of(BuildContext context) {
return Localizations.of<@(class)>(context, @(class)); return Localizations.of<@(class)>(context, @(class))@(needsNullCheck);
} }
static const LocalizationsDelegate<@(class)> delegate = _@(class)Delegate(); static const LocalizationsDelegate<@(class)> delegate = _@(class)Delegate();
......
...@@ -304,6 +304,7 @@ class LocalizationOptions { ...@@ -304,6 +304,7 @@ class LocalizationOptions {
this.deferredLoading, this.deferredLoading,
this.useSyntheticPackage = true, this.useSyntheticPackage = true,
this.areResourceAttributesRequired = false, this.areResourceAttributesRequired = false,
this.usesNullableGetter = true,
}) : assert(useSyntheticPackage != null); }) : assert(useSyntheticPackage != null);
/// The `--arb-dir` argument. /// The `--arb-dir` argument.
...@@ -365,6 +366,11 @@ class LocalizationOptions { ...@@ -365,6 +366,11 @@ class LocalizationOptions {
/// Whether to require all resource ids to contain a corresponding /// Whether to require all resource ids to contain a corresponding
/// resource attribute. /// resource attribute.
final bool areResourceAttributesRequired; final bool areResourceAttributesRequired;
/// The `nullable-getter` argument.
///
/// Whether or not the localizations class getter is nullable.
final bool usesNullableGetter;
} }
/// Parse the localizations configuration options from [file]. /// Parse the localizations configuration options from [file].
...@@ -398,6 +404,7 @@ LocalizationOptions parseLocalizationsOptions({ ...@@ -398,6 +404,7 @@ LocalizationOptions parseLocalizationsOptions({
deferredLoading: _tryReadBool(yamlNode, 'use-deferred-loading', logger), deferredLoading: _tryReadBool(yamlNode, 'use-deferred-loading', logger),
useSyntheticPackage: _tryReadBool(yamlNode, 'synthetic-package', logger) ?? true, useSyntheticPackage: _tryReadBool(yamlNode, 'synthetic-package', logger) ?? true,
areResourceAttributesRequired: _tryReadBool(yamlNode, 'required-resource-attributes', logger) ?? false, areResourceAttributesRequired: _tryReadBool(yamlNode, 'required-resource-attributes', logger) ?? false,
usesNullableGetter: _tryReadBool(yamlNode, 'nullable-getter', logger) ?? true,
); );
} }
......
...@@ -44,6 +44,7 @@ void main() { ...@@ -44,6 +44,7 @@ void main() {
untranslatedMessagesFile: Uri.file('untranslated'), untranslatedMessagesFile: Uri.file('untranslated'),
useSyntheticPackage: false, useSyntheticPackage: false,
areResourceAttributesRequired: true, areResourceAttributesRequired: true,
usesNullableGetter: false,
); );
final LocalizationsGenerator mockLocalizationsGenerator = MockLocalizationsGenerator(); final LocalizationsGenerator mockLocalizationsGenerator = MockLocalizationsGenerator();
...@@ -70,6 +71,7 @@ void main() { ...@@ -70,6 +71,7 @@ void main() {
projectPathString: '/', projectPathString: '/',
areResourceAttributesRequired: true, areResourceAttributesRequired: true,
untranslatedMessagesFile: 'untranslated', untranslatedMessagesFile: 'untranslated',
usesNullableGetter: false,
), ),
).called(1); ).called(1);
verify(mockLocalizationsGenerator.loadResources()).called(1); verify(mockLocalizationsGenerator.loadResources()).called(1);
...@@ -151,6 +153,9 @@ header-file: header ...@@ -151,6 +153,9 @@ header-file: header
header: HEADER header: HEADER
use-deferred-loading: true use-deferred-loading: true
preferred-supported-locales: en_US preferred-supported-locales: en_US
synthetic-package: false
required-resource-attributes: false
nullable-getter: false
'''); ''');
final LocalizationOptions options = parseLocalizationsOptions( final LocalizationOptions options = parseLocalizationsOptions(
...@@ -167,6 +172,9 @@ preferred-supported-locales: en_US ...@@ -167,6 +172,9 @@ preferred-supported-locales: en_US
expect(options.header, 'HEADER'); expect(options.header, 'HEADER');
expect(options.deferredLoading, true); expect(options.deferredLoading, true);
expect(options.preferredSupportedLocales, <String>['en_US']); expect(options.preferredSupportedLocales, <String>['en_US']);
expect(options.useSyntheticPackage, false);
expect(options.areResourceAttributesRequired, false);
expect(options.usesNullableGetter, false);
}); });
testWithoutContext('parseLocalizationsOptions handles preferredSupportedLocales as list', () async { testWithoutContext('parseLocalizationsOptions handles preferredSupportedLocales as list', () async {
......
...@@ -800,6 +800,82 @@ void main() { ...@@ -800,6 +800,82 @@ void main() {
}, },
); );
testUsingContext(
'generates nullable localizations class getter via static `of` method '
'by default',
() {
_standardFlutterDirectoryL10nSetup(fs);
LocalizationsGenerator generator;
try {
generator = LocalizationsGenerator(fs);
generator
..initialize(
inputPathString: defaultL10nPathString,
outputPathString: fs.path.join('lib', 'l10n', 'output'),
templateArbFileName: defaultTemplateArbFileName,
outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString,
useSyntheticPackage: false,
)
..loadResources()
..writeOutputFiles(BufferLogger.test());
} on L10nException catch (e) {
fail('Generating output should not fail: \n${e.message}');
}
final Directory outputDirectory = fs.directory('lib').childDirectory('l10n').childDirectory('output');
expect(outputDirectory.existsSync(), isTrue);
expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
expect(
outputDirectory.childFile('output-localization-file.dart').readAsStringSync(),
contains('static AppLocalizations? of(BuildContext context)'),
);
expect(
outputDirectory.childFile('output-localization-file.dart').readAsStringSync(),
contains('return Localizations.of<AppLocalizations>(context, AppLocalizations);'),
);
},
);
testUsingContext(
'can generate non-nullable localizations class getter via static `of` method ',
() {
_standardFlutterDirectoryL10nSetup(fs);
LocalizationsGenerator generator;
try {
generator = LocalizationsGenerator(fs);
generator
..initialize(
inputPathString: defaultL10nPathString,
outputPathString: fs.path.join('lib', 'l10n', 'output'),
templateArbFileName: defaultTemplateArbFileName,
outputFileString: defaultOutputFileString,
classNameString: defaultClassNameString,
useSyntheticPackage: false,
usesNullableGetter: false,
)
..loadResources()
..writeOutputFiles(BufferLogger.test());
} on L10nException catch (e) {
fail('Generating output should not fail: \n${e.message}');
}
final Directory outputDirectory = fs.directory('lib').childDirectory('l10n').childDirectory('output');
expect(outputDirectory.existsSync(), isTrue);
expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
expect(
outputDirectory.childFile('output-localization-file.dart').readAsStringSync(),
contains('static AppLocalizations of(BuildContext context)'),
);
expect(
outputDirectory.childFile('output-localization-file.dart').readAsStringSync(),
contains('return Localizations.of<AppLocalizations>(context, AppLocalizations)!;'),
);
},
);
testUsingContext('creates list of inputs and outputs when file path is specified', () { testUsingContext('creates list of inputs and outputs when file path is specified', () {
_standardFlutterDirectoryL10nSetup(fs); _standardFlutterDirectoryL10nSetup(fs);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment