一个容易被漏掉的多语言场景
假设应用首次启动时自动创建:
早睡、喝水、运动、阅读、冥想、学习
如果直接把这些中文保存到 Habit.name,用户切换到英语后,界面其他内容变成英文,默认习惯却仍是中文。
如果为了切换语言而批量改名,又会遇到:
- 用户可能已经手动把“早睡”改成其他名字;
- 中英文名称可能与用户自定义名称冲突;
- 旧版本保存过英文或日文;
- 快速添加需要判断某个默认习惯是否已经存在。
这说明“默认数据”不是普通翻译文案,而是带身份的持久化实体。
一、默认习惯定义需要四类信息
项目定义:
export interface StarterHabit {
resourceName: string;
storedName: string;
legacyNames: string[];
icon: string;
color: string;
}
示例:
{
resourceName: 'starter_sleep',
storedName: 'sleep-early',
legacyNames: [
'早睡',
'Sleep early',
'早寝'
],
icon: 'moon_z',
color: '#5E7CE2'
}
各字段职责:
| 字段 | 作用 |
|---|---|
resourceName | 从当前语言资源获取显示名称 |
storedName | 新版本写入磁盘的稳定身份 |
legacyNames | 识别旧版本曾保存的翻译文本 |
icon/color | 默认视觉配置 |
二、创建时只保存稳定 storedName
export function makeStarterHabit(
definition: StarterHabit
): Habit {
const now: number = Date.now();
return {
id: newId(),
name: definition.storedName,
icon: definition.icon,
color: definition.color,
isActive: true,
createdAtMillis: now,
updatedAtMillis: now,
archivedAtMillis: 0
};
}
无论首次启动时界面是中文、英文还是日文,磁盘中都保存:
sleep-early
语言变化不会修改业务身份。
三、显示时再根据资源本地化
先判断某个 Habit.name 是否对应默认习惯:
private starterDefinition(
name: string
): StarterHabit | undefined {
return STARTER_HABITS.find(
(starter: StarterHabit) =>
starter.storedName === name ||
starter.legacyNames.includes(name)
);
}
然后显示:
private localizedHabitName(habit: Habit): string {
const starter = this.starterDefinition(habit.name);
return starter
? this.named(starter.resourceName)
: habit.name;
}
因此:
磁盘:sleep-early
中文显示:早睡
英文显示:Sleep early
日文显示:早寝
用户自定义习惯找不到 StarterDefinition,就原样显示。
四、legacyNames 解决什么问题
假设早期测试版本已经把“早睡”直接写进磁盘。
升级后如果只识别 sleep-early,这个旧习惯会被当作普通自定义习惯:
- 切到英文仍显示中文;
- 快速添加可能再创建一个 sleep-early;
- 默认图标迁移逻辑无法识别它。
加入:
legacyNames: ['早睡', 'Sleep early', '早寝']
可以在不清空旧数据的情况下继续识别。
legacyNames 是兼容层,不应该成为新写入格式。
五、快速添加去重也必须使用同一身份规则
private hasStarterHabit(
starter: StarterHabit
): boolean {
return this.habits.some((habit: Habit) =>
habit.name === starter.storedName ||
starter.legacyNames.includes(habit.name)
);
}
快速添加前调用:
const alreadyExists = this.hasStarterHabit(starter);
if (!alreadyExists) {
this.habits = [
...this.habits,
makeStarterHabit(starter)
];
}
显示和去重必须共享同一套身份判断,否则页面认为它是默认习惯,添加逻辑却认为它不存在。
六、用户真正改名后应该发生什么
如果用户把“早睡”改成“23:00 前睡觉”,新名称不在 legacyNames 中。
之后它应被视为用户自定义名称:
- 切换语言不自动翻译;
- 显示用户输入原文;
- 快速添加是否允许再次添加默认“早睡”,属于产品决策。
当前轻量模型通过 name 是否匹配默认集合来推断身份,简单但存在边界:
- 用户恰好自定义了一个名为“早睡”的习惯,会被识别为默认项;
- 只修改图标但未改名时,保存的本地化名称可能进入兼容路径;
- 默认项与用户项没有显式类型字段。
更成熟的模型可以增加:
interface Habit {
id: string;
customName: string;
starterKey: string;
// ...
}
规则变为:
starterKey 有值且 customName 为空
→ 使用资源翻译
customName 有值
→ 显示用户名称
这样身份不再依赖名称碰撞。
七、首次初始化为什么要有 seededStarterHabits
只判断 habits.length === 0 不够。
用户可能主动删除全部习惯,此时数组为空,但应用不应该在下次启动时又自动创建六个默认习惯。
项目同时检查:
if (
this.habits.length > 0 ||
this.settings.seededStarterHabits
) {
return;
}
首次引导结束后设置:
next.seededStarterHabits = true;
删除全部数据时也保持这个标记为 true,确保“删除”结果不会被自动初始化抵消。
八、默认数据应该在隐私同意前创建吗
不应该。
即使默认习惯不是用户输入,创建它也意味着应用开始写入本地状态。
项目在用户同意隐私并完成引导后才执行:
private async finishOnboarding(): Promise<void> {
this.seedStarterHabits();
// 保存引导状态
}
这样首次隐私门禁之前不会偷偷改变用户数据空间。
九、新增默认习惯需要同步哪些地方
新增一个默认习惯时至少要完成:
- 在
STARTER_HABITS添加稳定storedName; - 添加中英日
legacyNames; - 三份
string.json增加相同resourceName; - 选择可用系统图标 Key;
- 验证快速添加去重;
- 验证首次引导初始化;
- 验证语言切换;
- 验证用户改名后保持原文;
- 运行资源 Key 校验脚本。
项目通过 Node 脚本比较三种语言的 Key 集合,防止只补一种语言。
十、默认数据兼容测试矩阵
| 测试数据 | 中文 | 英文 | 日文 |
|---|---|---|---|
sleep-early | 早睡 | Sleep early | 早寝 |
旧值 早睡 | 早睡 | Sleep early | 早寝 |
旧值 早寝 | 早睡 | Sleep early | 早寝 |
自定义 23:00 睡觉 | 原文 | 原文 | 原文 |
另外测试:
- 旧值存在时快速添加不重复;
- 删除默认习惯后不会自动复活;
- 删除全部数据后重启仍为空;
- 新增资源 Key 三种语言一致;
- 导出 JSON 保留稳定身份。
总结
默认数据国际化的核心原则是:
- 显示名称与稳定身份分离;
- 新数据只保存稳定 Key;
- 旧翻译文本通过兼容集合识别;
- 显示和去重共享身份规则;
- 用户自定义名称不自动翻译;
- 自动初始化要有“一次性完成”标记;
- 模型复杂后应增加显式 starterKey。
只要默认内容会写入磁盘,它就不再是单纯的界面文案,而是需要版本兼容的数据协议。
本文案例来自“心晴手记(MoodMemoir)”HarmonyOS 版的六个默认习惯与中英日切换。

2040

被折叠的 条评论
为什么被折叠?



