
WebToApp 宿主界面国际化实现详解10 语言字符串管理、语言切换链路与 RTL 支持【免费下载链接】web-to-appThe most full featured web-to-app toolkit on Android, a complete APK workshop that runs entirely on your phone项目地址: https://gitcode.com/GitHub_Trending/web/web-to-app本文基于 WebToApp 仓库的国际化开发文档与对应源码完整讲解宿主构建器界面的多语言机制字符串在 Strings.kt 中的组织方式、Strings/StringsA~StringsE拆分对象的真实结构、语言选择后的持久化与 Compose 重组链路以及阿拉伯语 RTL 的落地方式。读完后你可以按仓库约定新增一条完整的 10 语言用户可见字符串并理解语言切换在运行时如何逐层生效。一、两层独立的“翻译”先分清边界仓库文档 i18n.md 首先强调了一个容易混淆的边界宿主构建器界面本地化为 10 种语言。这与按应用配置、翻译生成应用内容的翻译叠加层相互独立。也就是说WebToApp 中存在两套互不干扰的本地化体系宿主界面本地化本文主题WebToApp 应用自身的菜单、按钮、设置项等 UI 文案共支持中文、英文、阿拉伯语完整 RTL、葡萄牙语、西班牙语、法语、德语、俄语、日语、韩语 10 种语言生成应用的内容翻译叠加层针对被打包成 APK 的网页内容按每个应用的配置做页面内翻译属于另一个功能模块与宿主 UI 字符串体系没有任何耦合。下面所有讨论均围绕第一层——宿主 UI 的国际化。二、支持的语言AppLanguage枚举是唯一事实源10 种语言在源码中被收敛为一个枚举 AppLanguage它同时携带语言切换所需的全部元数据enum class AppLanguage( val code: String, // 持久化用的代码如 zh val displayName: String, // 界面展示名英文 val nativeName: String, // 母语名称如 日本語 val locale: Locale, // 用于 createConfigurationContext val isRtl: Boolean false, // 仅 ARABIC 为 true val translationInProgress: Boolean false ) { CHINESE(zh, Chinese, 中文, Locale.CHINESE), ENGLISH(en, English, English, Locale.ENGLISH), ARABIC(ar, Arabic, العربية, Locale(ar), isRtl true), PORTUGUESE(pt, ...), SPANISH(es, ...), FRENCH(fr, ...), GERMAN(de, ...), RUSSIAN(ru, ...), JAPANESE(ja, ...), KOREAN(ko, ...) ; }两个值得注意的实现细节isRtl只在阿拉伯语上为trueLanguageManager.kt#L24这与文档中“阿拉伯文必须完整 RTL”的规则一一对应——RTL 不是布局层的特殊处理而是由这一个标志位驱动的通用能力见第六节fromCode的兜底是中文而非英文entries.find { it.code code } ?: CHINESELanguageManager.kt#L34-L36即未知代码一律回退到中文。三、字符串在哪里一个 6.6 万行的 Kotlin 文件文档明确指出宿主 UI 字符串不在 Android 资源系统里而是全部位于 Strings.kt拆分为Strings/StringsA…StringsE共 6 个 object。源码事实与文档描述完全吻合且可以进一步看清各 object 的物理边界Object起始行角色StringsL7门面facade 语言状态持有者StringsAL5032真实翻译文案StringsBL18776真实翻译文案StringsCL30399真实翻译文案StringsDL41355真实翻译文案StringsEL52306真实翻译文案文件共 66829 行两个可以佐证的实现事实Strings前 5000 行几乎全是一行式委托属性例如val appTitle: String get() StringsA.appTitleStrings.kt#L50-L56。业务代码只依赖Strings.xxx一个入口真正的 10 语言when分支分散在 A~E 中既保持了单一访问点又避免了单 object 过大传统 Android 资源文件 res/values/strings.xml 全文仅 10 行说明宿主 UI 文案确实刻意绕开了res/values-xx资源体系改用纯 Kotlin 的 getter 实现——这也是文档“像相邻代码那样从 Compose/UI 引用它”之所以成立的根本原因。3.1 一条字符串的真实形态以appTitle和myApps为例Strings.kt#L5033-L5066这就是文档示例代码对应的真实风格val appTitle: String get() WebToApp // 品牌名不翻译 val myApps: String get() when (Strings.lang) { AppLanguage.CHINESE - 我的应用 AppLanguage.ENGLISH - My Apps AppLanguage.ARABIC - تطبيقاتي AppLanguage.PORTUGUESE - Meus Aplicativos AppLanguage.SPANISH - Mis Aplicaciones AppLanguage.FRENCH - Mes Applications AppLanguage.GERMAN - Meine Apps AppLanguage.RUSSIAN - Мои приложения AppLanguage.JAPANESE - マイアプリ AppLanguage.KOREAN - 내 앱 }注意when枚举全部 10 个分支、没有else——这正是文档规则“绝不用else”的源码体现编译器会强制分支穷尽漏掉任何一种语言都过不了编译。带参数的文案则采用函数形式 %s占位符例如 Strings.kt#L126 的batchImportParseStats(invalid, duplicates)以及 Strings.kt#L66794 的确认删除弹窗文案agentFileDeleteConfirmMessage10 语言均以%s携带文件名参数。四、语言状态如何驱动 Compose 重组Strings.lang的巧思Strings不仅是文案门面还持有一个Compose 可观察的语言状态Strings.kt#L9-L48object Strings { private val _currentLanguage mutableStateOf(AppLanguage.CHINESE) val currentLanguage: StateAppLanguage _currentLanguage private val contextVersion mutableIntStateOf(0) fun setLanguage(language: AppLanguage) { _currentLanguage.value language } internal val lang: AppLanguage get() { contextVersion.intValue // 读取一次 snapshot登记为重组依赖 return _currentLanguage.value } ... }这里有三个关键点_currentLanguage是 ComposemutableStateOf意味着任何在 Composable 作用域内读取Strings.lang的字符串 getter都会自动把“当前语言”登记为重组依赖——语言一变所有引用过的界面文本自动重算无需手动通知langgetter 中那行看似多余的contextVersion.intValue它读取另一个mutableIntStateOf在initialize/attachContext时自增确保“本地化 Context 已切换”这件事也能触发重组避免状态与文案错位initialize与attachContext的分工initialize(baseContext)Strings.kt#L20-L27在 WebToAppApplication.onCreate 中调用只缓存一个兜底 ContextattachContext(baseContext, language)Strings.kt#L33-L42则通过LanguageManager.applyLanguage生成一份带目标 Locale 的ConfigurationContext并缓存失败时静默回退到应用 Context。五、语言切换的完整调用链把持久化、状态、UI 三个文件串起来一次完整的语言切换例如在设置中从中文切到日语的链路如下UI 发起LanguageSelector.kt 中的LanguageSelectorButton先collectAsState收集当前语言L71用户在LanguageSelectionDialog中点选后调用languageManager.setLanguage(language)L91/L340/L523写入 DataStoreLanguageManager.setLanguage 向名为language_settings的 Preferences DataStore 同时写入两个键——app_language语言代码与language_selected标记“用户已显式选择过”Flow 广播新值currentLanguageFlowLanguageManager.kt#L59-L62从 DataStore 映射出新的AppLanguage导航根节点响应InitializeLanguage()这个 ComposableStrings.kt#L66815-L66829在 AppNavigation.kt#L133 被挂载它收集currentLanguageFlow并用LaunchedEffect(language)完成两步LaunchedEffect(language) { Strings.attachContext(context, language) // 1) 换 Locale含 RTL 方向 Strings.setLanguage(language) // 2) 更新 Compose 语言状态 // 3) 顺带重载内置扩展模块文案 ExtensionManager.getInstance(context).reloadBuiltInModules() }从源码结构看第 3 步说明语言切换还会级联到扩展模块modules/下的 JS 模块的本地化重载这与宿主字符串体系是两条并行的刷新路径。系统语言兜底若用户从未手动选过语言currentLanguageFlow取不到app_language时会走getSystemLanguageCode()LanguageManager.kt#L64-L78——把Locale.getDefault()映射到 10 种支持代码之一其余一律回退英文。六、阿拉伯语完整 RTL 是如何实现的文档规则要求“阿拉伯文必须完整 RTL——验证布局正确镜像”。源码中这条规则的实现落在 LanguageManager.applyLanguagefun applyLanguage(context: Context, language: AppLanguage): Context { val locale language.locale Locale.setDefault(locale) val config Configuration(context.resources.configuration) config.setLocale(locale) config.setLayoutDirection(locale) // 由 locale 自动推导 RTL/LTR return context.createConfigurationContext(config) }setLayoutDirection(locale)是关键阿拉伯语Locale(ar)会被系统自动解析为 RTL 布局方向随后attachContext缓存的正是这份ConfigurationContextCompose 与资源系统都会基于它做镜像布局。也就是说 RTL 不需要为阿拉伯语写任何特判代码只需保证isRtl标志与 locale 正确——这也是文档要求“验证布局正确镜像”而非“为阿拉伯语单独开发”的原因。七、添加用户可见字符串可复制的操作清单继承文档 i18n.md 的三步流程并结合上述源码事实补充为可直接执行的操作第 1 步在正确的Strings*拆分对象上添加属性匹配周围风格。真实模板对照 StringsA.myApps 的既有风格// 示意 —— 匹配 Strings.kt 中实际的拆分对象风格 val myNewLabel: String get() when (Strings.lang) { AppLanguage.CHINESE - 我的标签 AppLanguage.ENGLISH - My label AppLanguage.ARABIC - ... // ... 全部 10 种,绝不用 else }选择 A~E 中哪一个 object遵循相邻归属原则例如统计页文案放StringsA的 stats 段落Agent 文案放StringsE尾部不要新建机制。第 2 步提供全部 10 种语言。缺少完整覆盖的新宿主字符串是不完整的。when必须穷尽AppLanguage的 10 个分支——由于不使用else遗漏任何一种语言都会直接编译失败语言覆盖度因此由编译器保证。第 3 步在Strings门面加一行委托再从 Compose/UI 引用。仿照 Strings.kt#L50 的既有委托模式添加val myNewLabel: String get() StringsA.myNewLabel之后界面代码只写Strings.myNewLabel。7.1 规则与边界文档原规则 源码佐证优先在现有拆分对象上添加属性不要创建新的本地化机制现有机制是“门面 5 个拆分对象 when(Strings.lang)穷尽分支”任何旁路如私有资源文件、字符串内联都会破坏InitializeLanguage的统一刷新链路不要在 Compose 界面中硬编码用户可见文本始终经由Strings只有经由Strings.lang的 getter 才能获得“语言变化 → 自动重组”的响应性硬编码文本在切换语言后不会更新阿拉伯语必须完整 RTL依赖applyLanguage的setLayoutDirection(locale)自动镜像LanguageManager.kt#L105新增界面后需人工验证镜像效果。八、相关文件索引文件作用Strings.kt10 语言文案全集 Strings门面 InitializeLanguage()ComposableLanguageManager.ktAppLanguage枚举、DataStore 持久化、Locale/RTL 应用AppNavigation.kt挂载InitializeLanguage()的导航根L133LanguageSelector.kt语言选择按钮与对话框 UIWebToAppApplication.kt启动时Strings.initialize(this)L57i18n.md本文对应的开发者文档原文适用前提说明以上机制描述以当前仓库源码为准Strings.kt的行号会随字符串增删漂移引用具体行号时请以当前检出版本为准。【免费下载链接】web-to-appThe most full featured web-to-app toolkit on Android, a complete APK workshop that runs entirely on your phone项目地址: https://gitcode.com/GitHub_Trending/web/web-to-app创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考