@harmonyos-arkts/d2h 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.claude-plugin/marketplace.json +13 -0
  2. package/.claude-plugin/plugin.json +7 -0
  3. package/README.md +168 -2
  4. package/agents/android-to-hmos-00-orchestrator.md +312 -0
  5. package/agents/d2h.md +168 -0
  6. package/bin/install-opencode.mjs +51 -0
  7. package/install-opencode.sh +160 -0
  8. package/opencode/agents.json +16 -0
  9. package/package.json +16 -4
  10. package/schemas/android-source-manifest.schema.json +25 -0
  11. package/schemas/checkpoint-provenance.schema.json +67 -0
  12. package/schemas/final-acceptance.schema.json +29 -0
  13. package/schemas/managed-evidence-index.schema.json +15 -0
  14. package/schemas/managed-evidence.schema.json +114 -0
  15. package/schemas/migration-config.schema.json +51 -0
  16. package/schemas/migration-report-index.schema.json +39 -0
  17. package/schemas/migration-report-item.schema.json +51 -0
  18. package/schemas/migration-report-summary.schema.json +25 -0
  19. package/schemas/migration-status.schema.json +202 -0
  20. package/schemas/preflight.schema.json +60 -0
  21. package/schemas/source-order-audit.schema.json +16 -0
  22. package/schemas/source-provenance-event.schema.json +19 -0
  23. package/schemas/spec-app-shard.schema.json +45 -0
  24. package/schemas/spec-fact-corrections-shard.schema.json +13 -0
  25. package/schemas/spec-features-shard.schema.json +43 -0
  26. package/schemas/spec-index.schema.json +26 -0
  27. package/schemas/spec-interactions-shard.schema.json +40 -0
  28. package/schemas/spec-page.schema.json +98 -0
  29. package/schemas/spec-pages-index.schema.json +1 -0
  30. package/schemas/spec-unresolved-shard.schema.json +1 -0
  31. package/schemas/task-envelope.schema.json +55 -0
  32. package/schemas/task-plan.schema.json +123 -0
  33. package/skills/android2hmos_resources_convert/SKILL.md +162 -0
  34. package/skills/android2hmos_resources_convert/references/image-conversion-rules.md +230 -0
  35. package/skills/android2hmos_resources_convert/references/svg-fix-patterns.md +175 -0
  36. package/skills/android2hmos_resources_convert/references/xml-drawable-to-svg-rules.md +513 -0
  37. package/skills/appgraph-rule-audit/SKILL.md +58 -0
  38. package/skills/appgraph-rule-audit/references/audit-contract.md +47 -0
  39. package/skills/appgraph-rule-audit/references/recommendation-schema.md +41 -0
  40. package/skills/appgraph-rule-audit/schemas/rule-opportunities.schema.json +125 -0
  41. package/skills/arkts-app-identity/SKILL.md +238 -0
  42. package/skills/arkts-i18n/SKILL.md +496 -0
  43. package/skills/arkts-i18n/evals/evals.json +84 -0
  44. package/skills/arkts-i18n/references/code-examples.md +302 -0
  45. package/skills/arkts-i18n/references/common-pitfalls.md +391 -0
  46. package/skills/arkts-i18n/references/dynamic-language-switch.md +604 -0
  47. package/skills/arkts-i18n/references/hardcoded-string-scanner.md +348 -0
  48. package/skills/arkts-i18n/references/language-codes.md +104 -0
  49. package/skills/arkts-i18n/references/resource-file-structure.md +775 -0
  50. package/skills/arkts-i18n/references/static-vs-dynamic.md +242 -0
  51. package/skills/arkts-i18n/references/v1-compat.md +244 -0
  52. package/skills/arkts-i18n/scripts/audit_i18n_completeness.sh +174 -0
  53. package/skills/arkts-icon-sizing/SKILL.md +211 -0
  54. package/skills/arkts-icon-sizing/scripts/icon_audit.py +131 -0
  55. package/skills/arkts-icon-sizing/scripts/icon_autofix.py +88 -0
  56. package/skills/arkts-icon-sizing/scripts/icon_dims.py +179 -0
  57. package/skills/arkts-icon-sizing/scripts/icon_fix.py +119 -0
  58. package/skills/arkts-mvvm-architecture/SKILL.md +613 -0
  59. package/skills/harmonyos-migration-playbook/SKILL.md +56 -0
  60. package/skills/harmonyos-migration-playbook/agents/openai.yaml +7 -0
  61. package/skills/harmonyos-migration-playbook/references/arkts-compile.md +24 -0
  62. package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +53 -0
  63. package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +45 -0
  64. package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +23 -0
  65. package/skills/harmonyos-migration-playbook/references/ui-automation.md +52 -0
  66. package/skills/harmonyos-migration-playbook/references/windows-environment.md +38 -0
  67. package/skills/maintaining-migration-report/SKILL.md +155 -0
  68. package/skills/native-library-substitution/SKILL.md +385 -0
  69. package/skills/native-library-substitution/references/native-library-substitution.json +56906 -0
  70. package/skills/native-library-substitution/references/native-library-substitution.md +163 -0
  71. package/skills/preparing-migration-workspace/SKILL.md +124 -0
  72. package/skills/preparing-migration-workspace/toolchain.json +43 -0
  73. package/skills/reviewing-migration-process/SKILL.md +62 -0
  74. package/src/install-opencode.mjs +108 -0
@@ -0,0 +1,242 @@
1
+ # 纯静态 vs 动态语言切换方案
2
+
3
+ > 基于代码仓实际实现的方案对比分析
4
+
5
+ ---
6
+
7
+ ## ✅ 已验证:纯静态国际化方案
8
+
9
+ > 基于代码仓实际实现(95%+ 使用静态引用,无动态语言切换)
10
+
11
+ ### 方案特点
12
+
13
+ **代码仓实践**:
14
+ - ✅ 依赖系统语言自动匹配
15
+ - ✅ 95%+ 使用 `$r('app.string.xxx')` 静态引用(270+ 处)
16
+ - ✅ 少数场景使用 `resourceManager.getStringByNameSync()` 动态获取
17
+ - ❌ 未实现运行时语言切换
18
+ - ⚠️ module.json5 未配置 `supportedLanguages`
19
+
20
+ **优点**:
21
+ - ✅ 实现简单,无需复杂状态管理
22
+ - ✅ 性能最优(编译时确定资源路径)
23
+ - ✅ 自动跟随系统语言,用户无感知
24
+ - ✅ 代码量少,维护成本低
25
+
26
+ **缺点**:
27
+ - ⚠️ 无法在应用内动态切换语言
28
+ - ⚠️ 用户需要通过系统设置更改语言
29
+ - ⚠️ 需要重启应用才能看到语言变化
30
+
31
+ ### 适用场景
32
+
33
+ | 场景 | 是否适用 | 说明 |
34
+ |------|---------|------|
35
+ | 跟随系统语言 | ✅ 推荐 | 无需额外开发,自动适配 |
36
+ | 单一市场应用 | ✅ 推荐 | 简化实现,降低复杂度 |
37
+ | 国际化应用(无切换需求)| ✅ 推荐 | 依赖系统设置即可 |
38
+ | 需要应用内切换语言 | ❌ 不适用 | 需实现动态方案 |
39
+ | 需要记住用户语言偏好 | ❌ 不适用 | 需实现状态持久化 |
40
+
41
+ ---
42
+
43
+ ## 实现要点
44
+
45
+ ### 1. 资源文件组织
46
+
47
+ **方案 1:base/ 放中文**(代码仓采用,适合单一市场)
48
+
49
+ ```
50
+ entry/src/main/resources/
51
+ ├── base/element/string.json # 中文(主要市场)
52
+ └── en_US/element/string.json # 英文
53
+ ```
54
+
55
+ **工作原理**:
56
+ - 系统中文 → 找不到 zh 目录 → 使用 base/ → 显示中文 ✅
57
+ - 系统英文 → 找到 en_US → 显示英文 ✅
58
+
59
+ **方案 2:创建 zh_CN 目录**(标准做法,适合国际化应用)
60
+
61
+ ```
62
+ entry/src/main/resources/
63
+ ├── base/element/string.json # Fallback(英文或其他)
64
+ ├── zh_CN/element/string.json # 中文
65
+ └── en_US/element/string.json # 英文
66
+ ```
67
+
68
+ **工作原理**:
69
+ - 系统中文 → 找到 zh_CN → 显示中文 ✅
70
+ - 系统英文 → 找到 en_US → 显示英文 ✅
71
+ - 其他语言 → 无匹配 → 使用 base/ → 显示 Fallback 语言 ✅
72
+
73
+ **关键规范**:
74
+ - ✅ base/ 和 en_US/(或 zh_CN/)的 string.json **行数完全一致**
75
+ - ✅ 所有字符串 key 在不同语言文件中**完全对应**
76
+ - ✅ 使用 `en_US/` 而非 `en/`(精确区域划分)
77
+
78
+ ### 2. 静态资源引用(主流方式,95%+)
79
+
80
+ **使用场景**:Button 文本、Text 标签、Title 标题等固定文本
81
+
82
+ ```typescript
83
+ // ✅ 推荐:在 build() 中使用静态引用
84
+ Text($r('app.string.app_name'))
85
+ Button($r('app.string.confirm'))
86
+ .title($r('app.string.settings_title'))
87
+ Image($r('app.media.logo'))
88
+ ```
89
+
90
+ **工作原理**:
91
+ 1. 编译时扫描所有 `$r()` 引用
92
+ 2. 根据系统语言自动选择资源文件:
93
+ - 中文环境 → `base/element/string.json`
94
+ - 英文环境 → `en_US/element/string.json`
95
+ 3. 运行时自动加载匹配的资源
96
+
97
+ ### 3. 动态资源获取(少数场景,<5%)
98
+
99
+ **使用场景**:需要动态处理字符串(如占位符替换、复数处理)
100
+
101
+ ```typescript
102
+ // ✅ 基于代码仓 ColumnsDialog.ets:22
103
+ const context = getContext(this)
104
+ const resourceManager = context.resourceManager
105
+ const str = resourceManager.getStringByNameSync('dialog_column')
106
+ ```
107
+
108
+ **何时使用**:
109
+ - 需要替换占位符(如 `"%d 列"` → `"3 列"`)
110
+ - 需要处理复数(如 `"1 column"` vs `"2 columns"`)
111
+ - 需要根据字符串内容判断语言环境
112
+
113
+ ### 4. module.json5 配置(可选)
114
+
115
+ ```json5
116
+ {
117
+ "module": {
118
+ "name": "entry",
119
+ "type": "entry",
120
+ "description": "$string:module_desc", // ✅ 使用 $string 引用
121
+ "abilities": [
122
+ {
123
+ "name": "EntryAbility",
124
+ "description": "$string:EntryAbility_desc",
125
+ "label": "$string:EntryAbility_label"
126
+ }
127
+ ]
128
+ // ⚠️ 当前代码仓未配置 supportedLanguages
129
+ // 如需配置,可添加:
130
+ // "supportedLanguages": ["zh-Hans", "en"]
131
+ }
132
+ }
133
+ ```
134
+
135
+ **配置说明**:
136
+ - ✅ `description` 和 `label` 也支持 `$string` 引用
137
+ - ⚠️ `supportedLanguages` 不是必需字段(当前代码仓未配置)
138
+ - 💡 配置后可明确告知系统应用支持的语言
139
+
140
+ ---
141
+
142
+ ## 对比动态语言切换方案
143
+
144
+ | 特性 | 纯静态方案 | 动态切换方案 |
145
+ |------|-----------|------------|
146
+ | **实现复杂度** | ✅ 低(仅资源文件) | ⚠️ 高(需状态管理 + UI 刷新) |
147
+ | **性能** | ✅ 最优(编译时确定) | ⚠️ 中等(运行时切换) |
148
+ | **用户体验** | ✅ 一致(自动跟随系统) | ⚠️ 需手动切换 |
149
+ | **维护成本** | ✅ 低 | ⚠️ 高 |
150
+ | **适用场景** | ✅ 大多数应用 | ⚠️ 特殊需求(如学习类应用) |
151
+ | **代码量** | ✅ 少(仅配置) | ⚠️ 多(需实现切换逻辑) |
152
+
153
+ ---
154
+
155
+ ## 当前代码仓选择分析
156
+
157
+ **选择方案**:纯静态方案 + `en_US/` 目录
158
+
159
+ **选择原因**:
160
+
161
+ 1. **简单可靠**
162
+ - ✅ 无需维护复杂的状态管理
163
+ - ✅ 自动跟随系统语言,用户无感知
164
+ - ✅ 代码量少,维护成本低
165
+
166
+ 2. **性能最优**
167
+ - ✅ 编译时确定资源路径
168
+ - ✅ 无运行时语言切换开销
169
+ - ✅ 资源加载效率高
170
+
171
+ 3. **需求匹配**
172
+ - ✅ 代码仓只需支持中英文
173
+ - ✅ 无应用内切换语言需求
174
+ - ✅ 跟随系统语言即可满足需求
175
+
176
+ **使用 `en_US/` 而非 `en/` 的原因**:
177
+
178
+ | 方案 | 优点 | 缺点 | 代码仓选择 |
179
+ |------|------|------|-----------|
180
+ | `en/` | 简单,适配所有英文区域 | 无法区分不同区域(US/GB/AU) | ❌ 未选择 |
181
+ | `en_US/` | 精确区域划分,可扩展 | 目录稍多 | ✅ 已选择 |
182
+
183
+ **结论**:
184
+ - ✅ 代码仓选择 `en_US/` 是为了精确区域划分和未来扩展性
185
+ - ✅ 纯静态方案满足当前需求,无需引入动态切换的复杂性
186
+ - 💡 如果未来需要动态切换,可参考 `dynamic-language-switch.md` 实现
187
+
188
+ ---
189
+
190
+ ## ⚠️ 未验证:动态语言切换
191
+
192
+ 以下代码基于 HarmonyOS 理论 API,需要查证官方文档:
193
+
194
+ ```typescript
195
+ import { resourceManager } from '@kit.LocalizationKit'
196
+ import { common } from '@kit.AbilityKit'
197
+
198
+ // ⚠️ 未验证 - 需要确认 API 是否存在
199
+ async function switchLanguage(lang: string): Promise<void> {
200
+ try {
201
+ const context = getContext(this) as common.UIAbilityContext
202
+ const resMgr = context.resourceManager
203
+
204
+ // ⚠️ 以下两个 API 需要查证官方文档
205
+ await resMgr.setPreferredLanguage([lang])
206
+ const langs = await resMgr.getPreferredLanguage()
207
+
208
+ console.log(`Switched to: ${langs[0]}`)
209
+ } catch (err) {
210
+ console.error(`Failed to switch language: ${err}`)
211
+ }
212
+ }
213
+ ```
214
+
215
+ **替代方案**(如 API 不存在):
216
+ - 方案 1:引导用户到系统设置修改语言
217
+ - 方案 2:应用内维护语言状态,手动切换资源
218
+
219
+ ---
220
+
221
+ ## ✅ 已验证:资源文件结构
222
+
223
+ 基于 `entry/src/main/resources/` 目录结构:
224
+
225
+ ```
226
+ resources/
227
+ ├── base/
228
+ │ └── element/
229
+ │ └── string.json # 中文(996 行)
230
+ ├── en_US/
231
+ │ └── element/
232
+ │ └── string.json # 英文(996 行)
233
+ ├── dark/
234
+ │ └── element/
235
+ │ └── color.json # 深色模式颜色
236
+ └── rawfile/ # 原始文件
237
+ ```
238
+
239
+ **验证结果**:
240
+ - ✅ base 和 en_US 的 string.json 行数一致(996 行)
241
+ - ✅ 两个文件的 key 完全对应
242
+ - ✅ 使用 en_US 而非 en(精确区域划分)
@@ -0,0 +1,244 @@
1
+ # V1 兼容写法参考(仅老项目查阅)
2
+
3
+ > ⚠️ **本项目锁 ArkTS V2**。本文档为 **V1 历史参考**(`@Component / @State / @StorageLink / @Prop / @Watch` 等),仅用于阅读老代码或迁移老项目时查阅。
4
+ >
5
+ > **新代码请使用 V2 装饰器**:参阅 [`SKILL.md`](../SKILL.md)、[`dynamic-language-switch.md`](./dynamic-language-switch.md)、[`code-examples.md`](./code-examples.md)(已升级 V2)。
6
+ >
7
+ > i18n 相关 API(`resourceManager` / `intl` / `$r('app.string.xxx')` / `setPreferredLanguage` / `getPreferredLanguage` / `onLanguageConfigurationUpdate`)在 V1/V2 之间**完全不变**,本文仅记录"持有 locale / refreshKey 状态"的 V1 装饰器写法。
8
+
9
+ ---
10
+
11
+ ## V1 / V2 速查(仅 i18n 相关)
12
+
13
+ | V1 | V2 |
14
+ |---|---|
15
+ | `@Component` | `@ComponentV2` |
16
+ | `@State refreshKey: number = 0` | `@Local refreshKey: number = 0` |
17
+ | `@StorageLink('currentLanguage') lang: string = 'zh'` | 先定义 `@ObservedV2 LocaleModel`,然后 `@Local locale = AppStorageV2.connect(LocaleModel, 'locale', () => new LocaleModel())!`,访问 `this.locale.currentLanguage` |
18
+ | `@StorageLink('languageRefreshKey') key: number = 0` | 同上,访问 `this.locale.refreshKey` |
19
+ | `@Prop currentColumns: number = 3` | `@Param @Once currentColumns: number = 3`(只读) |
20
+ | `@Prop currentColumns: number = 3`(子可改本地副本) | `@Param currentColumns: number = 3`(不带 @Once) |
21
+ | `@Watch('lang') onLangChange()` | `@Monitor('lang') onLangChange(m: IMonitor)` |
22
+ | `AppStorage.setOrCreate('refreshKey', n)` | `localeStore.refreshKey = n`(直接改 @Trace 字段) |
23
+ | `PersistentStorage.persistProp('currentLanguage', 'zh')` | `PersistenceV2.globalConnect({ type: LocaleModel, key: 'locale', defaultCreator: () => new LocaleModel() })` |
24
+
25
+ ---
26
+
27
+ ## V1 简单实现(单页面语言切换)
28
+
29
+ ```typescript
30
+ import { resourceManager } from '@kit.LocalizationKit'
31
+ import { common } from '@kit.AbilityKit'
32
+
33
+ @Entry
34
+ @Component
35
+ struct LanguageSwitchDemoV1 {
36
+ private context = getContext(this) as common.UIAbilityContext
37
+ @State private currentLanguage: string = 'zh'
38
+ @State private refreshKey: number = 0
39
+
40
+ async switchLanguage(lang: string): Promise<void> {
41
+ const resMgr = this.context.resourceManager
42
+ await resMgr.setPreferredLanguage([lang])
43
+ this.currentLanguage = lang
44
+ this.refreshKey++
45
+ }
46
+
47
+ build() {
48
+ Column() {
49
+ Text($r('app.string.welcome'))
50
+ Button('English').onClick(() => this.switchLanguage('en'))
51
+ }
52
+ .id('lang_' + this.refreshKey)
53
+ }
54
+ }
55
+ ```
56
+
57
+ **升级到 V2**(参考 [`dynamic-language-switch.md`](./dynamic-language-switch.md) 的"基本实现"):
58
+ - `@Component` → `@ComponentV2`
59
+ - `@State` → `@Local`
60
+ - 其他 API 调用不变
61
+
62
+ ---
63
+
64
+ ## V1 全局状态共享(@StorageLink 多 key 写法)
65
+
66
+ ```typescript
67
+ // V1:散落的 key(拼写易错、无类型保护)
68
+ class LanguageStateV1 {
69
+ @StorageLink('currentLanguage') currentLanguage: string = 'zh'
70
+ @StorageLink('languageRefreshKey') refreshKey: number = 0
71
+
72
+ async setLanguage(context: common.UIAbilityContext, lang: string): Promise<void> {
73
+ const resMgr = context.resourceManager
74
+ await resMgr.setPreferredLanguage([lang])
75
+ this.currentLanguage = lang
76
+ this.refreshKey++
77
+ }
78
+ }
79
+
80
+ // V1 设置页面
81
+ @Entry
82
+ @Component
83
+ struct SettingsPageV1 {
84
+ @StorageLink('currentLanguage') currentLanguage: string = 'zh'
85
+ @StorageLink('languageRefreshKey') refreshKey: number = 0
86
+
87
+ build() {
88
+ Column() {
89
+ Text(`Current: ${this.currentLanguage}`)
90
+ }
91
+ .id('settings_' + this.refreshKey)
92
+ }
93
+ }
94
+
95
+ // V1 任意页面
96
+ @Entry
97
+ @Component
98
+ struct AnyPageV1 {
99
+ @StorageLink('languageRefreshKey') refreshKey: number = 0
100
+
101
+ build() {
102
+ Column() {
103
+ Text($r('app.string.any_string'))
104
+ }
105
+ .id('page_' + this.refreshKey)
106
+ }
107
+ }
108
+ ```
109
+
110
+ **问题**:
111
+ - key 拼写错误(如 `'lanugageRefreshKey'`)静默失效
112
+ - 无类型保护(值是 Object)
113
+ - 字段加减需改多处
114
+
115
+ **V2 重构**(推荐,见 [`dynamic-language-switch.md`](./dynamic-language-switch.md) 的"全局 LocaleModel"段):
116
+ - 单一 `@ObservedV2 LocaleModel` 封装所有 locale 状态
117
+ - 任意组件 `@Local locale = AppStorageV2.connect(LocaleModel, 'locale', () => new LocaleModel())!`
118
+ - 访问 `this.locale.currentLanguage` / `this.locale.refreshKey`
119
+ - 编译期类型检查 + IDE 重命名安全
120
+
121
+ ---
122
+
123
+ ## V1 子组件接收只读 Prop
124
+
125
+ ```typescript
126
+ @Component
127
+ export struct ColumnsDialogV1 {
128
+ @Prop currentColumns: number = 3 // V1:单向只读(API 12+ 必须初始化)
129
+
130
+ build() {
131
+ Text(`Columns: ${this.currentColumns}`)
132
+ }
133
+ }
134
+ ```
135
+
136
+ **V2 等价写法**(见 [`code-examples.md`](./code-examples.md) 的 V2 ColumnsDialog):
137
+
138
+ ```typescript
139
+ @ComponentV2
140
+ export struct ColumnsDialog {
141
+ @Param @Once currentColumns: number = 3 // V2:单向只读(编译期检查)
142
+
143
+ build() {
144
+ Text(`Columns: ${this.currentColumns}`)
145
+ }
146
+ }
147
+ ```
148
+
149
+ ---
150
+
151
+ ## V1 监听语言变化(@Watch)
152
+
153
+ ```typescript
154
+ @Component
155
+ struct PageV1 {
156
+ @StorageLink('currentLanguage') @Watch('onLangChange') currentLanguage: string = 'zh'
157
+
158
+ onLangChange(propName: string): void {
159
+ console.info(`Language changed: ${this.currentLanguage}`)
160
+ // 副作用:埋点、清缓存等
161
+ }
162
+
163
+ build() { Column() { Text($r('app.string.hello')) } }
164
+ }
165
+ ```
166
+
167
+ **V2 等价**:
168
+
169
+ ```typescript
170
+ @ComponentV2
171
+ struct Page {
172
+ @Local locale: LocaleModel = AppStorageV2.connect(
173
+ LocaleModel, 'locale', () => new LocaleModel()
174
+ )!
175
+
176
+ @Monitor('locale.currentLanguage')
177
+ onLangChange(monitor: IMonitor): void {
178
+ console.info(`Language changed: ${this.locale.currentLanguage}`)
179
+ // 副作用
180
+ }
181
+
182
+ build() { Column() { Text($r('app.string.hello')) } }
183
+ }
184
+ ```
185
+
186
+ ---
187
+
188
+ ## V1 持久化语言偏好(PersistentStorage.persistProp)
189
+
190
+ ```typescript
191
+ // V1:双层模式(AppStorage + Preferences)
192
+ PersistentStorage.persistProp('currentLanguage', 'zh')
193
+
194
+ @Component
195
+ struct PageV1 {
196
+ @StorageLink('currentLanguage') currentLanguage: string = 'zh'
197
+ // 修改 currentLanguage 自动落盘 + UI 刷新
198
+ }
199
+ ```
200
+
201
+ **V2 等价**(一站式):
202
+
203
+ ```typescript
204
+ @ObservedV2
205
+ class LocaleModel {
206
+ @Trace currentLanguage: string = 'zh'
207
+ }
208
+
209
+ @ComponentV2
210
+ struct Page {
211
+ @Local locale: LocaleModel = PersistenceV2.globalConnect({
212
+ type: LocaleModel,
213
+ key: 'app_locale',
214
+ defaultCreator: () => new LocaleModel()
215
+ })!
216
+ // 修改 this.locale.currentLanguage 自动落盘 + UI 刷新(无需双层)
217
+ }
218
+ ```
219
+
220
+ ---
221
+
222
+ ## V1 → V2 迁移建议
223
+
224
+ 1. **先升级 SKILL.md / 主参考的代码模板**(已完成 — 见本目录其他文件)
225
+ 2. **逐文件迁移老代码**:从最常用的 LanguageSettings / Settings 页面开始
226
+ 3. **抽取 LocaleModel**:把所有 `@StorageLink('currentLanguage' | 'languageRefreshKey' | ...)` 整合到一个 `@ObservedV2 LocaleModel` 类
227
+ 4. **替换装饰器**:
228
+ - `@Component` → `@ComponentV2`
229
+ - `@State` → `@Local`
230
+ - `@StorageLink('xxx') xxx: T = default` → `@Local locale: LocaleModel = AppStorageV2.connect(LocaleModel, 'locale', () => new LocaleModel())!`,访问 `this.locale.xxx`
231
+ - `@Prop` → `@Param @Once`(只读)/ `@Param`(可改本地副本)
232
+ - `@Watch` → `@Monitor`
233
+ - `PersistentStorage.persistProp` → `PersistenceV2.globalConnect`
234
+ 5. **同 struct 内不混用 V1/V2 装饰器**(编译报错)
235
+
236
+ ---
237
+
238
+ ## 跨文档参考
239
+
240
+ - [`SKILL.md`](../SKILL.md) — V2 优先决策树与陷阱
241
+ - [`dynamic-language-switch.md`](./dynamic-language-switch.md) — V2 完整动态切换模板
242
+ - [`code-examples.md`](./code-examples.md) — V2 ColumnsDialog 实战示例
243
+ - [`common-pitfalls.md`](./common-pitfalls.md) — V2 i18n 避坑
244
+ - 跨 Skill:[`arkts-mvvm-architecture/SKILL.md`](../../arkts-mvvm-architecture/SKILL.md) — 当前工程的 V2 状态管理、组件和架构规范
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env bash
2
+ # audit_i18n_completeness.sh — i18n cross-locale key consistency audit.
3
+ #
4
+ # 单一职责:检测跨 locale 的 string.json / plural.json / strarray.json 的 key 一致性。
5
+ # base 的全部 key 必须在每个 language locale(zh_CN / en_US / ...)都存在;
6
+ # mode locale(dark / horizontal / vertical 等)按差异覆盖即可,豁免。
7
+ #
8
+ # 资源 value 内的 [TODO: translate] / 裸 TODO 检测不在本脚本范围;调用者必须在
9
+ # 当前请求范围收尾时另行扫描并把发现项返回为 unresolved。本脚本只管跨 locale 完整性。
10
+ #
11
+ # 用法(由 arkts-i18n 任务完成后自调,不被其他 skill 跨引用):
12
+ # bash audit_i18n_completeness.sh \
13
+ # --project-root <abs> \
14
+ # --output-json docs/i18n-audit.json
15
+ #
16
+ # 退出码:
17
+ # 0 全 PASS(key 一致)
18
+ # 1 有 key_missing FAIL 项
19
+ # 2 脚本错误(参数缺失等)
20
+ #
21
+ # 输出 JSON 结构:
22
+ # {
23
+ # "locales": ["base", "zh_CN", ...],
24
+ # "language_locales": ["zh_CN", ...], # 参与 key 一致性校验的 locale
25
+ # "mode_locales": ["dark", ...], # 豁免 locale
26
+ # "key_count_per_locale": { "base": N, "zh_CN": N, ... },
27
+ # "findings": [
28
+ # { "type": "key-missing", "locale": "zh_CN", "key": "home_feed", "base_locale": "base", "suggested_action": "..." }
29
+ # ],
30
+ # "summary": { "key_missing": N, "FAIL": N }
31
+ # }
32
+
33
+ set -euo pipefail
34
+
35
+ PROJECT_ROOT=""
36
+ OUTPUT_JSON=""
37
+
38
+ while [[ $# -gt 0 ]]; do
39
+ case "$1" in
40
+ --project-root) PROJECT_ROOT="$2"; shift 2 ;;
41
+ --output-json) OUTPUT_JSON="$2"; shift 2 ;;
42
+ --help|-h)
43
+ grep '^#' "$0" | head -40
44
+ exit 0
45
+ ;;
46
+ *)
47
+ echo "unknown arg: $1" >&2
48
+ exit 2
49
+ ;;
50
+ esac
51
+ done
52
+
53
+ if [[ -z "$PROJECT_ROOT" ]]; then
54
+ echo "error: --project-root required" >&2
55
+ exit 2
56
+ fi
57
+
58
+ # Resolve to absolute path; on Git Bash use `pwd -W` to get Windows-style for Python interop
59
+ if pwd -W >/dev/null 2>&1; then
60
+ PROJECT_ROOT="$(cd "$PROJECT_ROOT" && pwd -W)"
61
+ else
62
+ PROJECT_ROOT="$(cd "$PROJECT_ROOT" && pwd)"
63
+ fi
64
+ OUTPUT_JSON="${OUTPUT_JSON:-/dev/stdout}"
65
+
66
+ RESOURCES_ROOT="$PROJECT_ROOT/entry/src/main/resources"
67
+
68
+ if [[ ! -d "$RESOURCES_ROOT" ]]; then
69
+ echo "error: resources root not found: $RESOURCES_ROOT" >&2
70
+ exit 2
71
+ fi
72
+
73
+ # ─── Python helper: cross-locale key consistency only ───────────────────────
74
+ PY_HELPER=$(cat <<'PYEOF'
75
+ import json, os, sys
76
+
77
+ project_root = sys.argv[1]
78
+ output_path = sys.argv[2]
79
+ resources_root = os.path.join(project_root, 'entry', 'src', 'main', 'resources')
80
+
81
+ # 仅对翻译类 element 文件做校验
82
+ TRANSLATION_ELEMENT_FILES = {'string.json', 'plural.json', 'strarray.json'}
83
+
84
+ # Mode locale 豁免(按差异覆盖即可)
85
+ MODE_LOCALES = {'dark', 'light', 'horizontal', 'vertical', 'land', 'port'}
86
+
87
+ def is_language_locale(name: str) -> bool:
88
+ if name in MODE_LOCALES:
89
+ return False
90
+ if '_' in name:
91
+ return True
92
+ if 2 <= len(name) <= 3 and name.isalpha():
93
+ return True
94
+ return False
95
+
96
+ # Collect locales + key sets
97
+ locales = []
98
+ key_per_locale = {}
99
+ for entry in sorted(os.listdir(resources_root)):
100
+ locale_dir = os.path.join(resources_root, entry)
101
+ elem_dir = os.path.join(locale_dir, 'element')
102
+ if not os.path.isdir(elem_dir):
103
+ continue
104
+ if entry in ('rawfile', 'profile'):
105
+ continue
106
+ locales.append(entry)
107
+ key_per_locale[entry] = set()
108
+ for fname in os.listdir(elem_dir):
109
+ if fname not in TRANSLATION_ELEMENT_FILES:
110
+ continue
111
+ fpath = os.path.join(elem_dir, fname)
112
+ try:
113
+ with open(fpath, encoding='utf-8') as f:
114
+ data = json.load(f)
115
+ except Exception:
116
+ continue
117
+ for items in data.values():
118
+ if not isinstance(items, list):
119
+ continue
120
+ for item in items:
121
+ if isinstance(item, dict) and 'name' in item:
122
+ key_per_locale[entry].add(item['name'])
123
+
124
+ # Language vs mode locales
125
+ language_locales = [l for l in locales if is_language_locale(l)]
126
+ mode_locales = [l for l in locales if l in MODE_LOCALES]
127
+
128
+ # Key consistency check: base keys must exist in every language locale
129
+ findings = []
130
+ base_locale = 'base' if 'base' in key_per_locale else (locales[0] if locales else None)
131
+ if base_locale:
132
+ base_keys = key_per_locale[base_locale]
133
+ for locale in language_locales:
134
+ if locale == base_locale:
135
+ continue
136
+ missing = base_keys - key_per_locale[locale]
137
+ for k in sorted(missing):
138
+ findings.append({
139
+ "type": "key-missing",
140
+ "locale": locale,
141
+ "key": k,
142
+ "base_locale": base_locale,
143
+ "suggested_action": f"Add key `{k}` to {locale}/element/<file>.json with locale-appropriate value.",
144
+ })
145
+
146
+ summary = {
147
+ "key_missing": len(findings),
148
+ "FAIL": len(findings),
149
+ }
150
+
151
+ out = {
152
+ "locales": locales,
153
+ "language_locales": language_locales,
154
+ "mode_locales": mode_locales,
155
+ "key_count_per_locale": {l: len(k) for l, k in key_per_locale.items()},
156
+ "summary": summary,
157
+ "findings": findings,
158
+ }
159
+
160
+ if output_path == '/dev/stdout':
161
+ print(json.dumps(out, ensure_ascii=False, indent=2))
162
+ else:
163
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
164
+ with open(output_path, 'w', encoding='utf-8') as f:
165
+ json.dump(out, f, ensure_ascii=False, indent=2)
166
+ print(f"i18n audit complete: {output_path}")
167
+ print(f" locales={locales} language={language_locales} mode={mode_locales}")
168
+ print(f" FAIL={summary['FAIL']} (key_missing={summary['key_missing']})")
169
+
170
+ sys.exit(1 if summary["FAIL"] > 0 else 0)
171
+ PYEOF
172
+ )
173
+
174
+ python3 -c "$PY_HELPER" "$PROJECT_ROOT" "$OUTPUT_JSON"