@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,604 @@
1
+ # 动态语言切换实现(V2)
2
+
3
+ > 完整的运行时语言切换代码模板,包括状态管理和 UI 刷新。**本项目锁 V2**:组件用 `@ComponentV2`,本地状态用 `@Local`,全局 locale / refreshKey 用 `@ObservedV2 LocaleModel + AppStorageV2.connect`。i18n API 与装饰器版本无关。
4
+ >
5
+ > V1 老项目(`@Component / @State / @StorageLink`)查阅请见 [`v1-compat.md`](./v1-compat.md)。
6
+
7
+ ---
8
+
9
+ ## ⚠️ API 验证状态
10
+
11
+ | API | 状态 | 说明 |
12
+ |-----|------|------|
13
+ | `resourceManager.setPreferredLanguage()` | ⚠️ 未验证 | 需查官方文档确认 API 存在性 |
14
+ | `resourceManager.getPreferredLanguage()` | ⚠️ 未验证 | 需查官方文档确认 API 存在性 |
15
+ | V2 `@Local` / `AppStorageV2.connect` 触发 UI 刷新 | ✅ 已验证 | ArkTS V2 标准机制(API 12+) |
16
+ | `onLanguageConfigurationUpdate()` | ⚠️ 未验证 | 需确认生命周期回调是否正确 |
17
+
18
+ > **建议**:使用前请查阅官方文档
19
+ > - resourceManager API:https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-resource-manager-V5
20
+ > - 如 API 不存在,可使用应用内状态管理替代
21
+
22
+ ---
23
+
24
+ ## 基本实现模式
25
+
26
+ ### 简单实现(单页面,V2)
27
+
28
+ ```typescript
29
+ import { resourceManager } from '@kit.LocalizationKit'
30
+ import { common } from '@kit.AbilityKit'
31
+ import { hilog } from '@kit.PerformanceAnalysisKit'
32
+
33
+ const DOMAIN = 0x0001
34
+ const TAG = 'I18nDemo'
35
+
36
+ @Entry
37
+ @ComponentV2
38
+ struct LanguageSwitchDemo {
39
+ private context = getContext(this) as common.UIAbilityContext
40
+ @Local private currentLanguage: string = 'zh'
41
+ @Local private refreshKey: number = 0
42
+
43
+ aboutToAppear(): void {
44
+ this.loadCurrentLanguage()
45
+ }
46
+
47
+ async loadCurrentLanguage(): Promise<void> {
48
+ try {
49
+ const resMgr = this.context.resourceManager
50
+ const langs = await resMgr.getPreferredLanguage()
51
+ this.currentLanguage = langs[0] || 'zh'
52
+ hilog.info(DOMAIN, TAG, `Current language: ${this.currentLanguage}`)
53
+ } catch (e) {
54
+ hilog.error(DOMAIN, TAG, `Failed to get language: ${e.message}`)
55
+ }
56
+ }
57
+
58
+ async switchLanguage(lang: string): Promise<void> {
59
+ try {
60
+ hilog.info(DOMAIN, TAG, `Switching to: ${lang}`)
61
+
62
+ const resMgr = this.context.resourceManager
63
+ await resMgr.setPreferredLanguage([lang])
64
+
65
+ this.currentLanguage = lang
66
+ this.refreshKey++
67
+
68
+ hilog.info(DOMAIN, TAG, `Language switched to: ${lang}`)
69
+ } catch (e) {
70
+ hilog.error(DOMAIN, TAG, `Failed to switch language: ${e.message}`)
71
+ }
72
+ }
73
+
74
+ build() {
75
+ Column({ space: 20 }) {
76
+ Text($r('app.string.app_name'))
77
+ .fontSize(24)
78
+ .fontWeight(FontWeight.Bold)
79
+
80
+ Text($r('app.string.welcome'))
81
+ .fontSize(16)
82
+
83
+ Text($r('app.string.settings_language'))
84
+ .fontSize(14)
85
+ .fontColor('#666666')
86
+
87
+ Row({ space: 12 }) {
88
+ Button('中文')
89
+ .onClick(() => this.switchLanguage('zh'))
90
+ .backgroundColor(this.currentLanguage === 'zh' ? '#007AFF' : '#CCCCCC')
91
+
92
+ Button('English')
93
+ .onClick(() => this.switchLanguage('en'))
94
+ .backgroundColor(this.currentLanguage === 'en' ? '#007AFF' : '#CCCCCC')
95
+
96
+ Button('日本語')
97
+ .onClick(() => this.switchLanguage('ja'))
98
+ .backgroundColor(this.currentLanguage === 'ja' ? '#007AFF' : '#CCCCCC')
99
+ }
100
+
101
+ Text(`Current: ${this.currentLanguage}`)
102
+ .fontSize(12)
103
+ .fontColor('#999999')
104
+ }
105
+ .width('100%')
106
+ .height('100%')
107
+ .justifyContent(FlexAlign.Center)
108
+ .id('lang_container_' + this.refreshKey)
109
+ }
110
+ }
111
+ ```
112
+
113
+ ---
114
+
115
+ ## 完整实现(设置页面 + 全局状态,V2)
116
+
117
+ ### 1. 全局 LocaleModel(@ObservedV2 + AppStorageV2)
118
+
119
+ V2 推荐**单一 `@ObservedV2` 类封装所有 locale 相关全局状态**,避免 V1 散落的 `@StorageLink('currentLanguage')` / `@StorageLink('languageRefreshKey')` 多 key 写法(拼写易错、无类型保护)。
120
+
121
+ ```typescript
122
+ // common/LocaleModel.ets
123
+ import { resourceManager } from '@kit.LocalizationKit'
124
+ import { common } from '@kit.AbilityKit'
125
+ import { AppStorageV2 } from '@kit.ArkUI'
126
+ import { hilog } from '@kit.PerformanceAnalysisKit'
127
+
128
+ const DOMAIN = 0x0000
129
+ const TAG = 'LocaleModel'
130
+
131
+ @ObservedV2
132
+ export class LocaleModel {
133
+ @Trace currentLanguage: string = 'zh'
134
+ @Trace refreshKey: number = 0
135
+
136
+ async init(context: common.UIAbilityContext): Promise<void> {
137
+ try {
138
+ const resMgr = context.resourceManager
139
+ const langs = await resMgr.getPreferredLanguage()
140
+ this.currentLanguage = langs[0] || 'zh'
141
+ } catch (e) {
142
+ this.currentLanguage = 'zh'
143
+ }
144
+ }
145
+
146
+ async setLanguage(context: common.UIAbilityContext, lang: string): Promise<void> {
147
+ try {
148
+ const resMgr = context.resourceManager
149
+ await resMgr.setPreferredLanguage([lang])
150
+ this.currentLanguage = lang
151
+ this.refreshKey++ // @Trace 字段变化 → 所有 connect 同 key 的组件自动刷新
152
+ } catch (e) {
153
+ hilog.error(DOMAIN, TAG, `Failed to set language: ${e.message}`)
154
+ }
155
+ }
156
+ }
157
+
158
+ // 全局单例(推荐在 EntryAbility.onCreate 中预热一次;任何位置 connect 同 key 都返回同一实例)
159
+ export const localeStore = AppStorageV2.connect(
160
+ LocaleModel,
161
+ 'locale',
162
+ () => new LocaleModel()
163
+ )!
164
+ ```
165
+
166
+ ### 2. 设置页面(V2)
167
+
168
+ ```typescript
169
+ // pages/SettingsPage.ets
170
+ import { LocaleModel, localeStore } from '../common/LocaleModel'
171
+ import { common } from '@kit.AbilityKit'
172
+
173
+ @Entry
174
+ @ComponentV2
175
+ struct SettingsPage {
176
+ @Local locale: LocaleModel = AppStorageV2.connect(
177
+ LocaleModel,
178
+ 'locale',
179
+ () => new LocaleModel()
180
+ )!
181
+
182
+ @Builder
183
+ LanguageItem(lang: string, label: string) {
184
+ Row() {
185
+ Text(label)
186
+ .fontSize(16)
187
+
188
+ if (this.locale.currentLanguage === lang) {
189
+ Text('✓')
190
+ .fontSize(16)
191
+ .fontColor('#007AFF')
192
+ }
193
+ }
194
+ .width('100%')
195
+ .padding(16)
196
+ .backgroundColor(this.locale.currentLanguage === lang ? '#F0F8FF' : '#FFFFFF')
197
+ .onClick(() => {
198
+ const context = getContext(this) as common.UIAbilityContext
199
+ this.locale.setLanguage(context, lang)
200
+ })
201
+ }
202
+
203
+ build() {
204
+ Column() {
205
+ Text($r('app.string.settings_title'))
206
+ .fontSize(20)
207
+ .fontWeight(FontWeight.Bold)
208
+ .padding(16)
209
+
210
+ List() {
211
+ ListItem() {
212
+ this.LanguageItem('zh', '简体中文')
213
+ }
214
+
215
+ ListItem() {
216
+ this.LanguageItem('zh-Hans', '简体中文 (简体)')
217
+ }
218
+
219
+ ListItem() {
220
+ this.LanguageItem('zh-Hant', '繁体中文 (繁体)')
221
+ }
222
+
223
+ ListItem() {
224
+ this.LanguageItem('en', 'English')
225
+ }
226
+
227
+ ListItem() {
228
+ this.LanguageItem('ja', '日本語')
229
+ }
230
+ }
231
+ }
232
+ .width('100%')
233
+ .height('100%')
234
+ .id('settings_' + this.locale.refreshKey)
235
+ }
236
+ }
237
+ ```
238
+
239
+ ---
240
+
241
+ ## 语言切换后刷新页面的方法(V2)
242
+
243
+ ### 方法 1:@Local refreshKey + ID 变化
244
+
245
+ ```typescript
246
+ @Local private refreshKey: number = 0
247
+
248
+ async switchLanguage(lang: string) {
249
+ await resourceManager.setPreferredLanguage([lang])
250
+ this.refreshKey++
251
+ }
252
+
253
+ build() {
254
+ Column() {
255
+ Text($r('app.string.hello'))
256
+ }
257
+ .id('page_' + this.refreshKey) // ID 变化触发重建
258
+ }
259
+ ```
260
+
261
+ ### 方法 2:router.replaceUrl 重新加载
262
+
263
+ ```typescript
264
+ import { router } from '@kit.ArkUI'
265
+
266
+ async switchLanguage(lang: string) {
267
+ await resourceManager.setPreferredLanguage([lang])
268
+ router.replaceUrl({ url: 'pages/SettingsPage' })
269
+ }
270
+ ```
271
+
272
+ ### 方法 3:Navigation 模式(V2)
273
+
274
+ ```typescript
275
+ @Local private navPathStack: NavPathStack = new NavPathStack()
276
+ @Local private refreshKey: number = 0
277
+
278
+ build() {
279
+ Navigation(this.navPathStack) {
280
+ // 内容
281
+ }
282
+ .id('nav_' + this.refreshKey)
283
+ }
284
+ ```
285
+
286
+ ### 方法 4:@Monitor 触发副作用(V2 新增)
287
+
288
+ ```typescript
289
+ @ComponentV2
290
+ struct PageWithMonitor {
291
+ @Local locale: LocaleModel = AppStorageV2.connect(
292
+ LocaleModel, 'locale', () => new LocaleModel()
293
+ )!
294
+
295
+ @Monitor('locale.currentLanguage')
296
+ onLanguageChange(monitor: IMonitor): void {
297
+ // currentLanguage 改变时执行副作用:埋点、缓存清理等
298
+ console.info('Language changed to:', this.locale.currentLanguage)
299
+ }
300
+
301
+ build() {
302
+ Text($r('app.string.hello'))
303
+ .id('page_' + this.locale.refreshKey)
304
+ }
305
+ }
306
+ ```
307
+
308
+ ---
309
+
310
+ ## 获取支持的语言列表
311
+
312
+ ```typescript
313
+ import { resourceManager } from '@kit.LocalizationKit'
314
+
315
+ interface LanguageInfo {
316
+ language: string // 如 'zh'
317
+ region?: string // 如 'CN'
318
+ displayName: string // 如 '简体中文'
319
+ }
320
+
321
+ async function getSupportedLanguages(): Promise<LanguageInfo[]> {
322
+ // 这是一个示例,实际 API 可能不同
323
+ const supported = ['zh', 'zh-Hans', 'zh-Hant', 'en', 'ja', 'ko']
324
+ const displayNames: Record<string, string> = {
325
+ 'zh': '中文',
326
+ 'zh-Hans': '简体中文',
327
+ 'zh-Hant': '繁体中文',
328
+ 'en': 'English',
329
+ 'ja': '日本語',
330
+ 'ko': '한국어'
331
+ }
332
+
333
+ return supported.map(lang => ({
334
+ language: lang,
335
+ displayName: displayNames[lang] || lang
336
+ }))
337
+ }
338
+ ```
339
+
340
+ ---
341
+
342
+ ## 监听系统语言变化(V2)
343
+
344
+ ### 在 UIAbility 中处理(写入 AppStorageV2)
345
+
346
+ ```typescript
347
+ // EntryAbility.ets
348
+ import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit'
349
+ import { hilog } from '@kit.PerformanceAnalysisKit'
350
+ import { AppStorageV2 } from '@kit.ArkUI'
351
+ import { LocaleModel } from '../common/LocaleModel'
352
+
353
+ export default class EntryAbility extends UIAbility {
354
+ onLanguageConfigurationUpdate(): void {
355
+ hilog.info(0x0000, 'EntryAbility', 'System language configuration updated')
356
+
357
+ // 1. 拿到全局 LocaleModel(与组件 connect 同一实例)
358
+ const locale = AppStorageV2.connect(
359
+ LocaleModel,
360
+ 'locale',
361
+ () => new LocaleModel()
362
+ )!
363
+
364
+ // 2. 重新获取当前语言
365
+ this.context.resourceManager.getPreferredLanguage().then((langs: string[]) => {
366
+ locale.currentLanguage = langs[0] || 'zh'
367
+ locale.refreshKey++ // 通知所有页面刷新
368
+ })
369
+ }
370
+
371
+ onConfigurationUpdate(configuration: AbilityConstant.Configuration): void {
372
+ hilog.info(0x0000, 'EntryAbility', 'Configuration updated: %{public}s',
373
+ JSON.stringify(configuration))
374
+
375
+ if (configuration.language !== undefined) {
376
+ hilog.info(0x0000, 'EntryAbility', `Language changed to: ${configuration.language}`)
377
+ }
378
+ }
379
+ }
380
+ ```
381
+
382
+ ---
383
+
384
+ ## 完整页面刷新机制(V2)
385
+
386
+ ```typescript
387
+ // 确保语言切换后所有页面都刷新的完整 V2 模式
388
+
389
+ // 1. 全局 LocaleModel
390
+ @ObservedV2
391
+ class LocaleModel {
392
+ @Trace currentLanguage: string = 'zh'
393
+ @Trace refreshKey: number = 0
394
+
395
+ async switchLanguage(context: common.UIAbilityContext, lang: string): Promise<void> {
396
+ // 1. 切换语言
397
+ const resMgr = context.resourceManager
398
+ await resMgr.setPreferredLanguage([lang])
399
+
400
+ // 2. 更新状态:所有 connect 'locale' 的组件 @Trace 字段变化即刷新
401
+ this.currentLanguage = lang
402
+ this.refreshKey++
403
+ }
404
+ }
405
+
406
+ // 2. 任意页面用 @Local + AppStorageV2.connect 引用单例
407
+ @Entry
408
+ @ComponentV2
409
+ struct AnyPage {
410
+ @Local locale: LocaleModel = AppStorageV2.connect(
411
+ LocaleModel, 'locale', () => new LocaleModel()
412
+ )!
413
+
414
+ build() {
415
+ Column() {
416
+ Text($r('app.string.any_string'))
417
+ .fontSize(16)
418
+ }
419
+ .id('page_' + this.locale.refreshKey)
420
+ }
421
+ }
422
+ ```
423
+
424
+ ---
425
+
426
+ ## 注意事项
427
+
428
+ 1. **语言代码大小写**:`zh-Hans` vs `zh-hans` — 必须完全匹配
429
+ 2. **切换后需要时间生效**:异步操作,避免连续快速切换
430
+ 3. **并非所有页面都需要重建**:只有持有 `LocaleModel`(`@Local locale = AppStorageV2.connect(LocaleModel, 'locale', ...)`)并在 `build()` 中读取 `locale.refreshKey` 的组件才会重建
431
+ 4. **性能考虑**:避免高频调用,建议在语言切换按钮上做防抖
432
+ 5. **V1/V2 不混用**:同一 struct 内不要混用 `@State` 和 `@Local`,会编译报错
433
+
434
+ ---
435
+
436
+ ## 持久化用户语言偏好(V2 推荐 PersistenceV2)
437
+
438
+ V1 用 `PersistentStorage.persistProp('currentLanguage', 'zh')` + `@StorageLink`;V2 一站式:
439
+
440
+ ```typescript
441
+ // common/LocaleModel.ets
442
+ @ObservedV2
443
+ export class LocaleModel {
444
+ @Trace currentLanguage: string = 'zh'
445
+ @Trace refreshKey: number = 0
446
+ }
447
+
448
+ // 在任意位置 connect(自动持久化到磁盘 + UI 响应式)
449
+ @ComponentV2
450
+ struct App {
451
+ @Local locale: LocaleModel = PersistenceV2.globalConnect({
452
+ type: LocaleModel,
453
+ key: 'app_locale',
454
+ defaultCreator: () => new LocaleModel()
455
+ })!
456
+
457
+ build() {
458
+ Column() {
459
+ Text(`当前: ${this.locale.currentLanguage}`)
460
+ Button('切换 EN').onClick(() => {
461
+ this.locale.currentLanguage = 'en' // 自动落盘 + 触发所有引用刷新
462
+ })
463
+ }
464
+ }
465
+ }
466
+ ```
467
+
468
+ ---
469
+
470
+ ## 如果 setPreferredLanguage API 不存在的替代方案
471
+
472
+ > ⚠️ 如果官方 API 确认不存在 `setPreferredLanguage()`,可以使用以下替代方案
473
+
474
+ ### 方案 1:应用内状态管理(V2 推荐)
475
+
476
+ ```typescript
477
+ // common/LocaleModel.ets — V2 写法(同上)
478
+ @ObservedV2
479
+ export class LocaleModel {
480
+ @Trace currentLanguage: string = 'zh'
481
+ @Trace refreshKey: number = 0
482
+
483
+ setLanguage(lang: string): void {
484
+ this.currentLanguage = lang
485
+ this.refreshKey++
486
+ }
487
+ }
488
+ ```
489
+
490
+ **使用方式**:
491
+
492
+ ```typescript
493
+ // 页面中使用
494
+ @Entry
495
+ @ComponentV2
496
+ struct SettingsPage {
497
+ @Local locale: LocaleModel = AppStorageV2.connect(
498
+ LocaleModel, 'locale', () => new LocaleModel()
499
+ )!
500
+
501
+ build() {
502
+ Column() {
503
+ Button('中文')
504
+ .onClick(() => {
505
+ this.locale.setLanguage('zh')
506
+ // UI 会自动刷新,因为 @Local 持有 @ObservedV2 实例的 @Trace 属性
507
+ })
508
+
509
+ Button('English')
510
+ .onClick(() => {
511
+ this.locale.setLanguage('en')
512
+ })
513
+
514
+ Text($r('app.string.welcome'))
515
+ }
516
+ .id('page_' + this.locale.refreshKey)
517
+ }
518
+ }
519
+ ```
520
+
521
+ **优点**:
522
+ - ✅ 不依赖未知 API
523
+ - ✅ 完全可控
524
+ - ✅ 即时生效
525
+ - ✅ V2 类型安全(@Trace 属性强类型,不再有 string key 拼写错误)
526
+
527
+ **缺点**:
528
+ - ⚠️ 仅应用内生效,不影响系统语言
529
+ - ⚠️ 需要手动维护语言状态
530
+
531
+ ---
532
+
533
+ ### 方案 2:引导用户到系统设置
534
+
535
+ ```typescript
536
+ import { common, Want } from '@kit.AbilityKit'
537
+ import { promptAction } from '@kit.ArkUI'
538
+
539
+ async function openSystemLanguageSettings(): Promise<void> {
540
+ try {
541
+ const context = getContext(this) as common.UIAbilityContext
542
+ const want: Want = {
543
+ action: 'action.settings.language',
544
+ }
545
+
546
+ context.startAbility(want)
547
+ .then(() => {
548
+ hilog.info(DOMAIN, TAG, 'Opened language settings')
549
+ })
550
+ .catch((err: Error) => {
551
+ promptAction.showToast({ message: '无法打开设置' })
552
+ })
553
+ } catch (err) {
554
+ promptAction.showToast({ message: '打开设置失败' })
555
+ }
556
+ }
557
+ ```
558
+
559
+ **使用场景**:
560
+ - 当无法动态切换时
561
+ - 用户需要永久修改系统语言
562
+
563
+ ---
564
+
565
+ ### 方案 3:重启应用应用语言
566
+
567
+ ```typescript
568
+ import { common } from '@kit.AbilityKit'
569
+ import { process } from '@kit.BasicServicesKit'
570
+
571
+ function restartApp(): void {
572
+ const context = getContext(this) as common.UIAbilityContext
573
+ // ⚠️ 需要查证:HarmonyOS 是否支持应用重启
574
+ // Android: Process.killProcess(Process.myPid())
575
+ // HarmonyOS: 可能需要使用 context.terminateSelf() 然后重新启动
576
+ context.terminateSelf()
577
+ }
578
+ ```
579
+
580
+ > ⚠️ **注意**:此方案需要查证 HarmonyOS 是否支持应用自重启
581
+
582
+ ---
583
+
584
+ ### 最佳实践推荐
585
+
586
+ 1. **优先尝试官方 API**:查证 `setPreferredLanguage()` 是否存在
587
+ 2. **方案 1 作为备选**:V2 LocaleModel + AppStorageV2 / PersistenceV2 是通用解决方案
588
+ 3. **方案 2 作为兜底**:引导用户到系统设置
589
+ 4. **避免方案 3**:重启应用体验较差
590
+
591
+ ---
592
+
593
+ ## V1 → V2 迁移速查(仅本文涉及部分)
594
+
595
+ | V1 | V2 | 备注 |
596
+ |---|---|---|
597
+ | `@Component` | `@ComponentV2` | struct 装饰器 |
598
+ | `@State refreshKey: number = 0` | `@Local refreshKey: number = 0` | 必须初始化 |
599
+ | `@StorageLink('currentLanguage') lang: string = 'zh'` | `@Local locale: LocaleModel = AppStorageV2.connect(LocaleModel, 'locale', () => new LocaleModel())!` 然后 `this.locale.currentLanguage` | 先抽 @ObservedV2 类 |
600
+ | `AppStorage.setOrCreate('refreshKey', n+1)` | `localeStore.refreshKey++` | 直接改 @Trace 字段 |
601
+ | `PersistentStorage.persistProp('lang', 'zh')` | `PersistenceV2.globalConnect({ type: LocaleModel, key, defaultCreator })` | V2 一站式 |
602
+ | `@Watch('lang') onLangChange()` | `@Monitor('lang') onLangChange(m: IMonitor)` | 方法装饰器 + IMonitor |
603
+
604
+ > V1 老项目兼容写法见 [`v1-compat.md`](./v1-compat.md)。