HarmonyOS NEXT 实战:从零开发一个多语言翻译助手 App
HarmonyOS NEXT 实战:从零开发一个多语言翻译助手 App
本文记录使用 HarmonyOS NEXT (API 23) + ArkTS/ArkUI 从零开发一款完整翻译应用的全过程,包含架构设计、核心代码、编译排坑全记录。
一、前言
最近在钻研 HarmonyOS NEXT 应用开发,为了练手决定做一个翻译助手 App。市面上翻译软件很多,但自己动手做一个不仅能加深对 ArkTS/ArkUI 的理解,还能积累 Stage 模型下的完整开发经验。
本文你能学到
- HarmonyOS NEXT 项目结构拆解
- ArkTS 声明式 UI 开发实践(@Entry / @Component / @Builder / @State / @Prop)
- 数据持久化(Preferences API)的正确用法
- HTTP 网络请求 + 多 API 容灾策略
- ArkTS 编译错误的排查与修复(含 8 个典型错误)
- HarmonyOS SVG 图标的使用技巧
二、项目概况
功能清单
| 模块 | 功能 |
|---|---|
| 🌐 翻译核心 | 支持中/英/日/韩/法/德/西/俄/葡/意/泰/越 12 种语言互译 |
| 🔄 语言自动检测 | 输入文本自动识别语言 |
| 🕐 翻译历史 | 保存最近 200 条记录,支持搜索 |
| ⭐ 收藏夹 | 标记常用翻译 |
| 📋 一键复制 | 翻译结果复制到系统剪贴板 |
| 🔀 语言互换 | 源语言与目标语言一键交换 |
技术栈
- 语言/框架:ArkTS + ArkUI 声明式 UI
- SDK 版本:API 23 (HarmonyOS NEXT)
- 数据持久化:
@kit.ArkData→preferences - 网络请求:
@kit.NetworkKit→http - 翻译 API:LibreTranslate(主)+ MyMemory(备用)
- 应用模型:Stage 模型
三、项目结构
MyApplication/
├── AppScope/ # 应用全局配置
│ └── app.json5 # bundleName、图标、版本
├── entry/src/main/
│ ├── ets/
│ │ ├── entryability/ # UIAbility 生命周期入口
│ │ ├── model/ # 数据模型
│ │ │ └── TranslationEntry.ets # 翻译记录 + 语言定义
│ │ ├── data/ # 数据持久化层
│ │ │ └── PreferencesManager.ets
│ │ ├── service/ # 业务服务层
│ │ │ └── TranslationService.ets
│ │ ├── utils/ # 工具类
│ │ │ └── SystemUtils.ets # 剪贴板 + 时间格式化
│ │ └── pages/
│ │ └── Index.ets # 主页面(三合一)
│ ├── resources/ # 资源文件
│ │ └── base/
│ │ ├── element/ # 颜色、字体、字符串
│ │ └── media/ # SVG 图标
│ └── module.json5 # 模块配置 + 权限
└── build-profile.json5 # 构建配置
四、核心代码实现
4.1 数据模型
定义翻译记录的数据结构和 12 种支持的语言:
// model/TranslationEntry.ets
export interface TranslationEntry {
id: string;
sourceLang: string;
targetLang: string;
sourceText: string;
targetText: string;
timestamp: number;
isFavorite: boolean;
}
export interface LanguageOption {
code: string;
name: string;
}
export const LANGUAGES: LanguageOption[] = [
{ code: 'zh', name: '中文' },
{ code: 'en', name: '英语' },
{ code: 'ja', name: '日语' },
{ code: 'ko', name: '韩语' },
{ code: 'fr', name: '法语' },
{ code: 'de', name: '德语' },
{ code: 'es', name: '西班牙语' },
{ code: 'ru', name: '俄语' },
{ code: 'pt', name: '葡萄牙语' },
{ code: 'it', name: '意大利语' },
{ code: 'th', name: '泰语' },
{ code: 'vi', name: '越南语' },
];
4.2 翻译服务层
双 API 容灾设计,主用 LibreTranslate(开源免费),备用 MyMemory:
// service/TranslationService.ets
export class TranslationService {
private static readonly LIBRE_TRANSLATE_URL = 'https://libretranslate.com/translate';
private static readonly MY_MEMORY_URL = 'https://api.mymemory.translated.net/get';
async translate(text: string, sourceLang: string, targetLang: string): Promise<string> {
if (!text.trim()) return '';
// 方案一:LibreTranslate
try {
const result = await this.translateLibre(text, sourceLang, targetLang);
if (result) return result;
} catch (err) {
// 自动切换到 MyMemory
}
// 方案二:MyMemory
try {
const result = await this.translateMyMemory(text, sourceLang, targetLang);
if (result) return result;
} catch (err) {
throw new Error('所有翻译服务均不可用');
}
}
// 语言自动检测
static detectLanguage(text: string): string {
for (let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i);
if (code >= 0x4e00 && code <= 0x9fff) return 'zh';
if ((code >= 0x3040 && code <= 0x309f) ||
(code >= 0x30a0 && code <= 0x30ff)) return 'ja';
if ((code >= 0xac00 && code <= 0xd7af) ||
(code >= 0x1100 && code <= 0x11ff)) return 'ko';
}
return 'en';
}
}
注意:使用 JSON.parse() 时在 ArkTS 中必须加上类型断言,否则会报 arkts-no-any-unknown 错误:
interface LibreTranslateResponse {
translatedText: string;
}
const json = JSON.parse(data.result as string) as LibreTranslateResponse;
4.3 数据持久化层
使用 HarmonyOS 的 Preferences API 存储翻译历史:
// data/PreferencesManager.ets
export class PreferencesManager {
private pref: preferences.Preferences | null = null;
async init(context: Context): Promise<void> {
this.pref = await preferences.getPreferences(context, STORE_NAME);
}
async addEntry(sourceLang: string, targetLang: string,
sourceText: string, targetText: string): Promise<TranslationEntry> {
const entries = await this.getAllEntries();
const newEntry: TranslationEntry = {
id: generateId(),
sourceLang, targetLang,
sourceText, targetText,
timestamp: Date.now(),
isFavorite: false,
};
entries.unshift(newEntry);
if (entries.length > 200) entries.length = 200;
await this.saveEntries(entries);
return newEntry;
}
async searchEntries(keyword: string): Promise<TranslationEntry[]> {
const entries = await this.getAllEntries();
const kw = keyword.toLowerCase();
return entries.filter(e =>
e.sourceText.toLowerCase().includes(kw) ||
e.targetText.toLowerCase().includes(kw)
);
}
}
4.4 主页面 UI 架构
采用三 Tab 设计:翻译 / 历史 / 收藏。使用 @Builder 拆分为多个构建函数:
// pages/Index.ets
@Entry
@Component
struct Index {
@State currentTab: number = 0;
@State sourceText: string = '';
@State translatedText: string = '';
@State isTranslating: boolean = false;
@State showLangPicker: boolean = false;
build() {
Column() {
this.buildTopBar(); // 顶部 Tab 导航
if (this.currentTab === 0) {
this.buildTranslationPage(); // 翻译页面
} else if (this.currentTab === 1) {
HistoryTab({...}); // 历史记录(子组件)
} else if (this.currentTab === 2) {
FavoritesTab({...}); // 收藏夹(子组件)
}
}
// 语言选择弹窗
.bindContentCover($$this.showLangPicker, this.buildLangPicker())
}
@Builder
buildLanguageBar() { /* 源语言 ↔ 目标语言选择栏 */ }
@Builder
buildInputArea() { /* 文本输入区域 */ }
@Builder
buildTranslateButton() { /* 翻译按钮 + Loading 动画 */ }
@Builder
buildResultArea() { /* 翻译结果 + 复制/朗读按钮 */ }
@Builder
buildLangPicker() { /* 语言选择底部弹窗 */ }
}
五、SVG 图标的最佳实践
HarmonyOS 的 Image 组件支持 .fillColor() 方法动态着色,但前提是 SVG 文件中不能内嵌 fill 属性。
✅ 正确的做法(无 fill):
<!-- ic_arrow_down.svg -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/>
</svg>
Image($r('app.media.ic_arrow_down'))
.width(12).height(12)
.fillColor($r('app.color.text_secondary')) // 动态设色
❌ 错误的做法(硬编码 fill 将覆盖 .fillColor()):
<path d="..." fill="#666666"/> <!-- 删掉 fill 属性 -->
六、编译排坑全记录(8个错误+17个警告)
这是本文最有价值的部分——我在 hvigor assembleHap 编译中踩了 8 个坑,完全解决后成功通过。
❌ 错误1:arkts-no-any-unknown
Use explicit types instead of "any", "unknown"
原因:ArkTS 不允许 JSON.parse() 的隐式 any 返回值。
解决:定义接口 + 类型断言:
interface LibreTranslateResponse { translatedText: string; }
const json = JSON.parse(data.result as string) as LibreTranslateResponse;
❌ 错误2:数字前缀污染
.bindContentCover(344this.showLangPicker, ...) ← 多出来的 344
原因:之前修复脚本的写入异常导致 $$ 被替换为 344。
解决:回退为 $$this.showLangPicker($$ 是 ArkUI 双向绑定语法)。
❌ 错误3:Promise → string 类型不匹配
Conversion of type 'Promise<ValueType>' to type 'string' may be a mistake
原因:preferences.get() 返回 ValueType,需要显式转型。
解决:
const entries: TranslationEntry[] = JSON.parse(jsonStr) as TranslationEntry[];
❌ 错误4:bindContentCover 参数类型错误
Argument of type 'number' is not assignable to parameter of type 'boolean'
原因:同上,344 数字被传入 isShow: boolean 参数。
解决:恢复为 $$this.showLangPicker。
❌ 错误5:this 不是 Context
Argument of type 'this' is not assignable to parameter of type 'Context'
原因:在 ArkUI 组件中 this 指向组件实例,不是 Context。
解决:用 getContext(this) 获取正确上下文:
ClipboardUtil.copyText(getContext(this), this.translatedText);
❌ 错误6:textSelectable(true) 过时
Argument of type 'true' is not assignable to parameter of type 'TextSelectableMode'
原因:API 23 中 textSelectable 参数改为枚举类型。
解决:
.textSelectable(TextSelectableMode.SELECTABLE)
❌ 错误7:私有属性不能作为组件构造参数传递
Property 'prefManager' is private and can not be initialized through the component constructor.
原因:子组件用 private 声明的属性不能从父组件传入。
解决:根据子组件角色改为 @State(有独立状态)或 @Prop(从父组件传递):
// 父组件传入
@State onSelectEntry: (entry: TranslationEntry) => void = () => {};
// 父组件传入 + 父组件数据
@Prop entry: TranslationEntry = {...};
@Prop onTap: () => void = () => {};
❌ 错误8:资源 $ 符号丢失
WARN: 'app_name' conflict, first declared.
原因:$string: / $media: / $color: 中的 $ 在文件写入时丢失。
解决:使用 Python chr(36) 在字符串拼接中注入 $ 符号,逐条修复 module.json5 中的所有资源引用:
D = chr(36) # dollar sign
content = content.replace(':module_desc', D + 'string:module_desc')
content = content.replace(':layered_image', D + 'media:layered_image')
💡 小贴士:在编写 ArkTS 文件时,
$在 Shell/Python 中有特殊含义,最好用文件 API 直接写入而非通过 Shell 拼接。
七、module.json5 配置要点
权限声明和 Ability 配置是必填项:
{
"module": {
"name": "entry",
"type": "entry",
"pages": "$profile:main_pages",
"abilities": [{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"exported": true,
"launchType": "singleton",
"skills": [{
"entities": ["entity.system.home"],
"actions": ["ohos.want.action.home"]
}]
}],
"requestPermissions": [{
"name": "ohos.permission.INTERNET",
"reason": "$string:permission_internet"
}]
}
}
注意配置的 AppScope/resources 和 entry/resources 都有各自的 string.json,其中 app_name 只在 AppScope 中定义一次,否则会报 'app_name' conflict 警告。
八、HarmonyOS NEXT 开发心得
8.1 Kit 导入机制
API 23 采用新的 @kit.* 模块导入体系:
| 能力 | 导入路径 |
|---|---|
| UI 组件 | @kit.ArkUI |
| 网络请求 | @kit.NetworkKit |
| 数据持久化 | @kit.ArkData |
| 系统剪贴板 | @kit.BasicServicesKit(⚠️ 不是 PasteboardKit) |
| 日志 | @kit.PerformanceAnalysisKit |
8.2 ArkTS 语法限制速查
| 限制 | 解决 |
|---|---|
不能有 any / unknown |
始终显式声明类型或用 as 断言 |
| 对象字面量不能独立赋值 | 直接作为函数参数传入 |
FontWeight.SemiBold 不存在 |
用 FontWeight.Medium 或 Bold |
FlexAlign.FlexEnd 不存在 |
用 FlexAlign.End |
true 不能传给枚举参数 |
用枚举值如 TextSelectableMode.SELECTABLE |
| 子组件私有属性不能接收外部传参 | 用 @State / @Prop / @Link |
8.3 翻译 API 选择
实测 @kit.TranslationKit 在公开 HarmonyOS SDK 23 中不存在,采用 LibreTranslate(开源免费,可自部署)+ MyMemory API(免费但有调用限制)双保险方案是当前最优选择。
九、总结
从零开始搭建一个 HarmonyOS NEXT 应用,涉及了 Stage 模型、ArkTS 声明式 UI、数据持久化、网络请求、SVG 图标等一系列技术点。编译阶段虽然踩了不少坑,但每个错误都让我对 ArkTS 的类型系统有了更深理解。
项目代码不到 1200 行(7 个文件),实现了翻译、历史、收藏三大核心模块,具备了一定的实用价值。


如果你也在学习 HarmonyOS 开发,欢迎在评论区交流!觉得有用的话点个赞 👍 支持一下~
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)