
如果你做过软件本地化项目,你一定遇到过这种情况:翻译工作明明已经完成,测试却告诉你某些界面上的文字还是英文,或者直接显示成一团乱码。你去检查代码,发现开发人员在源代码里直接写死了中文文本,比如 msgbox("请输入正确的手机号码") 这样的写法。这种把文本内容直接嵌入代码的做法,就是我们今天要聊的"硬编码"问题。
硬编码可以说是本地化翻译的"隐形杀手"。它不像语法错误那样会直接报红报错,而是悄悄藏在代码的某个角落,等你发现的时候,可能已经延误了项目进度。那么,到底怎么解决这个问题呢?康茂峰在多年的本地化服务实践中,积累了不少实战经验,今天就给大家系统地梳理一下。
在说解决方法之前,我们得先搞清楚硬编码为什么会让本地化工作变得这么麻烦。简单来说,硬编码就是把用户可见的文本直接写在程序代码里,而不是存放在外部的资源文件中。
这样做的问题在于,当你要把软件翻译成另一种语言时,翻译人员根本没办法接触到这些文本。他们面对的是一堆代码,而不是独立的翻译文件。更麻烦的是,每种语言的文本长度、复数形式、日期格式都不一样。中文一句"确定"两个字符,翻译成德语可能变成"Bestätigen"十个字符,如果代码里固定了文本框的长度,界面就会变形。阿拉伯语从右往左写,如果界面布局写死了,文字就会显示错位。
还有一点很让人头疼的是,同一个词在代码里可能重复出现好多次。开发人员今天心情好写了"取消",明天可能随手写了"取消",后天又写了"退出"。翻译人员根本不知道这几个词是不是同一个意思,该不该保持翻译一致。最后做出来的软件,同一个按钮在不同地方出现好几种不同的译法,用户看着也糊涂。
硬编码的表现形式多种多样,有些很明显,一眼就能看出来,有些则藏得很深,需要仔细排查。以下是几种最常见的类型,测试的时候可以重点关注这些位置。

对话框和消息框里的文本是最典型的硬编码重灾区。比如 MessageBox.Show("操作成功")、Alert("您确定要删除吗?") 这类代码,文字直接写在代码里,翻译人员根本拿不到。
菜单和按钮上的标签也很常见。像 menuItem.Text = "文件"、button.Content = "保存" 这样的写法,如果你不把 "文件" 和 "保存" 这几个字抽出来,翻译工作就没法进行。
错误提示信息也是硬编码的高发区。软件运行过程中弹出的各种错误提示,往往都是开发人员随手写的,比如 throw new Exception("用户名不能为空")。这类文本通常都很长,涉及的语境也比较复杂,处理起来更麻烦。
格式化的字符串处理起来特别棘手。比如 string.Format("您好,{0},您有{1}条未读消息", userName, msgCount),这里的字符串包含了占位符,翻译的时候不仅要翻译句子本身,还要考虑占位符的位置会不会因为语言习惯不同而需要调整。
说了这么多硬编码的危害,真正有用的方法到底是什么呢?最根本的解决办法就是把所有的用户可见文本从代码里分离出来,存放到专门的资源文件中。这不是什么新技术,而是软件工程里的老常识,但很多团队因为赶进度或者其他原因,总是忽视这一步。
几乎所有主流开发平台都提供了资源管理机制。Windows平台有 resx 文件,Java 有 properties 文件和 resource bundles,iOS 有 strings 文件和 storyboards,Android 有 strings.xml。这些格式都支持键值对的结构,每一条文本都有一个唯一的标识符,代码里只存这个标识符,翻译的时候根据标识符去对应的语言文件里查找译文。
举个小例子,原来代码里可能是这样的:

label.Text = "请输入您的邮箱地址";
改造成资源外部化之后,代码变成这样:
label.Text = Resources.Labels.EmailPlaceholder;
而 Resources.Labels.EmailPlaceholder 的值则存放在资源文件里,中文版的资源文件里是"请输入您的邮箱地址",英文版里是"Please enter your email address",日文版里是「メールアドレスを入力してください」。翻译人员只需要翻译资源文件,完全不用碰代码。
资源文件虽然好用,但如果管理不规范,照样会乱成一团。康茂峰在服务客户的过程中,经常看到一些团队的资源文件命名毫无章法,同一个功能模块的文本散落在十几个不同的文件里,翻译根本无从下手。
建议的做法是按功能模块来组织资源文件。比如登录相关的文本放在一起,支付相关的放在一起,错误提示放在一起。每个资源键的命名也要有统一的规范,最好能反映出它所在的模块和用途。比如 Login.Button.Submit、Login.Error.WrongPassword、Payment.Confirm.DialogTitle 这样的命名方式,翻译人员看到键名就能大概知道这段文字是干什么用的,有助于保持翻译的一致性和准确性。
很多语言都有复数形式,俄语甚至有六种复数形式。如果你的资源文件不支持复数管理,最后翻译出来的文本在某些数量情况下就会出错。
以英文为例,"1 message" 和 "2 messages" 是两种不同的表达。资源文件里需要能够表达这种区别。ICU(International Components for Unicode)提供了一套完整的复数规则库,大多数现代开发框架都支持按这套规则来定义复数形式。翻译人员在处理这类文本时,需要根据目标语言的复数规则来填写不同的译文,而不是简单地复制粘贴。
技术手段再完善,如果流程上不卡住硬编码,它还是会反复出现。很多团队花大力气重构了一次代码,结果下一个版本开发人员又顺手写了硬编码,问题卷土重来。所以,必须在流程上建立起防护机制。
代码审查(Code Review)是发现硬编码的最后一道防线。在审查清单里加上专门针对本地化的检查项,要求审查人员关注所有用户可见的字符串。只要发现有字符串常量直接写在代码里,就打回去让开发改成资源引用。
这个习惯一开始推行起来可能有点困难,开发人员会觉得多此一举。但只要坚持一段时间,大家形成了习惯,硬编码的出现频率会明显下降。毕竟谁也不想自己提交的代码被反复打回来。
人工审查会有疏漏,尤其是项目赶得紧的时候,审查人员可能就睁一只眼闭一只眼了。这时候需要自动化工具来帮忙。市面上有一些静态代码分析工具可以扫描代码中的硬编码字符串,把它们标记出来供人工复核。
这类工具可以配置忽略列表,把一些确实不需要翻译的内容(比如变量名、代码示例)排除掉。每周生成一次扫描报告,定期清理新增的硬编码,逐步净化代码库。
传统的本地化流程往往是开发做完了,代码冻结了,才把文本交给翻译团队。这时候再发现问题,改动成本已经很高了。更合理的做法是让翻译流程前置,在开发阶段就开始介入。
具体来说,当产品确定要支持某语言版本时,就可以开始整理需要翻译的文本清单。这时候资源文件可能还不完整,但至少可以让翻译团队了解大概有多少内容要翻,涉及哪些模块。开发每完成一个模块,翻译就可以跟进处理一个模块,而不是等到最后集中爆发。
有些文本确实不适合抽到外部资源文件里,比如软件里内置的帮助文档示例代码、技术文档里的变量名和参数名。这类内容如果也翻译了,反而会影响用户理解。怎么处理呢?
最好的办法是在代码里用注释标注哪些字符串是需要翻译的,哪些是不需要翻译的。比如可以用 // TRANSLATE: 开始标记需要翻译的字符串 这样的注释把要翻译的内容括起来。翻译人员在处理的时候只关注标记范围内的内容,既不会漏翻,也不会误翻。
还有一些动态生成的文本,比如从数据库里读出来的产品名称、从服务器返回的实时数据。这类内容本质上不是软件界面的一部分,而是数据的一部分,通常不需要走本地化翻译流程。但产品名称如果有品牌意义,可能需要单独处理,确保在不同语言环境下保持一致的命名风格。
在解决硬编码问题的过程中,有些团队会走向另一个极端,反而带来新的麻烦。以下是几个值得注意的误区。
误区一:把所有字符串都抽出来。并不是所有文本都需要本地化。比如代码调试日志、系统内部错误码、只有开发人员才会看到的提示信息,这些内容用户根本看不到,翻译它们完全是浪费时间。在设计资源文件的时候,要明确哪些是需要翻译的公开文本,哪些是内部使用的,保持清醒的边界意识。
误区二:资源键命名过于随意。有些团队为了省事,用数字编号当键名,比如 String001、String002。这样做翻译人员根本不知道每个字符串是干什么用的,翻译质量没法保证。键名要取得有意义,能够反映文本的用途和上下文。
误区三:忽视语境信息。同一个英文单词在不同语境下可能有完全不同的意思。比如 "bank" 可以是银行,也可以是河岸。如果资源文件里只写了 bank = 银行,翻译人员很可能搞错。最好在资源文件里加上注释,说明每段文本是用在什么场景的,帮助翻译人员做出正确的判断。
康茂峰在服务客户时发现,很多本地化问题其实不是翻译本身的问题,而是前期准备不充分、资源管理不规范造成的。把这些基础工作做扎实了,后面的翻译工作才能顺畅。
硬编码这个问题,说大不大,说小不小。项目小的时候可能忍一忍就过去了,等项目要做多语言版本的时候,它就会跳出来给你添堵。与其在出问题的时候手忙脚乱,不如从现在开始就把资源外部化的工作做起来。
改变习惯需要时间,一开始可能会觉得多了一道手续,有点麻烦。但只要坚持一阵子,你会发现后面的本地化工作轻松很多。代码整洁了,翻译质量上去了,项目进度也更可控了。这些看不见的投入,最后都会变成实实在在的回报。
如果你正在为本地化过程中的硬编码问题头疼,不妨从今天开始,挑选一个模块试点改造。积累一些经验之后,再逐步推广到整个项目。很多事情都是这样,迈出第一步,后面的路就顺了。
