feat: add runtime i18n API with manifest translations
This commit is contained in:
@@ -181,6 +181,25 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector {
|
||||
_resources = activation.resources;
|
||||
_audio = activation.audio ?? _createAudioManager();
|
||||
_scriptEngine = activation.scriptEngine;
|
||||
|
||||
// 合并所有包的翻译字典,框架包先、游戏包后,后加载的覆盖先加载的。
|
||||
final mergedTranslations = <String, Map<String, String>>{};
|
||||
for (final package in activation.packages) {
|
||||
for (final entry in package.manifest.translations.entries) {
|
||||
mergedTranslations.putIfAbsent(entry.key, () => {})
|
||||
.addAll(entry.value);
|
||||
}
|
||||
}
|
||||
final resolvedLocale = RuntimeLocaleResolver.resolve(
|
||||
requested: _localeOverride ?? PlatformDispatcher.instance.locale,
|
||||
defaultLocale: activation.package.manifest.defaultLocale,
|
||||
supportedLocales: activation.package.manifest.supportedLocales,
|
||||
);
|
||||
_scriptEngine.setTranslations(
|
||||
locale: resolvedLocale.resolved,
|
||||
translations: mergedTranslations,
|
||||
);
|
||||
|
||||
_viewportConfig = activation.package.manifest.display.toViewportConfig();
|
||||
_viewportRoot = PositionComponent();
|
||||
add(_viewportRoot);
|
||||
|
||||
@@ -186,6 +186,7 @@ class PackageActivationController {
|
||||
_ensureContinue(shouldContinue);
|
||||
return PackageActivationPlan(
|
||||
package: candidate,
|
||||
packages: allPackages,
|
||||
initialDiff: diff,
|
||||
resources: preparedResources,
|
||||
scriptEngine: preparedScriptEngine,
|
||||
@@ -219,6 +220,7 @@ class PackageActivationController {
|
||||
class PackageActivationPlan {
|
||||
const PackageActivationPlan({
|
||||
required this.package,
|
||||
required this.packages,
|
||||
required this.initialDiff,
|
||||
required this.resources,
|
||||
required this.scriptEngine,
|
||||
@@ -226,6 +228,7 @@ class PackageActivationPlan {
|
||||
});
|
||||
|
||||
final GamePackage package;
|
||||
final List<GamePackage> packages;
|
||||
final GameDiff initialDiff;
|
||||
final GameResourceManager resources;
|
||||
final ScriptEngine scriptEngine;
|
||||
@@ -235,6 +238,7 @@ class PackageActivationPlan {
|
||||
class PackageActivationResult {
|
||||
const PackageActivationResult({
|
||||
required this.package,
|
||||
required this.packages,
|
||||
required this.initialDiff,
|
||||
required this.resources,
|
||||
required this.scriptEngine,
|
||||
@@ -244,6 +248,7 @@ class PackageActivationResult {
|
||||
factory PackageActivationResult.fromPlan(PackageActivationPlan plan) {
|
||||
return PackageActivationResult(
|
||||
package: plan.package,
|
||||
packages: plan.packages,
|
||||
initialDiff: plan.initialDiff,
|
||||
resources: plan.resources,
|
||||
scriptEngine: plan.scriptEngine,
|
||||
@@ -252,6 +257,7 @@ class PackageActivationResult {
|
||||
}
|
||||
|
||||
final GamePackage package;
|
||||
final List<GamePackage> packages;
|
||||
final GameDiff initialDiff;
|
||||
final GameResourceManager resources;
|
||||
final ScriptEngine scriptEngine;
|
||||
|
||||
@@ -15,6 +15,7 @@ class GamePackageManifest {
|
||||
this.display = const GameDisplayConfig(),
|
||||
this.resources = const {},
|
||||
this.modules = const {},
|
||||
this.translations = const {},
|
||||
this.base,
|
||||
});
|
||||
|
||||
@@ -30,6 +31,10 @@ class GamePackageManifest {
|
||||
final Map<String, GameResource> resources;
|
||||
final Map<String, String> modules;
|
||||
|
||||
/// 翻译字典:locale → { key → translatedText }。
|
||||
/// Runtime 会按包加载顺序合并,游戏包覆盖框架包。
|
||||
final Map<String, Map<String, String>> translations;
|
||||
|
||||
/// 依赖的框架包 gameId。加载时会先加载框架包,再加载游戏包。
|
||||
final String? base;
|
||||
|
||||
@@ -64,6 +69,26 @@ class GamePackageManifest {
|
||||
|
||||
final base = map['base'] as String?;
|
||||
|
||||
final translationsValue = map['translations'];
|
||||
final translations = <String, Map<String, String>>{};
|
||||
if (translationsValue is Map) {
|
||||
for (final entry in translationsValue.entries) {
|
||||
if (entry.key is! String || entry.value is! Map) {
|
||||
throw const FormatException('manifest.translations must be a map');
|
||||
}
|
||||
final localeDict = <String, String>{};
|
||||
for (final kv in (entry.value as Map).entries) {
|
||||
if (kv.key is! String || kv.value is! String) {
|
||||
throw const FormatException(
|
||||
'manifest.translations values must be string maps',
|
||||
);
|
||||
}
|
||||
localeDict[kv.key as String] = kv.value as String;
|
||||
}
|
||||
translations[entry.key as String] = localeDict;
|
||||
}
|
||||
}
|
||||
|
||||
final defaultLocale = (map['defaultLocale'] as String?) ?? 'en';
|
||||
final supportedLocales = _stringList(
|
||||
map,
|
||||
@@ -95,6 +120,7 @@ class GamePackageManifest {
|
||||
display: display,
|
||||
resources: resources,
|
||||
modules: modules,
|
||||
translations: translations,
|
||||
base: base,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,6 +71,69 @@ class LuaDardoScriptEngine implements ScriptEngine {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void setTranslations({
|
||||
required String locale,
|
||||
required Map<String, Map<String, String>> translations,
|
||||
}) {
|
||||
_lua.getGlobal('runtime');
|
||||
if (_lua.isNil(-1)) {
|
||||
_lua.pop(1);
|
||||
return;
|
||||
}
|
||||
|
||||
_lua.newTable();
|
||||
|
||||
_lua.pushString(locale);
|
||||
_lua.setField(-2, 'locale');
|
||||
|
||||
_lua.pushDartFunction((LuaState lua) {
|
||||
lua.pushString(locale);
|
||||
return 1;
|
||||
});
|
||||
_lua.setField(-2, 'get_locale');
|
||||
|
||||
_lua.pushDartFunction((LuaState lua) {
|
||||
final key = lua.toStr(1);
|
||||
if (key == null || key.isEmpty) {
|
||||
if (lua.getTop() >= 2 && !lua.isNil(2)) {
|
||||
lua.pushValue(2);
|
||||
} else {
|
||||
lua.pushString(key ?? '');
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
final localeDict = translations[locale];
|
||||
final value = localeDict?[key];
|
||||
if (value != null) {
|
||||
lua.pushString(value);
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (final entry in translations.entries) {
|
||||
if (entry.key != locale) {
|
||||
final fallbackValue = entry.value[key];
|
||||
if (fallbackValue != null) {
|
||||
lua.pushString(fallbackValue);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lua.getTop() >= 2 && !lua.isNil(2)) {
|
||||
lua.pushValue(2);
|
||||
} else {
|
||||
lua.pushString(key);
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
_lua.setField(-2, 't');
|
||||
|
||||
_lua.setField(-2, 'i18n');
|
||||
_lua.pop(1);
|
||||
}
|
||||
|
||||
@override
|
||||
bool smokeTest(Map<String, Object?> context) {
|
||||
_lua.getGlobal('smoke_test');
|
||||
|
||||
@@ -22,4 +22,10 @@ abstract interface class ScriptEngine {
|
||||
GameDiff init(Map<String, Object?> context);
|
||||
|
||||
GameDiff dispatchEvent(RuntimeEvent event);
|
||||
|
||||
/// 设置翻译字典和当前语言,Lua 侧可通过 runtime.i18n.t(key, fallback) 查询。
|
||||
void setTranslations({
|
||||
required String locale,
|
||||
required Map<String, Map<String, String>> translations,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user