@harmonyos-arkts/d2h 0.0.0-stage → 0.1.1
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.
- package/.claude-plugin/marketplace.json +13 -0
- package/.claude-plugin/plugin.json +7 -0
- package/README.md +177 -2
- package/agents/android-to-hmos-00-orchestrator.md +312 -0
- package/agents/d2h.md +168 -0
- package/bin/install-opencode.mjs +57 -0
- package/install-opencode.sh +160 -0
- package/opencode/agents.json +16 -0
- package/package.json +43 -4
- package/schemas/android-source-manifest.schema.json +25 -0
- package/schemas/checkpoint-provenance.schema.json +67 -0
- package/schemas/final-acceptance.schema.json +29 -0
- package/schemas/managed-evidence-index.schema.json +15 -0
- package/schemas/managed-evidence.schema.json +114 -0
- package/schemas/migration-config.schema.json +51 -0
- package/schemas/migration-report-index.schema.json +39 -0
- package/schemas/migration-report-item.schema.json +51 -0
- package/schemas/migration-report-summary.schema.json +25 -0
- package/schemas/migration-status.schema.json +202 -0
- package/schemas/preflight.schema.json +60 -0
- package/schemas/source-order-audit.schema.json +16 -0
- package/schemas/source-provenance-event.schema.json +19 -0
- package/schemas/spec-app-shard.schema.json +45 -0
- package/schemas/spec-fact-corrections-shard.schema.json +13 -0
- package/schemas/spec-features-shard.schema.json +43 -0
- package/schemas/spec-index.schema.json +26 -0
- package/schemas/spec-interactions-shard.schema.json +40 -0
- package/schemas/spec-page.schema.json +98 -0
- package/schemas/spec-pages-index.schema.json +1 -0
- package/schemas/spec-unresolved-shard.schema.json +1 -0
- package/schemas/task-envelope.schema.json +55 -0
- package/schemas/task-plan.schema.json +123 -0
- package/skills/android2hmos_resources_convert/SKILL.md +162 -0
- package/skills/android2hmos_resources_convert/references/image-conversion-rules.md +230 -0
- package/skills/android2hmos_resources_convert/references/svg-fix-patterns.md +175 -0
- package/skills/android2hmos_resources_convert/references/xml-drawable-to-svg-rules.md +513 -0
- package/skills/appgraph-rule-audit/SKILL.md +58 -0
- package/skills/appgraph-rule-audit/references/audit-contract.md +47 -0
- package/skills/appgraph-rule-audit/references/recommendation-schema.md +41 -0
- package/skills/appgraph-rule-audit/schemas/rule-opportunities.schema.json +125 -0
- package/skills/arkts-app-identity/SKILL.md +238 -0
- package/skills/arkts-i18n/SKILL.md +496 -0
- package/skills/arkts-i18n/evals/evals.json +84 -0
- package/skills/arkts-i18n/references/code-examples.md +302 -0
- package/skills/arkts-i18n/references/common-pitfalls.md +391 -0
- package/skills/arkts-i18n/references/dynamic-language-switch.md +604 -0
- package/skills/arkts-i18n/references/hardcoded-string-scanner.md +348 -0
- package/skills/arkts-i18n/references/language-codes.md +104 -0
- package/skills/arkts-i18n/references/resource-file-structure.md +775 -0
- package/skills/arkts-i18n/references/static-vs-dynamic.md +242 -0
- package/skills/arkts-i18n/references/v1-compat.md +244 -0
- package/skills/arkts-i18n/scripts/audit_i18n_completeness.sh +174 -0
- package/skills/arkts-icon-sizing/SKILL.md +211 -0
- package/skills/arkts-icon-sizing/scripts/icon_audit.py +131 -0
- package/skills/arkts-icon-sizing/scripts/icon_autofix.py +88 -0
- package/skills/arkts-icon-sizing/scripts/icon_dims.py +179 -0
- package/skills/arkts-icon-sizing/scripts/icon_fix.py +119 -0
- package/skills/arkts-mvvm-architecture/SKILL.md +613 -0
- package/skills/harmonyos-migration-playbook/SKILL.md +56 -0
- package/skills/harmonyos-migration-playbook/agents/openai.yaml +7 -0
- package/skills/harmonyos-migration-playbook/references/arkts-compile.md +24 -0
- package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +53 -0
- package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +45 -0
- package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +23 -0
- package/skills/harmonyos-migration-playbook/references/ui-automation.md +52 -0
- package/skills/harmonyos-migration-playbook/references/windows-environment.md +38 -0
- package/skills/maintaining-migration-report/SKILL.md +155 -0
- package/skills/native-library-substitution/SKILL.md +385 -0
- package/skills/native-library-substitution/references/native-library-substitution.json +56906 -0
- package/skills/native-library-substitution/references/native-library-substitution.md +163 -0
- package/skills/preparing-migration-workspace/SKILL.md +124 -0
- package/skills/preparing-migration-workspace/toolchain.json +43 -0
- package/skills/reviewing-migration-process/SKILL.md +62 -0
- package/src/install-opencode.mjs +110 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: arkts-i18n
|
|
3
|
+
description: >
|
|
4
|
+
ArkTS/HarmonyOS 国际化(i18n)技能(V2 优先,API 12+)。当用户需要添加多语言支持、实现动态语言切换、配置资源文件、处理国际化资源目录结构、扫描硬编码字符串,或任何与"国际化"、"多语言"、"切换语言"相关的问题时,务必触发此 skill。
|
|
5
|
+
即使用户只是说"怎么添加英文"或"怎么做中英文切换",也应触发。
|
|
6
|
+
type: domain
|
|
7
|
+
domain: engineering
|
|
8
|
+
tags: [i18n, localization, multi-language, resource-manager]
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# ArkTS I18N — 国际化指南(V2 优先)
|
|
12
|
+
|
|
13
|
+
## 输入与统一结果
|
|
14
|
+
|
|
15
|
+
根据用户请求和当前工作区确定范围,始终执行同一套资源迁移、代码替换和完整性审计流程,不识别或修改外部编排状态。
|
|
16
|
+
|
|
17
|
+
输入:
|
|
18
|
+
|
|
19
|
+
- HarmonyOS 工程根路径;
|
|
20
|
+
- Android→HarmonyOS 迁移时提供 Android 工程根路径;纯 HarmonyOS i18n 工作不要求 Android 工程;
|
|
21
|
+
- 请求范围内实际读取到的 `values*/strings.xml`、代码引用和 HarmonyOS 资源文件;
|
|
22
|
+
- 目标 locales。未显式给出时,从 Android 语言限定符和现有 HarmonyOS 目录确定,不凭空新增语言。
|
|
23
|
+
|
|
24
|
+
返回结果:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"status": "completed | partial | failed",
|
|
29
|
+
"changedFiles": [],
|
|
30
|
+
"sourceFiles": [],
|
|
31
|
+
"evidence": [],
|
|
32
|
+
"domainChecks": [{"name": "", "status": "passed | failed", "evidence": []}],
|
|
33
|
+
"unresolved": [{"kind": "", "blocking": true, "location": "", "reason": ""}],
|
|
34
|
+
"verificationRequests": [{"type": "build | test | device | review", "scope": [], "reason": ""}],
|
|
35
|
+
"suggestedRemediation": [],
|
|
36
|
+
"details": {"locales": [], "translatedCount": 0}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`status` 只表示本 Skill 的领域工作状态,不表示工程已构建或功能已验证。不得把翻译摘要当作 Android 源码依据,也不得在仍有阻断性未解决翻译时返回 `completed`。需要构建、语言切换测试或设备核对时写入 `verificationRequests`。
|
|
41
|
+
|
|
42
|
+
## API 版本与项目策略
|
|
43
|
+
|
|
44
|
+
本 skill 的代码示例基于 **API 12+(HarmonyOS 5.0.0+)和 ArkTS V2 装饰器体系**。
|
|
45
|
+
|
|
46
|
+
> **项目锁 V2**:所有新生成的"语言切换 / locale 状态 / refreshKey"代码使用 V2 装饰器(`@ComponentV2 / @Local / @Param / @Once / @ObservedV2 / @Trace / AppStorageV2.connect / PersistenceV2.globalConnect`)。i18n 相关 API(`resourceManager` / `intl` / `$r('app.string.xxx')` / `setPreferredLanguage` / `getPreferredLanguage` / `onLanguageConfigurationUpdate`)在 V1/V2 之间**完全不变**。
|
|
47
|
+
>
|
|
48
|
+
> 仅当需要查阅老项目 V1 写法(`@Component / @State / @StorageLink('currentLanguage')` 等)时,参阅 [`references/v1-compat.md`](./references/v1-compat.md) 与现有 legacy 文档(已加历史标识)。
|
|
49
|
+
|
|
50
|
+
V1 → V2 关键差异(仅与本 skill 相关的 state 部分):
|
|
51
|
+
|
|
52
|
+
| V1 | V2 |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `@Component` | `@ComponentV2` |
|
|
55
|
+
| `@State refreshKey: number = 0` | `@Local refreshKey: number = 0` |
|
|
56
|
+
| `@StorageLink('currentLanguage') lang: string = 'zh'` | `@Local locale: LocaleModel = AppStorageV2.connect(LocaleModel, 'locale', () => new LocaleModel())!`(需先定义 `@ObservedV2 LocaleModel`) |
|
|
57
|
+
| `@Prop currentColumns: number = 3` | `@Param @Once currentColumns: number = 3`(只读) |
|
|
58
|
+
| `@Watch('lang') onLangChange()` | `@Monitor('lang') onLangChange(m: IMonitor)` |
|
|
59
|
+
| `AppStorage.setOrCreate('refreshKey', n)` | `localeModel.refreshKey = n`(直接改 @Trace 字段,自动同步) |
|
|
60
|
+
| `PersistentStorage.persistProp('currentLanguage', 'zh')` | `PersistenceV2.globalConnect({ type: LocaleModel, key: 'locale', defaultCreator: () => new LocaleModel() })` |
|
|
61
|
+
|
|
62
|
+
> **重要**:i18n 业务 API(`resourceManager.setPreferredLanguage()` / `$r()` / `onLanguageConfigurationUpdate()` 等)**与装饰器版本无关**。本次升级仅替换状态装饰器,API 调用方式不变。
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 资源迁移与补齐模式
|
|
67
|
+
|
|
68
|
+
当前请求或调用范围需要迁移、补齐文案时执行:
|
|
69
|
+
|
|
70
|
+
1. **读取真实来源**:读取请求范围内的 Android `values*/strings.xml`、Gradle `resValue`、引用这些资源的 Kotlin/Java/XML/Compose 文件,以及当前 HarmonyOS `resources/`。摘要只用于定位。
|
|
71
|
+
2. **确定 locales**:按 Android 语言限定符和现有 HarmonyOS 资源目录分组;非语言限定符不作为 locale。
|
|
72
|
+
3. **建立 key 映射**:保持同一业务文案在各 locale 的稳定 key;记录 Android 源文件和 HarmonyOS 目标文件。
|
|
73
|
+
4. **写入资源**:更新 `string.json`、`plural.json`、`strarray.json` 或语言图片,替换 `.ets` 中的用户可见硬编码文案。
|
|
74
|
+
5. **处理翻译缺口**:能够可靠翻译时写入目标 locale;无法可靠翻译或术语依据不足时保留明确占位并写入 `unresolved`,不得伪造已完成翻译。
|
|
75
|
+
6. **完整性审计**:运行 `scripts/audit_i18n_completeness.sh`,并扫描资源值和 ArkTS 文件中的 `[TODO: translate]`、裸 TODO 和未迁移用户可见文案。
|
|
76
|
+
7. **返回结果**:把修改文件、Android 源文件、locales、审计输出、未解决 key 和后续验证请求写入统一领域结果。
|
|
77
|
+
|
|
78
|
+
`audit_i18n_completeness.sh` 退出码 1、仍有缺失 key 或未解决翻译时返回 `partial`/`failed`。
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 核心概念
|
|
83
|
+
|
|
84
|
+
HarmonyOS 国际化基于「**资源文件目录分级**」和「**系统语言检测**」:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
resources/
|
|
88
|
+
├── base/ // Fallback 资源(无匹配时使用)
|
|
89
|
+
├── zh_CN/ // 简体中文
|
|
90
|
+
├── en_US/ // 美国英文
|
|
91
|
+
└── ja/ // 日文
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- **静态引用**:`$r('app.string.xxx')` 由系统根据当前语言自动加载
|
|
95
|
+
- **动态切换**:通过 `resourceManager.setPreferredLanguage()` 手动指定语言
|
|
96
|
+
|
|
97
|
+
### ⚠️ base/ 目录的作用
|
|
98
|
+
|
|
99
|
+
**base/ 不是"中文目录",而是 Fallback 目录!**
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
资源匹配规则:
|
|
103
|
+
1. 系统根据当前语言查找匹配目录(zh-CN → zh_CN、zh-Hans、zh)
|
|
104
|
+
2. 找到匹配目录?→ ✅ 使用该目录的资源
|
|
105
|
+
3. 未找到?→ 使用 base/ 目录的资源(Fallback)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 资源文件结构
|
|
111
|
+
|
|
112
|
+
### 标准目录结构
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
src/main/resources/
|
|
116
|
+
├── base/ # Fallback 资源
|
|
117
|
+
│ ├── element/
|
|
118
|
+
│ │ └── string.json # 字符串资源
|
|
119
|
+
│ └── media/ # 图片资源
|
|
120
|
+
├── zh_CN/ # 简体中文
|
|
121
|
+
│ └── element/
|
|
122
|
+
│ └── string.json
|
|
123
|
+
├── en_US/ # 美国英文
|
|
124
|
+
│ ├── element/
|
|
125
|
+
│ │ └── string.json
|
|
126
|
+
│ └── media/
|
|
127
|
+
│ └── logo.png # 英文版 logo(可选)
|
|
128
|
+
└── ja/ # 日文
|
|
129
|
+
└── element/
|
|
130
|
+
└── string.json
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 目录命名规范
|
|
134
|
+
|
|
135
|
+
| 类型 | 格式 | 示例 |
|
|
136
|
+
|------|------|------|
|
|
137
|
+
| 语言 | ISO 639-1 | `en`, `ja`, `ko`, `zh` |
|
|
138
|
+
| 语言+区域 | ISO 639-1 + ISO 3166-1 | `zh_CN`, `en_US`, `zh_TW` |
|
|
139
|
+
|
|
140
|
+
> ⚠️ 目录名必须完全匹配,否则系统无法识别
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 字符串资源文件
|
|
145
|
+
|
|
146
|
+
### JSON 格式
|
|
147
|
+
|
|
148
|
+
**base/element/string.json**:
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"string": [
|
|
152
|
+
{ "name": "app_name", "value": "App Name" },
|
|
153
|
+
{ "name": "confirm", "value": "Confirm" }
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**zh_CN/element/string.json**:
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"string": [
|
|
162
|
+
{ "name": "app_name", "value": "应用名称" },
|
|
163
|
+
{ "name": "confirm", "value": "确认" }
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 引用方式
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// 静态引用
|
|
172
|
+
Text($r('app.string.app_name'))
|
|
173
|
+
Button($r('app.string.confirm'))
|
|
174
|
+
Image($r('app.media.logo'))
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 插值参数(占位符)
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
// string.json
|
|
181
|
+
{ "name": "file_count", "value": "共 %d 个文件" }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// 代码中替换
|
|
186
|
+
const str = resourceManager.getStringByNameSync('file_count')
|
|
187
|
+
const message = str.replace('%d', count.toString())
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## module.json5 配置
|
|
193
|
+
|
|
194
|
+
### 声明支持的语言
|
|
195
|
+
|
|
196
|
+
```json5
|
|
197
|
+
{
|
|
198
|
+
"module": {
|
|
199
|
+
"name": "entry",
|
|
200
|
+
"type": "entry",
|
|
201
|
+
"supportedLanguages": [
|
|
202
|
+
"zh-Hans", // 简体中文
|
|
203
|
+
"en", // 英文
|
|
204
|
+
"ja" // 日文
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
> ⚠️ `supportedLanguages` 声明后,`setPreferredLanguage()` 才能正确工作
|
|
211
|
+
> 当前代码仓未配置此字段,如需动态语言切换,建议添加
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## 动态语言切换(V2)
|
|
216
|
+
|
|
217
|
+
### 基本实现
|
|
218
|
+
|
|
219
|
+
V2 项目用 `@Local` 持有 refreshKey;i18n API 不变:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import { resourceManager } from '@kit.LocalizationKit'
|
|
223
|
+
import { common } from '@kit.AbilityKit'
|
|
224
|
+
|
|
225
|
+
@Entry
|
|
226
|
+
@ComponentV2
|
|
227
|
+
struct LanguageSettings {
|
|
228
|
+
private context = getContext(this) as common.UIAbilityContext
|
|
229
|
+
@Local private refreshKey: number = 0
|
|
230
|
+
|
|
231
|
+
async switchLanguage(lang: string): Promise<void> {
|
|
232
|
+
const resMgr = this.context.resourceManager
|
|
233
|
+
await resMgr.setPreferredLanguage([lang])
|
|
234
|
+
this.refreshKey++ // 触发 UI 重建
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
build() {
|
|
238
|
+
Column() {
|
|
239
|
+
Text($r('app.string.welcome'))
|
|
240
|
+
.fontSize(20)
|
|
241
|
+
}
|
|
242
|
+
.id('lang_' + this.refreshKey)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 全局共享 locale 状态(V2 推荐)
|
|
248
|
+
|
|
249
|
+
把 `currentLanguage` / `refreshKey` 封装到一个 `@ObservedV2 LocaleModel`,用 `AppStorageV2.connect` 在所有页面共享单例:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// common/LocaleModel.ets
|
|
253
|
+
@ObservedV2
|
|
254
|
+
export class LocaleModel {
|
|
255
|
+
@Trace currentLanguage: string = 'zh'
|
|
256
|
+
@Trace refreshKey: number = 0
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 任意 V2 组件
|
|
260
|
+
@ComponentV2
|
|
261
|
+
struct AnyPage {
|
|
262
|
+
@Local locale: LocaleModel = AppStorageV2.connect(
|
|
263
|
+
LocaleModel,
|
|
264
|
+
'locale',
|
|
265
|
+
() => new LocaleModel()
|
|
266
|
+
)!
|
|
267
|
+
|
|
268
|
+
build() {
|
|
269
|
+
Column() {
|
|
270
|
+
Text($r('app.string.hello'))
|
|
271
|
+
.id('page_' + this.locale.refreshKey) // refreshKey 改 → 自动重建
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
> 修改 `this.locale.refreshKey++` 即可同步刷新所有 `connect` 同一 key 的组件,无需 V1 `@StorageLink` 散字段。
|
|
278
|
+
|
|
279
|
+
### 获取当前系统语言
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
async getCurrentLanguage(): Promise<string> {
|
|
283
|
+
const resMgr = this.context.resourceManager
|
|
284
|
+
const languages = await resMgr.getPreferredLanguage()
|
|
285
|
+
return languages[0] || 'zh'
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### 监听语言变化
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
// 在 EntryAbility 中
|
|
293
|
+
onLanguageConfigurationUpdate(): void {
|
|
294
|
+
hilog.info(0x0000, 'I18N', 'Language changed')
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## 图片国际化
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
resources/
|
|
304
|
+
├── base/media/logo.png # 默认 logo
|
|
305
|
+
├── en/media/logo.png # 英文版 logo(同名)
|
|
306
|
+
└── zh_CN/media/logo.png # 中文版 logo(同名)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
// 同一引用路径,不同语言加载不同文件
|
|
311
|
+
Image($r('app.media.logo'))
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
> ⚠️ 不同语言目录下的图片**文件名必须一致**
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## 常见错误
|
|
319
|
+
|
|
320
|
+
### 错误 1:硬编码字符串
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// ❌ 错误
|
|
324
|
+
Text('确认')
|
|
325
|
+
Button('登录')
|
|
326
|
+
|
|
327
|
+
// ✅ 正确
|
|
328
|
+
Text($r('app.string.confirm'))
|
|
329
|
+
Button($r('app.string.login'))
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### 错误 2:目录命名错误
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
// ❌ 错误
|
|
336
|
+
resources/chinese/element/string.json
|
|
337
|
+
|
|
338
|
+
// ✅ 正确
|
|
339
|
+
resources/zh_CN/element/string.json
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### 错误 3:未声明 supportedLanguages
|
|
343
|
+
|
|
344
|
+
导致 `setPreferredLanguage()` 不生效。
|
|
345
|
+
|
|
346
|
+
### 错误 4:动态切换后 UI 不更新
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
// ❌ 错误 — 直接返回
|
|
350
|
+
await resMgr.setPreferredLanguage([lang])
|
|
351
|
+
|
|
352
|
+
// ✅ 正确 — 触发重建
|
|
353
|
+
await resMgr.setPreferredLanguage([lang])
|
|
354
|
+
this.refreshKey++
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### 错误 5:string.json 格式错误
|
|
358
|
+
|
|
359
|
+
```json
|
|
360
|
+
// ❌ 错误 — value 被双引号包裹
|
|
361
|
+
{ "name": "app_name", "value": "\"图库\"" }
|
|
362
|
+
|
|
363
|
+
// ✅ 正确
|
|
364
|
+
{ "name": "app_name", "value": "图库" }
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## 硬编码字符串扫描与迁移
|
|
370
|
+
|
|
371
|
+
当用户需要**扫描项目中的硬编码字符串并迁移到 resources**时,执行以下流程:
|
|
372
|
+
|
|
373
|
+
### 步骤 1:扫描硬编码
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
rg "Text\(['\"]" --type ets -n
|
|
377
|
+
rg "Button\(['\"]" --type ets -n
|
|
378
|
+
rg "showToast\(['\"]" --type ets -n
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 步骤 2:识别需要迁移的字符串
|
|
382
|
+
|
|
383
|
+
**排除**:已使用 `$r()` 引用、URL、代码变量、正则、单字符
|
|
384
|
+
|
|
385
|
+
**需要迁移**:
|
|
386
|
+
```typescript
|
|
387
|
+
Text('确认')
|
|
388
|
+
Button('取消')
|
|
389
|
+
.title('设置页面')
|
|
390
|
+
promptAction.showToast('操作成功')
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### 步骤 3:生成资源 key
|
|
394
|
+
|
|
395
|
+
| 场景 | key 格式 | 示例 |
|
|
396
|
+
|------|---------|------|
|
|
397
|
+
| 按钮 | `btn_xxx` | `btn_confirm`, `btn_cancel` |
|
|
398
|
+
| 标题 | `title_xxx` | `title_settings` |
|
|
399
|
+
| 消息 | `msg_xxx` | `msg_delete_confirm` |
|
|
400
|
+
| Toast | `toast_xxx` | `toast_save_success` |
|
|
401
|
+
|
|
402
|
+
### 步骤 4:添加到 string.json
|
|
403
|
+
|
|
404
|
+
**base/element/string.json**:
|
|
405
|
+
```json
|
|
406
|
+
{ "name": "confirm", "value": "确认" }
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**en_US/element/string.json**:
|
|
410
|
+
```json
|
|
411
|
+
{ "name": "confirm", "value": "Confirm" }
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### 步骤 5:替换代码
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// 替换前
|
|
418
|
+
Text('确认')
|
|
419
|
+
|
|
420
|
+
// 替换后
|
|
421
|
+
Text($r('app.string.confirm'))
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### 步骤 6:验证
|
|
425
|
+
|
|
426
|
+
**1) 跨 locale key 一致性(自动)** — 跑本 skill 自带的完整性脚本:
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
bash <skill-root>/arkts-i18n/scripts/audit_i18n_completeness.sh \
|
|
430
|
+
--project-root <abs> \
|
|
431
|
+
--output-json docs/i18n-audit.json
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
职责:扫 `string.json` / `plural.json` / `strarray.json`,base 的全部 key 必须在每个 language locale(zh_CN / en_US / ...)出现;mode locale(dark / horizontal / vertical 等)按差异覆盖即可,豁免。
|
|
435
|
+
|
|
436
|
+
退出码:`0` 全 PASS / `1` 有 key_missing FAIL / `2` 脚本参数错误。
|
|
437
|
+
|
|
438
|
+
`audit_i18n_completeness.sh` 负责 locale 间 key 集合一致性,不负责识别占位文本。当前调用范围收尾时还必须使用文本搜索扫描 HarmonyOS 资源值和 `.ets` 文件中的 `[TODO: translate]`、裸 TODO,以及仍然硬编码的用户可见文案;发现项写入 `unresolved`。不得依赖其他 Skill 代为扫描。
|
|
439
|
+
|
|
440
|
+
**2) 手工 / 编译检查**:
|
|
441
|
+
|
|
442
|
+
- [ ] 所有用户可见字符串都已替换为 `$r()` 引用
|
|
443
|
+
- [ ] 替换后能正常编译
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## 生成检查清单
|
|
448
|
+
|
|
449
|
+
- [ ] 资源目录使用标准语言代码
|
|
450
|
+
- [ ] string.json 格式正确
|
|
451
|
+
- [ ] 所有用户可见字符串使用 `$r()` 引用
|
|
452
|
+
- [ ] module.json5 中声明 `supportedLanguages`(如需动态切换)
|
|
453
|
+
- [ ] 不同语言目录下的图片文件名一致
|
|
454
|
+
- [ ] 已扫描硬编码字符串并迁移到 resources
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## API 验证状态
|
|
459
|
+
|
|
460
|
+
| API | 状态 |
|
|
461
|
+
|-----|------|
|
|
462
|
+
| `resourceManager.getStringByNameSync()` | ✅ 已验证 |
|
|
463
|
+
| `resourceManager.getStringByName()` | ⚠️ 需查证 |
|
|
464
|
+
| `setPreferredLanguage()` | ⚠️ 需查证 |
|
|
465
|
+
| `getPreferredLanguage()` | ⚠️ 需查证 |
|
|
466
|
+
| `$r('app.string.xxx')` | ✅ 已验证 |
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## References
|
|
471
|
+
|
|
472
|
+
详细文档和完整代码示例请参阅(**全部代码示例已升级 V2**):
|
|
473
|
+
|
|
474
|
+
| 文件 | 内容 |
|
|
475
|
+
|------|------|
|
|
476
|
+
| `references/resource-file-structure.md` | 完整资源目录结构模板(与装饰器版本无关) |
|
|
477
|
+
| `references/dynamic-language-switch.md` | 动态语言切换完整代码(V2,含 LocaleModel) |
|
|
478
|
+
| `references/common-pitfalls.md` | 避坑指南与错误对照表(V2) |
|
|
479
|
+
| `references/hardcoded-string-scanner.md` | 硬编码扫描脚本和工具(与装饰器版本无关) |
|
|
480
|
+
| `references/language-codes.md` | 语言代码对照表(ISO 标准) |
|
|
481
|
+
| `references/code-examples.md` | ColumnsDialog 等代码示例(V2) |
|
|
482
|
+
| `references/static-vs-dynamic.md` | 纯静态 vs 动态切换方案对比 |
|
|
483
|
+
| `references/v1-compat.md` | **V1 兼容写法**(@Component / @State / @StorageLink),仅老项目查阅 |
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## 可选领域协作
|
|
488
|
+
|
|
489
|
+
下列能力存在时可以协作,但不改变本 Skill 的输入、流程或统一结果:
|
|
490
|
+
|
|
491
|
+
| 需要什么 | 读取哪里 |
|
|
492
|
+
|---------|---------|
|
|
493
|
+
| V2 状态管理、页面和跨页 locale 状态 | `arkts-mvvm-architecture` |
|
|
494
|
+
| 图片资源与语言图片迁移 | `android2hmos_resources_convert` |
|
|
495
|
+
|
|
496
|
+
本 Skill 不更新迁移报告、不创建 checkpoint、不调用 `complete`,也不选择仓库级构建或设备测试命令;这些动作通过 `verificationRequests` 交给调用方。
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "arkts-i18n",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "为我的应用添加日语支持,需要创建哪些文件和目录?",
|
|
7
|
+
"expected_output": "说明资源目录结构(resources/ja/element/string.json)、string.json 格式、module.json5 的 supportedLanguages 配置。提供两种命名方案:1) 仅语言代码(ja),2) 语言+区域代码(ja_JP)。强调使用 ISO 639-1 标准,目录名必须完全匹配。",
|
|
8
|
+
"files": []
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": 2,
|
|
12
|
+
"prompt": "用户可以手动切换应用语言,从中文切换到英文后 UI 也要跟着变,怎么实现?",
|
|
13
|
+
"expected_output": "说明两种方案:1) ⚠️ 未验证:setPreferredLanguage() + V2 @Local refreshKey(或 @ObservedV2 LocaleModel + AppStorageV2.connect)触发 UI 刷新(需查证官方文档),2) ✅ 已验证:应用内维护语言状态 + router.replaceUrl() 重载页面。提供 V2 完整代码示例(@ComponentV2 + @Local),强调需要 module.json5 中声明 supportedLanguages。",
|
|
14
|
+
"files": []
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": 3,
|
|
18
|
+
"prompt": "检查我的 string.json 文件有没有国际化问题:base/element/string.json 和 en_US/element/string.json",
|
|
19
|
+
"expected_output": "基于代码仓实际结构(base/ 和 en_US/),检查:1) key 是否完全一致(996 行对应),2) value 是否为字符串类型,3) JSON 格式是否正确(无尾随逗号),4) 是否有硬编码。提供代码仓验证结果:✅ base 和 en_US 的 key 完全对应。",
|
|
20
|
+
"files": [
|
|
21
|
+
"entry/src/main/resources/base/element/string.json",
|
|
22
|
+
"entry/src/main/resources/en_US/element/string.json"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": 4,
|
|
27
|
+
"prompt": "我的应用只支持中英文,应该用什么语言目录名?en 还是 en_US?",
|
|
28
|
+
"expected_output": "说明两种方案的优缺点:1) 简单方案(推荐):en/ + zh/,优点是简单适配所有区域,缺点是无法区分 zh_CN 和 zh_TW;2) 精确方案:en_US/ + zh_CN/,优点是精确区域划分,缺点是维护成本高。代码仓使用 en_US/(方案2)。提供选择建议和语言代码对照表。",
|
|
29
|
+
"files": []
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": 5,
|
|
33
|
+
"prompt": "代码中怎么获取字符串资源?有没有同步和异步两种方法?",
|
|
34
|
+
"expected_output": "提供代码仓验证的实现:✅ 同步方法:resourceManager.getStringByNameSync('key')(基于 ColumnsDialog.ets:11)。⚠️ 异步方法:resourceManager.getStringByName('key')(未验证)。提供完整代码示例,包括错误处理(try-catch)。",
|
|
35
|
+
"files": [
|
|
36
|
+
"entry/src/main/ets/components/dialogs/ColumnsDialog.ets"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": 6,
|
|
41
|
+
"prompt": "怎么判断当前应用加载的是中文还是英文资源?",
|
|
42
|
+
"expected_output": "提供代码仓验证的方法(基于 ColumnsDialog.ets:7-16):1) 使用 resourceManager.getStringByNameSync('dialog_column') 获取字符串,2) 用正则表达式 /[\\u4e00-\\u9fa5]/.test(str) 检测中文字符(Unicode 范围 0x4e00-0x9fa5),3) 返回布尔值表示是否中文环境。提供完整代码示例,包括 try-catch 错误处理和默认返回值。说明适用场景(需要根据语言执行不同逻辑)。",
|
|
43
|
+
"files": [
|
|
44
|
+
"entry/src/main/ets/components/dialogs/ColumnsDialog.ets"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": 7,
|
|
49
|
+
"prompt": "英文环境下,'1 column' 和 '2 columns' 这种复数怎么处理?",
|
|
50
|
+
"expected_output": "提供代码仓验证的完整实现(基于 ColumnsDialog.ets:18-31):1) 判断语言环境(isChineseLocale),2) 使用 resourceManager.getStringByNameSync('dialog_column') 获取字符串(含占位符 %d),3) 替换占位符 str.replace('%d', num.toString()),4) 判断是否需要添加 's'(条件:num > 1 && !isChineseLocale && !str.endsWith('s')),5) 提供 try-catch 兜底逻辑(return num === 1 ? '1 column' : `${num} columns`)。说明仅支持规则复数(+s),不规则复数需特殊处理。",
|
|
51
|
+
"files": [
|
|
52
|
+
"entry/src/main/ets/components/dialogs/ColumnsDialog.ets",
|
|
53
|
+
"entry/src/main/resources/base/element/string.json",
|
|
54
|
+
"entry/src/main/resources/en_US/element/string.json"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": 8,
|
|
59
|
+
"prompt": "我的应用只需要中英文,不需要应用内切换语言,应该怎么实现?",
|
|
60
|
+
"expected_output": "说明纯静态方案(基于代码仓实际实现):1) 组织资源文件(base/ + en_US/,确保 key 完全对应),2) 95%+ 使用 $r('app.string.xxx') 静态引用(270+ 处),3) 少数场景用 resourceManager.getStringByNameSync() 动态获取(如占位符替换、复数处理),4) 依赖系统语言自动匹配。对比动态方案:优点(简单、性能优、自动跟随系统、维护成本低),缺点(无法应用内切换、需重启应用)。说明代码仓选择原因和适用场景(大多数应用)。",
|
|
61
|
+
"files": [
|
|
62
|
+
"entry/src/main/resources/base/element/string.json",
|
|
63
|
+
"entry/src/main/resources/en_US/element/string.json"
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": 9,
|
|
68
|
+
"prompt": "占位符 %d 或 %s 怎么替换成实际的值?",
|
|
69
|
+
"expected_output": "提供两种方法:1) ✅ 手动替换(已验证):str.replace('%d', value.toString()) 或 str.replace('%s', value),基于 ColumnsDialog.ets:23。2) ⚠️ API 替换(未验证):resourceManager.getStringByName('key', param)。推荐方法1(更可靠)。提供完整代码示例,包括多个占位符的处理(链式 replace)。说明支持的占位符类型(%d 整数、%s 字符串、%f 浮点数)。",
|
|
70
|
+
"files": [
|
|
71
|
+
"entry/src/main/ets/components/dialogs/ColumnsDialog.ets"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": 10,
|
|
76
|
+
"prompt": "代码仓为什么要用 en_US 而不是 en 作为英文资源目录?",
|
|
77
|
+
"expected_output": "说明选择 en_US 的三大原因:1) 精确区域划分:明确标识为美国英语,与 en_GB(英国)、en_AU(澳大利亚)区分,符合 ISO 3166-1 标准。2) 扩展性好:未来可添加 en_GB、en_CA 等其他英文区域。3) 代码仓实际情况:base/ 和 en_US/ 的 key 完全对应(996 行),所有翻译已完成。对比 en/ 方案(简单但无法精确划分)。说明当前代码仓只需支持中英文,但使用 en_US/ 为未来扩展留出空间。",
|
|
78
|
+
"files": [
|
|
79
|
+
"entry/src/main/resources/base/element/string.json",
|
|
80
|
+
"entry/src/main/resources/en_US/element/string.json"
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|