30分钟上手 HarmonyOS NEXT:手把手教你做翻译 App
这篇文章摘要: 《30分钟开发HarmonyOS NEXT翻译应用:从零到上手的完整指南》 本文提供了一个完整的HarmonyOS NEXT翻译应用开发教程,适合零基础开发者快速上手。教程涵盖了从项目创建到应用发布的全流程,重点包括: 项目选择了一个实用翻译应用作为案例,涉及网络请求、数据存储、列表管理等核心开发技能 详细展示了项目结构设计、数据模型定义和翻译服务实现 使用LibreTransla
30分钟上手 HarmonyOS NEXT:手把手教你做翻译 App
零基础也能学会!从网络请求到数据存储,保姆级教程带你打造第一个鸿蒙翻译应用
前言:为什么选择翻译 App?
Hello,各位开发者!今天带大家入门 HarmonyOS NEXT 开发,我们选择一个实用且技术全面的项目——多语言翻译应用。
为什么是翻译 App?
✅ 网络请求:学习如何调用 API
✅ 数据存储:掌握本地持久化
✅ 列表管理:历史记录、收藏功能
✅ 状态管理:响应式 UI 更新
✅ 界面友好:实用的用户功能
最重要的是,做完之后真的可以拿来用!
SDK:HarmonyOS NEXT API 23
开发工具:DevEco Studio
准备好了吗?Let’s go! 🚀
一、先看最终效果
完成后的应用长这样:
翻译页:
- 输入文本,点击翻译,立即显示结果
- 自动检测语言(中英日韩自动识别)
- 一键切换源语言和目标语言
- 复制翻译结果到剪贴板
历史页:
- 自动保存所有翻译记录
- 实时搜索历史记录
- 点击历史项快速复用
- 左滑删除不需要的记录
收藏页:
- 收藏重要的翻译结果
- 快速查看收藏列表
- 取消收藏或删除
支持 12 种语言:中文、英语、日语、韩语、法语、德语、西班牙语、俄语、葡萄牙语、意大利语、泰语、越南语。
二、创建项目
2.1 新建项目
- 打开 DevEco Studio
- File → New → Create Project
- 选择 “Application” → “Empty Ability”
- 填写项目信息:
- Project name: MyApplication
- Bundle name: com.example.myapplication
- API: 23(HarmonyOS NEXT)
- 点击 Finish,等待项目创建完成
2.2 项目结构
创建后,右键 ets 文件夹,新建以下目录:
ets/
├── model/ # 数据模型
├── service/ # 业务服务
├── data/ # 数据管理
├── utils/ # 工具类
└── pages/ # 页面
三、定义数据模型
3.1 创建模型文件
右键 model → New → File,命名为 TranslationEntry.ets。
3.2 翻译记录模型
// model/TranslationEntry.ets
/**
* 翻译记录
*/
export interface TranslationEntry {
id: string; // 唯一标识
sourceLang: string; // 源语言
targetLang: string; // 目标语言
sourceText: string; // 源文本
targetText: string; // 翻译结果
timestamp: number; // 时间戳
isFavorite: boolean; // 是否收藏
}
3.3 语言选项
/**
* 语言选项
*/
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: '越南语' },
];
/**
* 根据代码获取语言名称
*/
export function getLanguageName(code: string): string {
const lang = LANGUAGES.find(l => l.code === code);
return lang ? lang.name : code;
}
/**
* 生成唯一ID
*/
export function generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
}
四、实现翻译服务
4.1 选择翻译 API
我选择了两个免费的翻译 API:
-
LibreTranslate(主)
- 开源翻译服务
- 免费无需 API Key
- 支持多语言
-
MyMemory(备用)
- 免费翻译 API
- 作为 LibreTranslate 的备用
4.2 创建服务文件
右键 service → New → File,命名为 TranslationService.ets。
4.3 实现翻译服务
// service/TranslationService.ets
import { http } from '@kit.NetworkKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
const DOMAIN = 0x0000;
const TAG = 'TranslationService';
/**
* 翻译服务(单例)
*/
export class TranslationService {
private static instance: TranslationService;
private static readonly LIBRE_URL = 'https://libretranslate.com/translate';
private static readonly MYMEMORY_URL = 'https://api.mymemory.translated.net/get';
private constructor() {}
static getInstance(): TranslationService {
if (!TranslationService.instance) {
TranslationService.instance = new TranslationService();
}
return TranslationService.instance;
}
/**
* 翻译文本
*/
async translate(text: string, sourceLang: string, targetLang: string): Promise<string> {
if (!text.trim()) return '';
// 先尝试 LibreTranslate
try {
const result = await this.translateWithLibre(text, sourceLang, targetLang);
if (result) return result;
} catch (err) {
hilog.warn(DOMAIN, TAG, 'LibreTranslate failed');
}
// 失败则使用 MyMemory
try {
const result = await this.translateWithMyMemory(text, sourceLang, targetLang);
if (result) return result;
} catch (err) {
hilog.error(DOMAIN, TAG, 'MyMemory also failed');
}
throw new Error('翻译失败,请检查网络连接');
}
/**
* LibreTranslate API
*/
private async translateWithLibre(text: string, source: string, target: string): Promise<string | null> {
return new Promise((resolve, reject) => {
const httpRequest = http.createHttp();
httpRequest.request(
TranslationService.LIBRE_URL,
{
method: http.RequestMethod.POST,
header: { 'Content-Type': 'application/json' },
extraData: JSON.stringify({
q: text,
source: source,
target: target,
format: 'text'
}),
connectTimeout: 8000,
readTimeout: 8000,
},
(err: BusinessError, data: http.HttpResponse) => {
httpRequest.destroy();
if (err) {
reject(err);
return;
}
try {
const json = JSON.parse(data.result as string);
resolve(json.translatedText || null);
} catch (e) {
reject(e);
}
}
);
});
}
/**
* MyMemory API(备用)
*/
private async translateWithMyMemory(text: string, source: string, target: string): Promise<string | null> {
return new Promise((resolve, reject) => {
const httpRequest = http.createHttp();
const url = `${TranslationService.MYMEMORY_URL}?q=${encodeURIComponent(text)}&langpair=${source}|${target}`;
httpRequest.request(
url,
{
method: http.RequestMethod.GET,
connectTimeout: 8000,
readTimeout: 8000,
},
(err: BusinessError, data: http.HttpResponse) => {
httpRequest.destroy();
if (err) {
reject(err);
return;
}
try {
const json = JSON.parse(data.result as string);
resolve(json.responseData?.translatedText || null);
} catch (e) {
reject(e);
}
}
);
});
}
/**
* 自动检测语言
*/
static detectLanguage(text: string): string {
if (!text.trim()) return 'en';
// 检测中文
for (let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i);
if ((code >= 0x4e00 && code <= 0x9fff) || (code >= 0x3400 && code <= 0x4dbf)) {
return 'zh';
}
}
// 检测日文
for (let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i);
if ((code >= 0x3040 && code <= 0x309f) || (code >= 0x30a0 && code <= 0x30ff)) {
return 'ja';
}
}
// 检测韩文
for (let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i);
if ((code >= 0xac00 && code <= 0xd7af) || (code >= 0x1100 && code <= 0x11ff)) {
return 'ko';
}
}
return 'en';
}
}
关键点:
- 单例模式:全局只有一个实例
- 双重容错:主API失败自动切换备用
- 资源释放:
httpRequest.destroy()防止内存泄漏 - 超时设置:8秒超时,避免长时间等待
五、数据持久化
5.1 创建数据管理器
右键 data → New → File,命名为 PreferencesManager.ets。
5.2 实现数据管理
// data/PreferencesManager.ets
import { preferences } from '@kit.ArkData';
import { TranslationEntry, generateId } from '../model/TranslationEntry';
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN = 0x0000;
const TAG = 'PreferencesManager';
const STORE_NAME = 'translator_store';
const KEY_HISTORY = 'history';
const MAX_HISTORY = 200;
/**
* 数据管理器(单例)
*/
export class PreferencesManager {
private static instance: PreferencesManager;
private pref: preferences.Preferences | null = null;
private constructor() {}
static getInstance(): PreferencesManager {
if (!PreferencesManager.instance) {
PreferencesManager.instance = new PreferencesManager();
}
return PreferencesManager.instance;
}
/**
* 初始化
*/
async init(context: Context): Promise<void> {
this.pref = await preferences.getPreferences(context, STORE_NAME);
hilog.info(DOMAIN, TAG, 'Preferences initialized');
}
/**
* 获取所有记录
*/
async getAllEntries(): Promise<TranslationEntry[]> {
if (!this.pref) return [];
const jsonStr = await this.pref.get(KEY_HISTORY, '[]') as string;
const entries = JSON.parse(jsonStr) as TranslationEntry[];
return entries.sort((a, b) => b.timestamp - a.timestamp);
}
/**
* 添加记录
*/
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);
// 限制最多200条
if (entries.length > MAX_HISTORY) {
entries.length = MAX_HISTORY;
}
await this.saveEntries(entries);
return newEntry;
}
/**
* 切换收藏
*/
async toggleFavorite(id: string): Promise<void> {
const entries = await this.getAllEntries();
const entry = entries.find(e => e.id === id);
if (entry) {
entry.isFavorite = !entry.isFavorite;
await this.saveEntries(entries);
}
}
/**
* 删除记录
*/
async deleteEntry(id: string): Promise<void> {
let entries = await this.getAllEntries();
entries = entries.filter(e => e.id !== id);
await this.saveEntries(entries);
}
/**
* 获取收藏列表
*/
async getFavorites(): Promise<TranslationEntry[]> {
const entries = await this.getAllEntries();
return entries.filter(e => e.isFavorite);
}
/**
* 搜索记录
*/
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)
);
}
/**
* 保存数据
*/
private async saveEntries(entries: TranslationEntry[]): Promise<void> {
if (!this.pref) return;
await this.pref.put(KEY_HISTORY, JSON.stringify(entries));
await this.pref.flush(); // 必须调用 flush!
}
}
关键点:
- Preferences:轻量级键值对存储
- flush():必须调用才能持久化到磁盘
- 限制条数:最多200条,防止数据过大
- 排序:按时间倒序,最新的在最前
六、工具类
6.1 创建工具文件
右键 utils → New → File,命名为 SystemUtils.ets。
6.2 实现工具函数
// utils/SystemUtils.ets
import { pasteboard } from '@kit.BasicServicesKit';
/**
* 剪贴板工具
*/
export class ClipboardUtil {
/**
* 复制文本到剪贴板
*/
static copyText(context: Context, text: string): void {
const data = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text);
const pb = pasteboard.getSystemPasteboard();
pb.setData(data);
}
}
/**
* 格式化时间
*/
export function formatTime(timestamp: number): string {
const date = new Date(timestamp);
const now = new Date();
const isToday = date.getDate() === now.getDate() &&
date.getMonth() === now.getMonth() &&
date.getFullYear() === now.getFullYear();
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const timeStr = `${hours}:${minutes}`;
if (isToday) {
return timeStr;
}
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
if (date.getFullYear() === now.getFullYear()) {
return `${month}-${day} ${timeStr}`;
}
return `${date.getFullYear()}-${month}-${day}`;
}
七、主页面实现
7.1 页面结构
在 pages/Index.ets 中实现:
@Entry
@Component
struct Index {
// 状态变量
@State currentTab: number = 0; // 当前标签
@State sourceLang: string = 'zh'; // 源语言
@State targetLang: string = 'en'; // 目标语言
@State sourceText: string = ''; // 输入文本
@State translatedText: string = ''; // 翻译结果
@State isTranslating: boolean = false; // 是否正在翻译
@State showResult: boolean = false; // 是否显示结果
@State showLangPicker: boolean = false; // 显示语言选择器
private tabs: string[] = ['翻译', '历史', '收藏'];
build() {
Column() {
// 顶部标签栏
this.buildTabs();
// 内容区域
if (this.currentTab === 0) {
this.buildTranslatePage();
} else if (this.currentTab === 1) {
this.buildHistoryPage();
} else {
this.buildFavoritesPage();
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
buildTabs() {
Row() {
ForEach(this.tabs, (tab: string, index: number) => {
Column() {
Text(tab)
.fontSize(16)
.fontColor(this.currentTab === index ? '#4ECDC4' : '#999999')
.fontWeight(this.currentTab === index ? FontWeight.Medium : FontWeight.Regular)
if (this.currentTab === index) {
Divider()
.width(24)
.height(3)
.color('#4ECDC4')
.margin({ top: 4 })
}
}
.layoutWeight(1)
.padding({ top: 12, bottom: 12 })
.onClick(() => this.currentTab = index)
})
}
.width('100%')
.backgroundColor(Color.White)
}
}
7.2 翻译页实现
@Builder
buildTranslatePage() {
Scroll() {
Column() {
// 语言选择栏
Row() {
this.langChip(getLanguageName(this.sourceLang), () => {
this.showLangPicker = true;
});
Image($r('app.media.ic_swap'))
.width(20)
.height(20)
.margin({ left: 16, right: 16 })
.onClick(() => {
const tmp = this.sourceLang;
this.sourceLang = this.targetLang;
this.targetLang = tmp;
});
this.langChip(getLanguageName(this.targetLang), () => {
this.showLangPicker = true;
});
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 16, bottom: 16 })
// 输入框
TextArea({
text: this.sourceText,
placeholder: '请输入要翻译的文本...'
})
.width('100%')
.height(140)
.backgroundColor(Color.White)
.borderRadius(12)
.padding(12)
.onChange(value => {
this.sourceText = value;
if (!value.trim()) {
this.showResult = false;
}
})
// 翻译按钮
Button(this.isTranslating ? '' : '翻译')
.width('100%')
.height(44)
.backgroundColor('#4ECDC4')
.borderRadius(22)
.margin({ top: 16 })
.enabled(!this.isTranslating && this.sourceText.trim().length > 0)
.onClick(() => this.doTranslate())
// 翻译结果
if (this.showResult) {
Column() {
Row() {
Text(getLanguageName(this.targetLang))
.fontSize(12)
.fontColor('#999999')
Blank()
Image($r('app.media.ic_copy'))
.width(20)
.height(20)
.onClick(() => {
ClipboardUtil.copyText(getContext(this), this.translatedText);
// 显示提示
})
}
.width('100%')
Text(this.translatedText)
.fontSize(16)
.margin({ top: 8 })
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ top: 16 })
}
}
.width('100%')
.padding(16)
}
}
@Builder
langChip(text: string, onTap: () => void) {
Row() {
Text(text).fontSize(14)
Image($r('app.media.ic_arrow_down'))
.width(12)
.height(12)
.margin({ left: 4 })
}
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor('#F0F0F0')
.borderRadius(16)
.onClick(onTap)
}
7.3 执行翻译
private translationService = TranslationService.getInstance();
private prefManager = PreferencesManager.getInstance();
async doTranslate(): Promise<void> {
if (!this.sourceText.trim()) return;
this.isTranslating = true;
this.showResult = false;
try {
// 自动检测语言
this.sourceLang = TranslationService.detectLanguage(this.sourceText);
// 调用翻译服务
const result = await this.translationService.translate(
this.sourceText,
this.sourceLang,
this.targetLang
);
this.translatedText = result;
this.showResult = true;
// 保存到历史
await this.prefManager.addEntry(
this.sourceLang,
this.targetLang,
this.sourceText,
result
);
} catch (err) {
// 显示错误提示
console.error('Translation failed:', err);
} finally {
this.isTranslating = false;
}
}
八、历史记录页
@Component
struct HistoryPage {
@State entries: TranslationEntry[] = [];
@State searchText: string = '';
private prefManager = PreferencesManager.getInstance();
aboutToAppear(): void {
this.loadHistory();
}
async loadHistory(): Promise<void> {
if (this.searchText.trim()) {
this.entries = await this.prefManager.searchEntries(this.searchText);
} else {
this.entries = await this.prefManager.getAllEntries();
}
}
build() {
Column() {
// 搜索框
Row() {
Image($r('app.media.ic_search')).width(16).height(16)
TextInput({
text: this.searchText,
placeholder: '搜索历史记录...'
})
.layoutWeight(1)
.onChange(value => {
this.searchText = value;
this.loadHistory();
})
}
.height(40)
.padding({ left: 12, right: 12 })
.backgroundColor('#F0F0F0')
.borderRadius(20)
.margin(16)
// 列表
if (this.entries.length === 0) {
Text('暂无翻译记录')
.fontSize(14)
.fontColor('#999999')
.margin({ top: 100 })
} else {
List() {
ForEach(this.entries, (entry: TranslationEntry) => {
ListItem() {
this.historyItem(entry);
}
})
}
.width('100%')
.layoutWeight(1)
}
}
.width('100%')
.height('100%')
}
@Builder
historyItem(entry: TranslationEntry) {
Column() {
Row() {
Text(`${getLanguageName(entry.sourceLang)} → ${getLanguageName(entry.targetLang)}`)
.fontSize(11)
.fontColor('#4ECDC4')
.backgroundColor('#F0FFFA')
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(8)
}
Text(entry.sourceText)
.fontSize(14)
.margin({ top: 8 })
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(entry.targetText)
.fontSize(13)
.fontColor('#666666')
.margin({ top: 4 })
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(formatTime(entry.timestamp))
.fontSize(11)
.fontColor('#999999')
Blank()
Image(entry.isFavorite ?
$r('app.media.ic_favorite_filled') :
$r('app.media.ic_favorite'))
.width(18)
.height(18)
.fillColor(entry.isFavorite ? '#FF6B6B' : '#CCCCCC')
.onClick(() => this.toggleFav(entry.id))
Image($r('app.media.ic_delete'))
.width(16)
.height(16)
.margin({ left: 8 })
.onClick(() => this.deleteEntry(entry.id))
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ left: 16, right: 16, bottom: 8 })
}
async toggleFav(id: string): Promise<void> {
await this.prefManager.toggleFavorite(id);
this.loadHistory();
}
async deleteEntry(id: string): Promise<void> {
await this.prefManager.deleteEntry(id);
this.loadHistory();
}
}
九、初始化应用
9.1 添加网络权限
在 entry/src/main/module.json5 中添加:
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "$string:permission_internet"
}
]
9.2 初始化数据服务
在 entry/src/main/ets/entryability/EntryAbility.ets 中:
import PreferencesManager from '../data/PreferencesManager';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 初始化数据管理器
PreferencesManager.getInstance().init(this.context);
}
}
十、运行测试
10.1 连接模拟器
- 点击 DevEco Studio 顶部设备下拉框
- 选择 “Device Manager”
- 启动模拟器(Phone)
- 等待启动完成
10.2 运行应用
- 点击 ▶️ 按钮(Run)
- 等待编译和安装
- 应用自动启动


10.3 功能测试
✅ 测试清单:
翻译功能:
- 输入中文翻译为英文
- 输入英文翻译为中文
- 自动检测语言
- 切换语言
- 复制结果
历史记录:
- 查看历史列表
- 搜索历史
- 收藏翻译
- 删除记录
收藏功能:
- 查看收藏列表
- 取消收藏
十一、常见问题
Q1: Canvas 绑制不出来?
确保在 onReady 后才绘制。不过本应用没有用到 Canvas,所以不用担心这个问题。
Q2: 网络请求失败?
检查权限配置:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
]
Q3: 数据不保存?
确保调用了 flush():
await this.pref.put('key', value);
await this.pref.flush(); // 必须调用!
Q4: 列表不刷新?
重新赋值整个数组:
// ❌ 错误
this.entries[0].isFavorite = true;
// ✅ 正确
const newEntries = [...this.entries];
newEntries[0].isFavorite = true;
this.entries = newEntries;
Q5: 翻译API不可用?
切换到备用API,或者检查网络连接。本应用已实现双重容错机制。
十二、总结
你学到了什么
| 知识点 | 掌握程度 |
|---|---|
| 网络请求(http.createHttp) | ⭐⭐⭐⭐⭐ |
| 数据持久化(Preferences) | ⭐⭐⭐⭐ |
| 单例模式 | ⭐⭐⭐⭐⭐ |
| 列表渲染(ForEach) | ⭐⭐⭐⭐ |
| 状态管理(@State) | ⭐⭐⭐⭐⭐ |
项目亮点
✨ 双重容错:LibreTranslate + MyMemory 备用
✨ 自动检测:中英日韩自动识别
✨ 完整功能:翻译、历史、收藏
✨ 友好体验:实时搜索、错误提示
十三、下一步学习
完成基础功能后,可以继续扩展:
功能扩展
- 语音输入:接入语音识别
- 语音朗读:TTS 服务
- 图片翻译:OCR + 翻译
- 离线翻译:集成离线模型
技术深化
- 使用 RDB 数据库替代 Preferences
- 实现翻译结果缓存
- 添加动画效果
本文适合初学者快速上手,帮你 30 分钟构建第一个鸿蒙翻译应用。如有问题,欢迎评论区交流!
截图位置:
- 翻译主界面(中文→英文翻译)
- 语言选择列表(12种语言)
- 历史记录页面(搜索功能)
- 收藏记录页面
更多推荐



所有评论(0)