[鸿蒙从零到一] HarmonyOS 资源管理与多语言适配实战
把界面上的文字、颜色和图片直接写进 ArkTS,短期看很省事;一旦应用需要切换语言、适配深色模式或调整品牌视觉,散落在页面里的硬编码就会迅速成为维护负担。
HarmonyOS 提供了完整的资源限定词机制。开发者可以用同一个资源名组织不同语言、颜色模式和设备特征下的内容,由系统根据当前环境自动选择最匹配的资源。本文从基础目录开始,逐步完成字符串、颜色、媒体资源和中英文界面的适配,并补充工程中容易踩到的问题。
资源为什么不能全部写固定
下面这种写法能够运行,但扩展性很差:
Text('个人中心')
.fontColor('#182431')
.fontSize(20)
Image('/common/avatar.png')
它至少带来这些问题:
- 文案散落在代码中,翻译人员难以统一处理;
- 深色模式切换时,需要在多个组件中手动判断颜色;
- 图片路径与业务代码耦合,改名或替换资源容易漏改;
- 相同文案重复出现,无法保证用词一致;
- 不同模块可能定义出含义相同、命名不同的资源。
更稳妥的方式是让 ArkTS 只引用资源标识,把具体内容交给资源系统管理。
认识 resources 目录
Stage 模型的模块资源通常位于 entry/src/main/resources:
entry/src/main/resources/
├── base/
│ ├── element/
│ │ ├── string.json
│ │ ├── color.json
│ │ ├── float.json
│ │ └── plural.json
│ ├── media/
│ │ ├── avatar_default.png
│ │ └── ic_settings.svg
│ ├── profile/
│ └── rawfile/
├── zh_CN/
│ └── element/string.json
├── en_US/
│ └── element/string.json
└── dark/
└── element/color.json
base 是默认资源目录。当更具体的限定词目录没有匹配内容时,系统会回退到这里。zh_CN、en_US 是语言与地区限定目录,dark 用于深色模式。
资源按用途继续分类:
| 目录 | 典型用途 |
|---|---|
element |
字符串、颜色、尺寸、布尔值、复数等结构化资源 |
media |
PNG、WebP、SVG 等图片资源 |
profile |
页面清单、路由等 JSON 配置 |
rawfile |
需要按原始文件读取的模板、音频或其他文件 |
资源目录名称和文件格式有固定约定,不建议自行创建任意层级来代替限定词目录。
定义并引用字符串资源
先在 base/element/string.json 中准备默认文案:
{
"string": [
{
"name": "app_name", "value": "Harmony Shop" },
{
"name": "profile_title", "value": "个人中心" },
{
"name": "welcome_user", "value": "你好,%s" },
{
"name": "save", "value": "保存" }
]
}
ArkUI 组件中通过 $r 引用:
@Entry
@Component
struct ProfilePage {
build() {
Column({
space: 16 }) {
Text($r('app.string.profile_title'))
.fontSize(22)
.fontWeight(FontWeight.Bold)
Button($r('app.string.save'))
}
.width('100%')
.padding(20)
}
}
app 表示应用资源,string 表示资源类型,末尾是资源名。资源名应表达语义,例如 profile_title,不要使用 text1、label_a 这类难以维护的名称。
带占位符的文案可以通过资源管理器格式化:
import {
common } from '@kit.AbilityKit'
@Entry
@Component
struct WelcomePage {
@State welcomeText: string = ''
aboutToAppear(): void {
const context = getContext(this) as common.UIAbilityContext
context.resourceManager
.getStringValue($r('app.string.welcome_user').id, 'Lulu')
.then((value: string) => {
this.welcomeText = value
})
.catch((error: Error) => {
console.error(`load string failed: ${
error.message}`)
})
}
build() {
Text(this.welcomeText)
}
}
涉及姓名、数量等动态内容时,优先使用占位符,不要把可翻译文案拆成多个字符串后在代码里拼接。不同语言的语序可能完全不同。
配置中英文资源
在 zh_CN/element/string.json 中放中文内容:
{
"string": [
{
"name": "profile_title", "value": "个人中心" },
{
"name": "welcome_user", "value": "你好,%s" },
{
"name": "save", "value": "保存" }
]
}
在 en_US/element/string.json 中放英文内容:
{
"string": [
{
"name": "profile_title", "value": "Profile" },
{
"name": "welcome_user", "value": "Hello, %s" },
{
"name": "save", "value": "Save" }
]
}
页面仍然引用同一个资源名:
Text($r('app.string.profile_title'))
Button($r('app.string.save'))
系统会依据设备当前语言选择对应目录。业务代码不需要写 if (language === 'en')。这不仅减少判断,也能让运行时语言变化更自然地驱动界面刷新。
开发时要保证所有语言目录中的资源键保持一致。新增文案时,如果暂时没有完成翻译,也应确保 base 中存在可用的回退值,避免界面出现资源缺失。
使用颜色和尺寸资源统一视觉
base/element/color.json 可以定义浅色模式颜色:
{
"color": [
{
"name": "page_background", "value": "#F5F7FA" },
{
"name": "card_background", "value": "#FFFFFFFF" },
{
"name": "text_primary", "value": "#FF182431" },
{
"name": "brand_primary", "value": "#FF0A59F7" }
]
}
dark/element/color.json 使用相同资源名给出深色值:
{
"color": [
{
"name": "page_background", "value": "#FF111418" },
{
"name": "card_background", "value": "#FF1A1F24" },
{
"name": "text_primary", "value": "#FFE5EAF0" },
{
"name": "brand_primary", "value": "#FF6C9CFF" }
]
}
组件只关心语义:
Column() {
Text($r('app.string.profile_title'))
.fontColor($r('app.color.text_primary'))
Button($r('app.string.save'))
.backgroundColor($r('app.color.brand_primary'))
}
.backgroundColor($r('app.color.page_background'))
当系统切换颜色模式时,资源系统会选择对应颜色。这里的关键是按用途命名,而不是按色值命名。text_primary 比 black_90 更能表达设计意图,也允许深色模式使用完全不同的色值。
常用间距和圆角也可以放到 float.json:
{
"float": [
{
"name": "page_padding", "value": "20vp" },
{
"name": "card_radius", "value": "12vp" },
{
"name": "title_size", "value": "22fp" }
]
}
Column() {
}
.padding($r('app.float.page_padding'))
.borderRadius($r('app.float.card_radius'))
文字尺寸使用 fp,布局尺寸通常使用 vp,让界面适应不同密度和字体缩放设置。
管理图片与原始文件
放入 base/media 的图片可以通过资源引用:
Image($r('app.media.avatar_default'))
.width(72)
.height(72)
.borderRadius(36)
这种方式会在编译期检查资源,比手写相对路径更可靠。普通图标优先考虑 SVG,照片类素材可根据画质和体积选择 WebP 或 PNG。
rawfile 适合保留原始结构的文件,例如协议模板或本地初始化数据:
import {
common } from '@kit.AbilityKit'
const context = getContext(this) as common.UIAbilityContext
const data = await context.resourceManager.getRawFileContent('config/default.json')
const text = new TextDecoder().decode(data)
rawfile 通过文件名读取,不具备和普通资源完全相同的限定词匹配能力。需要本地化或随颜色模式变化的内容,应优先放入对应资源类型与限定词目录。
复数文案不要手动判断
“1 条消息”和“2 条消息”在不同语言中的规则并不只是单复数切换。HarmonyOS 的复数资源可以把语言规则从业务代码中分离出来。
base/element/plural.json 示例:
{
"plural": [
{
"name": "message_count",
"value": [
{
"quantity": "one", "value": "%d 条消息" },
{
"quantity": "other", "value": "%d 条消息" }
]
}
]
}
读取时传入数量:
const context = getContext(this) as common.UIAbilityContext
const count = 3
const text = await context.resourceManager.getPluralStringValue(
$r('app.plural.message_count').id,
count,
count
)
让资源系统处理语言规则,比在 ArkTS 中硬编码 count > 1 更准确,也更容易扩展新的语言。
在组件中建立资源边界
通用组件要避免依赖页面专属文案。可以接收 ResourceStr,让调用方决定使用字符串还是资源引用:
@Component
struct EmptyState {
title: ResourceStr = ''
actionText: ResourceStr = ''
onAction: () => void = () => {
}
build() {
Column({
space: 12 }) {
Image($r('app.media.empty_box'))
.width(120)
.height(120)
Text(this.title)
.fontColor($r('app.color.text_primary'))
Button(this.actionText)
.onClick(this.onAction)
}
}
}
调用页面负责提供业务语义:
EmptyState({
title: $r('app.string.order_empty'),
actionText: $r('app.string.go_shopping'),
onAction: () => {
// 跳转到商品页面
}
})
这样既保留组件复用能力,也不会把翻译后的具体字符串提前固化在组件内部。
多语言界面还要关注布局
替换字符串只是本地化的一部分。英文、德文等语言的文案通常比中文更长,界面还要处理文本膨胀:
- 按钮不要依赖固定宽度,优先使用内边距和最小宽度;
- 标题明确设置
maxLines与溢出策略; - 横向空间紧张时,允许关键内容使用
layoutWeight; - 不要用空格或换行符手工对齐文案;
- 检查大字体模式下是否截断或重叠;
- 面向从右到左书写的语言时,避免把“左侧”“右侧”写死为业务语义。
例如操作栏可以这样设计:
Row({
space: 12 }) {
Text($r('app.string.profile_title'))
.maxLines(1)
.textOverflow({
overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
Button($r('app.string.save'))
.constraintSize({
minWidth: 80 })
.padding({
left: 16, right: 16 })
}
.width('100%')
常见问题与排查方法
资源名存在但界面仍显示默认值
先检查限定词目录名称是否正确,再确认目标语言文件中资源类型、资源名与 base 完全一致。地区不匹配时系统可能选择更接近的目录或回退到默认资源。
修改资源后编译报重复定义
同一个限定词目录、同一种资源类型中不能重复定义相同名称。检查多个 element JSON 文件是否声明了相同资源键。
深色模式仍出现刺眼白块
通常是组件中保留了 Color.White 或十六进制硬编码。搜索页面和自定义组件中的直接色值,逐步替换为语义颜色资源。
翻译后按钮文字被截断
不要马上缩小字号。优先取消不必要的固定宽度,增加合理内边距,并测试长文本和系统大字体。字号过小会损害可读性和无障碍体验。
工程实践建议
资源规模增长后,可以建立以下约定:
- 公共资源使用稳定的语义命名,页面私有资源带业务前缀;
base始终保留完整、可运行的默认资源;- 新增资源时同步补齐目标语言,并在代码评审中检查;
- 颜色统一走设计令牌,不在业务组件中新增随意色值;
- 删除页面时同步清理不再引用的图片和文案;
- 在浅色、深色、中文、英文和大字体环境中执行界面验收。
还可以在持续集成中增加脚本,比较各语言 string.json 的资源名集合,及时发现漏翻或多余键值。
总结
HarmonyOS 的资源系统把文案、颜色、尺寸和图片从业务代码中解耦,并通过限定词目录自动适配语言、地区与颜色模式。实践中应以 base 作为可靠回退,用同名资源覆盖不同环境,通过 $r 和 resourceManager 访问内容,同时让组件只依赖具有明确语义的资源。
真正完善的多语言适配还包括长文本、大字体、布局弹性和复数规则。把这些能力纳入组件设计和测试流程,应用才能在语言与设备环境变化时保持一致、清晰且可维护的体验。