From 4ea3663853da4aca80822797c572251c1a533bdd Mon Sep 17 00:00:00 2001 From: gem Date: Fri, 12 Jun 2026 10:19:14 +0800 Subject: [PATCH] feat: add runtime i18n API with manifest translations --- lib/runtime/game/flame_lua_game.dart | 19 ++++++ .../game_package_activation_controller.dart | 6 ++ .../packages/game_package_manifest.dart | 26 ++++++++ .../scripting/lua_dardo_script_engine.dart | 63 +++++++++++++++++++ lib/runtime/scripting/script_engine.dart | 6 ++ .../events/runtime_event_dispatcher_test.dart | 7 +++ test/runtime/game/flame_lua_game_test.dart | 7 +++ ...me_package_activation_controller_test.dart | 7 +++ 8 files changed, 141 insertions(+) diff --git a/lib/runtime/game/flame_lua_game.dart b/lib/runtime/game/flame_lua_game.dart index ee0a8f4..26aa06b 100644 --- a/lib/runtime/game/flame_lua_game.dart +++ b/lib/runtime/game/flame_lua_game.dart @@ -181,6 +181,25 @@ class FlameLuaGame extends FlameGame with PanDetector, ScrollDetector { _resources = activation.resources; _audio = activation.audio ?? _createAudioManager(); _scriptEngine = activation.scriptEngine; + + // 合并所有包的翻译字典,框架包先、游戏包后,后加载的覆盖先加载的。 + final mergedTranslations = >{}; + 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); diff --git a/lib/runtime/packages/game_package_activation_controller.dart b/lib/runtime/packages/game_package_activation_controller.dart index 8bcc4b5..ca9adf4 100644 --- a/lib/runtime/packages/game_package_activation_controller.dart +++ b/lib/runtime/packages/game_package_activation_controller.dart @@ -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 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 packages; final GameDiff initialDiff; final GameResourceManager resources; final ScriptEngine scriptEngine; diff --git a/lib/runtime/packages/game_package_manifest.dart b/lib/runtime/packages/game_package_manifest.dart index b5bfe47..5a03406 100644 --- a/lib/runtime/packages/game_package_manifest.dart +++ b/lib/runtime/packages/game_package_manifest.dart @@ -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 resources; final Map modules; + /// 翻译字典:locale → { key → translatedText }。 + /// Runtime 会按包加载顺序合并,游戏包覆盖框架包。 + final Map> translations; + /// 依赖的框架包 gameId。加载时会先加载框架包,再加载游戏包。 final String? base; @@ -64,6 +69,26 @@ class GamePackageManifest { final base = map['base'] as String?; + final translationsValue = map['translations']; + final translations = >{}; + 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 = {}; + 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, ); } diff --git a/lib/runtime/scripting/lua_dardo_script_engine.dart b/lib/runtime/scripting/lua_dardo_script_engine.dart index 65ee1cb..4f85b31 100644 --- a/lib/runtime/scripting/lua_dardo_script_engine.dart +++ b/lib/runtime/scripting/lua_dardo_script_engine.dart @@ -71,6 +71,69 @@ class LuaDardoScriptEngine implements ScriptEngine { } } + @override + void setTranslations({ + required String locale, + required Map> 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 context) { _lua.getGlobal('smoke_test'); diff --git a/lib/runtime/scripting/script_engine.dart b/lib/runtime/scripting/script_engine.dart index 8f0af50..eb62fce 100644 --- a/lib/runtime/scripting/script_engine.dart +++ b/lib/runtime/scripting/script_engine.dart @@ -22,4 +22,10 @@ abstract interface class ScriptEngine { GameDiff init(Map context); GameDiff dispatchEvent(RuntimeEvent event); + + /// 设置翻译字典和当前语言,Lua 侧可通过 runtime.i18n.t(key, fallback) 查询。 + void setTranslations({ + required String locale, + required Map> translations, + }); } diff --git a/test/runtime/events/runtime_event_dispatcher_test.dart b/test/runtime/events/runtime_event_dispatcher_test.dart index ba44ade..661ecb0 100644 --- a/test/runtime/events/runtime_event_dispatcher_test.dart +++ b/test/runtime/events/runtime_event_dispatcher_test.dart @@ -138,6 +138,7 @@ void main() { expect(script.events.map((event) => event.target), ['bad', 'good']); }); }); + } RuntimeSession _activeSession() { @@ -177,4 +178,10 @@ class _FakeScriptEngine implements ScriptEngine { } return GameDiff.empty; } + + @override + void setTranslations({ + required String locale, + required Map> translations, + }) {} } diff --git a/test/runtime/game/flame_lua_game_test.dart b/test/runtime/game/flame_lua_game_test.dart index 1d20e0c..4a172bf 100644 --- a/test/runtime/game/flame_lua_game_test.dart +++ b/test/runtime/game/flame_lua_game_test.dart @@ -33,6 +33,7 @@ void main() { await expectLater(game.callLua('host.ready'), throwsA(isA())); }); }); + } class _FakeScriptEngine implements ScriptEngine { @@ -66,6 +67,12 @@ class _FakeScriptEngine implements ScriptEngine { bool smokeTest(Map context) { throw UnimplementedError(); } + + @override + void setTranslations({ + required String locale, + required Map> translations, + }) {} } class _FakePackageRepository implements GamePackageRepository { diff --git a/test/runtime/packages/game_package_activation_controller_test.dart b/test/runtime/packages/game_package_activation_controller_test.dart index 5327ea0..a2e01ea 100644 --- a/test/runtime/packages/game_package_activation_controller_test.dart +++ b/test/runtime/packages/game_package_activation_controller_test.dart @@ -217,6 +217,7 @@ void main() { expect(store.markedPackages, [fallback.rootPath]); }); }); + } Map _context(GamePackage package) { @@ -393,6 +394,12 @@ class _FakeScriptEngine implements ScriptEngine { @override GameDiff dispatchEvent(RuntimeEvent event) => GameDiff.empty; + + @override + void setTranslations({ + required String locale, + required Map> translations, + }) {} } const _validScript = '''