文章目录
背景
项目中主模块和其他库模块同时使用flutter_intl插件生成国际化代码后,为什么会出现不同模块的国际化代码互相冲突呢?
原理
简单说就是两个注册写,两个获取读
1.第一个注册写(由Localizations组件维护Map)
由Localizations组件维护Map
MaterialApp初始化的时候有往flutter widget为Localizations的组件对象中注册类型到实例的map映射(以类_LocalizationsState 的成员变量_typeToResources 的形式),每个模块都有各自的S实例,这个没有任何问题
runApp(MaterialApp(
...
localizationsDelegates: [
S.delegate,
CommonUILocalizations.delegate,
ConversationKitClient.delegate,
ChatKitClient.delegate,
ContactKitClient.delegate,
TeamKitClient.delegate,
SearchKitClient.delegate,
BaseUtils.delegate,
...GlobalMaterialLocalizations.delegates,
],
supportedLocales: IMKitClient.supportedLocales,
...
));
通过debug可以看到,不同模块下的S类实例是可以同时注册成功的,不会冲突
这里看下localizations.dart文件(位置:G:\MyWork\Flutter\Tools\flutter_windows_3.10.2-stable\flutter\packages\flutter\lib\src\widgets\localizations.dart)中的Map
class _LocalizationsState extends State<Localizations> {
。。。
Map<Type, dynamic> _typeToResources = <Type, dynamic>{};
。。。
}
怎么注册,这里以生成的国际化代码文件l10n.dart为例子
里面有类S的定义
暴露类S的静态成员delegate给到组件MaterialApp(这里以BaseUtils.delegate的形式)
MaterialApp组件就是最后通过Locations组件调用各种注册的LocalizationsDelegate实例执行load方法,把返回的对象存储在由Localizations组件维护Map里
// GENERATED CODE - DO NOT MODIFY BY HAND
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'intl/messages_all.dart';
// **************************************************************************
// Generator: Flutter Intl IDE plugin
// Made by Localizely
// **************************************************************************
// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars
// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each
// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes
class S {
S();
static S? _current;
static S get current {
assert(_current != null,
'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.');
return _current!;
}
static const AppLocalizationDelegate delegate = AppLocalizationDelegate();
static Future<S> load(Locale locale) {
final name = (locale.countryCode?.isEmpty ?? false)
? locale.languageCode
: locale.toString();
final localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName;
final instance = S();
S._current = instance;
return instance;
});
}
static S of(BuildContext context) {
final instance = S.maybeOf(context);
assert(instance != null,
'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?');
return instance!;
}
static S? maybeOf(BuildContext context) {
return Localizations.of<S>(context, S);
}
/// `search`
String get utils_search {
return Intl.message(
'search',
name: 'utils_search',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
const AppLocalizationDelegate();
List<Locale> get supportedLocales {
return const <Locale>[
Locale.fromSubtags(languageCode: 'en'),
Locale.fromSubtags(languageCode: 'zh'),
];
}
@override
bool isSupported(Locale locale) => _isSupported(locale);
@override
Future<S> load(Locale locale) => S.load(locale);
@override
bool shouldReload(AppLocalizationDelegate old) => false;
bool _isSupported(Locale locale) {
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode) {
return true;
}
}
return false;
}
}
这里我们是封装了一个类BaseUtils暴露类S的静态成员delegate的
base_utils.dart
import '../generated/l10n.dart';
class BaseUtils{
static get delegate {
return S.delegate;
}
}
2.第二个注册写(由intl库中类外变量间接维护Map)
问题是只有S实例存储正常,但是不同模块下不同语言的MessageLookupLibrary实例(也是一个map)存储冲突了
由intl库中类外变量间接维护Map
intl库中intl_helpers.dart的类外变量messageLookup 通过addLocale方法注册某一语言环境下的MessageLookupLibrary实例
(位置:C:\Users\Administrator\AppData\Local\Pub\Cache\hosted\pub.flutter-io.cn\intl-0.18.0\lib\src\intl_helpers.dart)
/// The internal mechanism for looking up messages. We expect this to be set
/// by the implementing package so that we're not dependent on its
/// implementation.
MessageLookup messageLookup =
UninitializedLocaleData('initializeMessages(<locale>)', null);
这里看下CompositeMessageLookup 类的实现代码
(位置:C:\Users\Administrator\AppData\Local\Pub\Cache\hosted\pub.flutter-io.cn\intl-0.18.0\lib\message_lookup_by_library.dart)
class CompositeMessageLookup implements MessageLookup {
/// A map from locale names to the corresponding lookups.
Map<String, MessageLookupByLibrary> availableMessages = {};//核心:这是存储不同语言下MessageLookupByLibrary实例的map
/// Return true if we have a message lookup for [localeName].
bool localeExists(String localeName) =>
availableMessages.containsKey(localeName);
/// The last locale in which we looked up messages.
///
/// If this locale matches the new one then we can skip looking up the
/// messages and assume they will be the same as last time.
String? _lastLocale;
/// Caches the last messages that we found
MessageLookupByLibrary? _lastLookup;
/// Look up the message with the given [name] and [locale] and return the
/// translated version with the values in [args] interpolated. If nothing is
/// found, return the result of [ifAbsent] or [messageText].
@override
String? lookupMessage(String? messageText, String? locale, String? name,
List<Object>? args, String? meaning,
{MessageIfAbsent? ifAbsent}) {
// If passed null, use the default.
var knownLocale = locale ?? Intl.getCurrentLocale();
var messages = (knownLocale == _lastLocale)
? _lastLookup
: _lookupMessageCatalog(knownLocale);
// If we didn't find any messages for this locale, use the original string,
// faking interpolations if necessary.
if (messages == null) {
return ifAbsent == null ? messageText : ifAbsent(messageText, args);
}
return messages.lookupMessage(messageText, locale, name, args, meaning,
ifAbsent: ifAbsent);
}
/// Find the right message lookup for [locale].
MessageLookupByLibrary? _lookupMessageCatalog(String locale) {
var verifiedLocale = Intl.verifiedLocale(locale, localeExists,
onFailure: (locale) => locale);
_lastLocale = locale;
_lastLookup = availableMessages[verifiedLocale];
return _lastLookup;
}
/// If we do not already have a locale for [localeName] then
/// [findLocale] will be called and the result stored as the lookup
/// mechanism for that locale.
@override
void addLocale(String localeName, Function findLocale) {
if (localeExists(localeName)) return;//核心:这里就会判断重复的locale添加只会保留第一个
var canonical = Intl.canonicalizedLocale(localeName);
var newLocale = findLocale(canonical);
if (newLocale != null) {
availableMessages[localeName] = newLocale;
availableMessages[canonical] = newLocale;
// If there was already a failed lookup for [newLocale], null the cache.
if (_lastLocale == newLocale) {
_lastLocale = null;
_lastLookup = null;
}
}
}
}
可以看到addLocale实现中有去重逻辑,只会保留第一个
这也就解释了为什么后注册的其他模块国际化不生效的问题
怎么注册,这里看下动态生成的国际化代码文件messages_all.dart
其中重要函数initializeMessages(String localeName)就是操作intl库中intl_helpers.dart的类外变量messageLookup进行注册的
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that looks up messages for specific locales by
// delegating to the appropriate library.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:implementation_imports, file_names, unnecessary_new
// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering
// ignore_for_file:argument_type_not_assignable, invalid_assignment
// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases
// ignore_for_file:comment_references
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
import 'package:intl/src/intl_helpers.dart';
import 'messages_en.dart' as messages_en;
import 'messages_zh.dart' as messages_zh;
typedef Future<dynamic> LibraryLoader();
Map<String, LibraryLoader> _deferredLibraries = {
'en': () => new SynchronousFuture(null),
'zh': () => new SynchronousFuture(null),
};
MessageLookupByLibrary? _findExact(String localeName) {
switch (localeName) {
case 'en':
return messages_en.messages;
case 'zh':
return messages_zh.messages;
default:
return null;
}
}
/// User programs should call this before using [localeName] for messages.
Future<bool> initializeMessages(String localeName) {
var availableLocale = Intl.verifiedLocale(
localeName, (locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null);
if (availableLocale == null) {
return new SynchronousFuture(false);
}
var lib = _deferredLibraries[availableLocale];
lib == null ? new SynchronousFuture(false) : lib();
initializeInternalMessageLookup(() => new CompositeMessageLookup());
//注册核心代码:如果有多个module同时使用flutter_intl生成国际化代码的话,
//这里先后注册就会发生冲突,同一语言locale下只有第一个注册的MessageLookupLibrary实例是生效的
//这里的messageLookup就是CompositeMessageLookup类实例
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
return new SynchronousFuture(true);
}
bool _messagesExistFor(String locale) {
try {
return _findExact(locale) != null;
} catch (e) {
return false;
}
}
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
var actualLocale =
Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null);
if (actualLocale == null) return null;
return _findExact(actualLocale);
}
3.两个获取读
使用国际化字符串的时候,是通过Intl#message方法的,这个方法其实就是从intl库中查找对应设备语言环境下的MessageLookupLibrary实例
怎么读
各自模块内部调用
S.of(context)?.utils_search
其中utils_search是业务定义的国际化字符串
1.读Localizations组件维护的Map(S.of(context)调用流程)
回到刚刚的国际化代码文件l10n.dart,部分函数如下
static S of(BuildContext context) {
final instance = S.maybeOf(context);
assert(instance != null,
'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?');
return instance!;
}
static S? maybeOf(BuildContext context) {
return Localizations.of<S>(context, S);
}
再回到刚刚的localizations.dart文件
...
static T? of<T>(BuildContext context, Type type) {
final _LocalizationsScope? scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
return scope?.localizationsState.resourcesFor<T?>(type);
}
...
class _LocalizationsState extends State<Localizations> {
...
T resourcesFor<T>(Type type) {
final T resources = _typeToResources[type] as T;
return resources;
}
...
}
...
可以看到能通过type正确读取之前存储在Localizations组件维护Map中的S类实例
2.读intl库中类外变量间接维护的Map(S.of(context)?.utils_search调用流程)
再次回到刚刚的国际化代码文件l10n.dart,部分函数如下
String get utils_search {
return Intl.message(
'search',
name: 'utils_search',
desc: '',
args: [],
);
}
跟踪intl库中代码文件\intl.dart
(位置:C:\Users\Administrator\AppData\Local\Pub\Cache\hosted\pub.flutter-io.cn\intl-0.18.0\lib\intl.dart)
static String message(String messageText,
{String? desc = '',
Map<String, Object>? examples,
String? locale,
String? name,
List<Object>? args,
String? meaning,
bool? skip}) =>
_message(messageText, locale, name, args, meaning);
...
@pragma('dart2js:noInline')
static String _message(String? messageText, String? locale, String? name,
List<Object>? args, String? meaning) {
return _lookupMessage(messageText, locale, name, args, meaning)!;
}
static String? _lookupMessage(String? messageText, String? locale,
String? name, List<Object>? args, String? meaning) {
return helpers.messageLookup
.lookupMessage(messageText, locale, name, args, meaning);//核心:又是回到intl库中类外变量messageLookup间接维护的Map
}
...
至此,终于清楚为啥读不到另一个模块的国际化字符串了,因为map中没有存储另外一个模块定义的MessageLookupLibrary实例,所以就找不到对应的国际化字符串,但是这个函数也实现了找不到的情况下使用第一个传参作为返回值的逻辑,所以
String get utils_search {
return Intl.message(
'search',
name: 'utils_search',
desc: '',
args: [],
);
}
找不到的话就直接返回字符串’search’,其他语言下也是返回’search’,不会导致app崩溃
flutter gen-l10n命令
总结:
不能使用flutter_intl插件动态生成不同模块的国际化字符串文件
应该使用flutter gen-l10n命令加上l10n.yaml动态配置文件生成不同模块的国际化字符串文件
参考官网 https://flutter.cn/docs/accessibility-and-localization/internationalization