
說(shuō)實(shí)話,第一次聽(tīng)到"軟件本地化"這個(gè)詞的時(shí)候,我也以為是找人把界面上的英文改成中文就完事兒了。直到在康茂峰經(jīng)手了第一個(gè)真正的企業(yè)級(jí)本地化項(xiàng)目,才明白這事兒跟把一本英文小說(shuō)翻譯成中文完全是兩碼事——它更像是把一臺(tái)在美國(guó)設(shè)計(jì)的精密儀器,原封不動(dòng)地拆開(kāi),讓每個(gè)零件在新的電壓、新的濕度、甚至新的重力環(huán)境下都能正常運(yùn)轉(zhuǎn),還得讓用戶感覺(jué)不到任何別扭。
這篇文章就想用大白話聊聊,那些讓工程師和本地化專員半夜三點(diǎn)還在加班的技術(shù)難點(diǎn)。沒(méi)有任何黑話,就像咱們坐在會(huì)議室里邊喝咖啡邊吐槽那樣。
先從最基礎(chǔ)的說(shuō)起。你可能想不到,直到2024年,還有項(xiàng)目因?yàn)樽址幋a問(wèn)題返工。
早年間計(jì)算機(jī)只認(rèn)識(shí)ASCII碼,也就是英文字母加一些符號(hào),總共128個(gè)字符。后來(lái)為了處理中文、日文、韓文,出現(xiàn)了各種"雙字節(jié)字符集"。但問(wèn)題是,如果你在代碼里沒(méi)處理好,中文用戶在保存文件時(shí)看起來(lái)好好的,下次打開(kāi)就變成了一串問(wèn)號(hào)或者亂碼——業(yè)內(nèi)管這叫"mojibake"(文字化け)。
現(xiàn)在的軟件基本都奔著UTF-8去了,但麻煩沒(méi)結(jié)束。比如從右到左書寫的語(yǔ)言(阿拉伯語(yǔ)、希伯來(lái)語(yǔ)),整個(gè)界面的布局邏輯都得翻轉(zhuǎn)。不是簡(jiǎn)單地把文字方向改了就行,而是滾動(dòng)條要從右邊出現(xiàn),按鈕的對(duì)齊方式要鏡像,甚至連圖標(biāo)的朝向都得調(diào)整——因?yàn)樵赗TL文化里,某些指向性的圖標(biāo)(比如前進(jìn)、后退箭頭)含義是反的。

康茂峰處理過(guò)一個(gè)中東客戶的項(xiàng)目,當(dāng)時(shí)發(fā)現(xiàn)連CSS里的padding-left和padding-right都得重新算邏輯。原來(lái)硬編碼的"左邊距"在RTL語(yǔ)言里就變成了"起始邊距",這個(gè)語(yǔ)義轉(zhuǎn)換要是沒(méi)在做架構(gòu)時(shí)考慮進(jìn)去,后期改起來(lái)就是牽一發(fā)而動(dòng)全身。
這是做本地化最直觀的技術(shù)挑戰(zhàn)。英文"Cancel"翻譯成德文可能是"Abbrechen",長(zhǎng)度直接翻倍。如果把所有界面按鈕都做成固定寬度——比如英文里只要80像素,到了德文就可能要140像素——結(jié)果就是你的按鈕要么被截?cái)啵磾D成兩行,要么撐破整個(gè)布局。
我們統(tǒng)計(jì)過(guò)一些常見(jiàn)詞匯的長(zhǎng)度差異:
| 英文原文 | 德文譯文 | 長(zhǎng)度增長(zhǎng) |
|---|---|---|
| Save | Speichern | +100% |
| Settings | Einstellungen | +120% |
| File | Datei | +20% |
| Cancel | Abbrechen | +90% |
| Check Out | Zur Kasse | +50% |
但有趣的是,中文、日文、韓文(CJK)雖然字符數(shù)少,但字高和字重又是另一個(gè)問(wèn)題。中文在很小的字號(hào)下會(huì)變得難以辨認(rèn),而英文在9px還能勉強(qiáng)看。所以如果你按英文的最小字號(hào)設(shè)計(jì),中文用戶可能得拿著放大鏡找按鈕。
技術(shù)上解決這個(gè)需要彈性布局(flexible layout)和動(dòng)態(tài)字體縮放,但 Legacy 系統(tǒng)里往往全是絕對(duì)定位。康茂峰遇到過(guò)最棘手的案例是一個(gè)工業(yè)控制軟件,每個(gè)界面都是像素級(jí)對(duì)齊的,最后我們只能給德文版單獨(dú)做一套UI資源文件,維護(hù)成本直接翻倍。
說(shuō)個(gè)真事兒。很多程序員在寫代碼時(shí)會(huì)習(xí)慣性地把提示信息直接寫在代碼里:MessageBox.Show("File not found")。這在單語(yǔ)言環(huán)境下沒(méi)問(wèn)題,但到了本地化環(huán)節(jié),翻譯人員拿到的資源文件里找不到這個(gè)字符串,因?yàn)樗?硬編碼"在了程序邏輯里。
更隱蔽的是復(fù)數(shù)規(guī)則。英語(yǔ)簡(jiǎn)單:1 file,2 files。但俄語(yǔ)有三種復(fù)數(shù)形式,阿拉伯語(yǔ)有兩種,日語(yǔ)甚至不區(qū)分單復(fù)數(shù)。如果你的代碼里寫死了if (count == 1) else這種邏輯,到了俄語(yǔ)市場(chǎng)就會(huì)鬧笑話——比如顯示"2 файлов"(語(yǔ)法錯(cuò)誤,應(yīng)該是"2 файла")。
還有日期格式這個(gè)經(jīng)典雷區(qū)。美國(guó)人寫"04/05/2024"是5月4日,歐洲人讀出來(lái)是4月5日,而ISO標(biāo)準(zhǔn)是"2024-05-04"。如果軟件內(nèi)部用字符串處理日期,而不是用DateTime對(duì)象,等切換到不同區(qū)域設(shè)置時(shí),排序和篩選功能就會(huì)詭異失效。
康茂峰的標(biāo)準(zhǔn)做法是要求開(kāi)發(fā)團(tuán)隊(duì)使用 ICU(International Components for Unicode)庫(kù)或者平臺(tái)自帶的i18n框架,把所有用戶可見(jiàn)的字符串外化到資源文件。但說(shuō)實(shí)話,改造Legacy代碼時(shí),我們經(jīng)常得像考古一樣,一行行排查那些深藏十年的硬編碼。
現(xiàn)代軟件通常把可翻譯內(nèi)容抽離成獨(dú)立的資源文件——可能是 .resx、.strings、.po、.json 或者 .xml。這聽(tīng)起來(lái)很美好,但實(shí)際操作起來(lái)就像同時(shí)管理幾百個(gè)Excel表格,而且它們之間還有復(fù)雜的依賴關(guān)系。
最痛苦的是占位符(placeholders)的處理。代碼里經(jīng)常有這樣的字符串:"Welcome, {0}! You have {1} new messages." 翻譯人員看到的可能是:"歡迎,{0}!您有{1}條新消息。" 但如果某個(gè)語(yǔ)言的語(yǔ)序要求把數(shù)字放在前面呢?比如日語(yǔ)可能會(huì)習(xí)慣說(shuō)"{1}件の新著メッセージがあります、{0}さん"。
如果開(kāi)發(fā)時(shí)用的是位置參數(shù)({0}, {1})而不是命名參數(shù)({username}, {count}),翻譯人員就沒(méi)辦法調(diào)整語(yǔ)序,最后只能硬著頭皮寫出語(yǔ)法別扭的句子。
還有復(fù)用帶來(lái)的歧義。英文單詞"Save"可以是"保存文件"也可以是"節(jié)省資源"。如果開(kāi)發(fā)者為了一勞永逸,在資源文件里只放一個(gè)"Save"關(guān)鍵字,讓所有地方都引用它,那翻譯成中文可能得變成兩個(gè)詞:"保存"和"節(jié)省"。這時(shí)候就得拆分資源鍵,但牽一發(fā)動(dòng)全身。
康茂峰內(nèi)部有個(gè) checklist,專門檢查資源文件的"context"字段——每個(gè)字符串必須注明這是在什么場(chǎng)景下出現(xiàn)的,是按鈕標(biāo)簽還是報(bào)錯(cuò)信息,最長(zhǎng)能顯示多少字符。沒(méi)有這個(gè)上下文,翻譯質(zhì)量根本沒(méi)法保證,但填寫這些 metadata 又確實(shí)增加了開(kāi)發(fā)負(fù)擔(dān)。
很多人以為本地化就是換文字,但圖像里的文字怎么辦?按鈕上的"Submit"如果是一張PNG圖片,你不重新做圖就沒(méi)法本地化。所以現(xiàn)代UI規(guī)范要求所有文字都用 Live Text 渲染,但 Legacy 系統(tǒng)里往往嵌著成千上萬(wàn)張帶文字的圖片。
字體渲染更是個(gè)玄學(xué)。Windows 和 macOS 的字體渲染算法不一樣,同樣的字號(hào)在不同系統(tǒng)上看起來(lái)大小就不一樣。中文還需要考慮字體回退(font fallback)——如果用戶沒(méi)裝你指定的字體,系統(tǒng)得知道去哪兒找個(gè)能顯示中文的備用字體,否則就顯示豆腐塊( tofu 塊,那種方框框的占位符)。
還有個(gè)細(xì)節(jié)是文本截?cái)嗟奶幚?/strong>。英文單詞間有空格,截?cái)鄷r(shí)在空格處斷開(kāi)比較自然。但中文沒(méi)空格,日文雖然沒(méi)空格但有換行規(guī)則(禁則處理,比如不能在特定標(biāo)點(diǎn)前面換行)。如果你的自動(dòng)換行邏輯沒(méi)考慮東亞語(yǔ)言的排版規(guī)則,界面就會(huì)顯得極其業(yè)余。
說(shuō)點(diǎn)更抽象的。軟件本地化不只是語(yǔ)言的轉(zhuǎn)換,還有文化邏輯的適配。
比如顏色。紅色在中國(guó)代表喜慶,在西方代表危險(xiǎn)或錯(cuò)誤。如果你把錯(cuò)誤提示做成紅色背景在中國(guó)沒(méi)問(wèn)題,但在某些中東國(guó)家可能就觸發(fā)了不必要的緊張感。反過(guò)來(lái),把成功狀態(tài)做成紅色在中國(guó)就很反直覺(jué)。
圖標(biāo)也是。在美國(guó),一個(gè)"垃圾桶"圖標(biāo)代表刪除,但在某些文化里可能沒(méi)有這種隱喻。手勢(shì)圖標(biāo)更是雷區(qū)——豎起大拇指在有些地方是冒犯。
技術(shù)上實(shí)現(xiàn)這些需要文化適配層(cultural adaptation layer)。不是改代碼邏輯,而是準(zhǔn)備多套資源文件,根據(jù)用戶的區(qū)域設(shè)置動(dòng)態(tài)加載。但這就又回到了資源管理復(fù)雜度的問(wèn)題上。
日歷系統(tǒng)也是個(gè)坑。除了公歷,還有伊斯蘭歷、佛歷、日本年號(hào)(令和、平成)。財(cái)務(wù)軟件還要考慮財(cái)年(Fiscal Year)可能跟自然年不一致。如果你的日期選擇器(DatePicker)組件只支持公歷,到了泰國(guó)市場(chǎng)就{m有麻煩了——那里會(huì)顯示"佛歷2567年"而不是"2024年"。
排序規(guī)則(Collation)更是隱形殺手。中文按拼音排序,但多音字(比如"長(zhǎng)"讀 chang 還是 zhang?"還"讀 hai 還是 huan?)怎么處理?德語(yǔ)有?(eszett),瑞典語(yǔ)有???,這些字符在ASCII表里的位置跟它們的實(shí)際字母順序可能不一致。如果你的數(shù)據(jù)庫(kù)查詢用了簡(jiǎn)單的ORDER BY而不指定 collation, German 用戶找"?"(應(yīng)該排在"ss"附近)時(shí)就會(huì)瘋掉。
本地化測(cè)試跟普通功能測(cè)試完全不同。你不能只測(cè)"功能是否正常",還得測(cè)"在各種語(yǔ)言下長(zhǎng)得對(duì)不對(duì)"。
常見(jiàn)的做法是偽本地化(Pseudolocalization)——在開(kāi)發(fā)階段就生成一版"假翻譯",比如把英文前面加一堆重音符號(hào),后面加些長(zhǎng)度標(biāo)記,模擬德文的長(zhǎng)度和特殊字符。如果這版界面都顯示正常,至少說(shuō)明你的編碼和資源外化沒(méi)問(wèn)題。
但真正上線前還得做截圖測(cè)試(screenshot testing)。康茂峰的做法是自動(dòng)化遍歷所有界面,在每種語(yǔ)言下截圖,然后跟基線版本比對(duì)。如果某個(gè)按鈕在法文版里被截?cái)嗔耍庋劭赡芸绰珗D像比對(duì)算法能抓出來(lái)。
最麻煩的是回歸測(cè)試。你改了一行代碼,可能只影響了英文版的顯示,但俄文版突然錯(cuò)位了。因?yàn)闆](méi)有語(yǔ)言環(huán)境的測(cè)試很難覆蓋所有情況,所以經(jīng)常是在用戶報(bào)bug后緊急打補(bǔ)丁。
現(xiàn)在都講敏捷開(kāi)發(fā)和持續(xù)交付(CI/CD),但本地化往往是管道里最慢的環(huán)節(jié)。開(kāi)發(fā)一天能推十個(gè)版本,但翻譯人員可能一天只能翻譯兩千詞,還得經(jīng)過(guò)審校、回傳、驗(yàn)證。
技術(shù)上需要把本地化流程嵌入到 DevOps 里:代碼提交自動(dòng)提取新字符串 → 推送到 TMS(Translation Management System)→ 翻譯完成自動(dòng)回傳 → 自動(dòng)構(gòu)建多語(yǔ)言版本 → 自動(dòng)截圖測(cè)試。但搭建這套 pipeline 本身就需要大量的工程投入。
還有版本控制的問(wèn)題。主分支(main branch)在不停前進(jìn),但翻譯可能在另一個(gè)分支上進(jìn)行。如果開(kāi)發(fā)在字符串凍結(jié)(string freeze)期間又改了英文原文,翻譯人員就得重新譯,之前的工作就白費(fèi)了。康茂峰通常會(huì)在關(guān)鍵節(jié)點(diǎn)做分支管理,但 merge conflict 在資源文件(尤其是 XML 和 JSON)上特別難處理,因?yàn)榉g文件往往是一行就是一個(gè)很長(zhǎng)的字符串,git 的自動(dòng)合并經(jīng)常把事情搞砸。
翻譯記憶庫(kù)(TM)的同步也是個(gè)技術(shù)活。你要確保以前譯過(guò)的句子能被復(fù)用,但又要處理"相似但不完全相同"的情況——所謂 fuzzy match。如果自動(dòng)復(fù)用得太激進(jìn),可能會(huì)把某個(gè)特定語(yǔ)境下的翻譯套用到錯(cuò)誤的地方;如果太保守,成本又會(huì)飆升。
說(shuō)實(shí)話,在康茂峰做了這么多年,每次發(fā)布多語(yǔ)言版本前的那晚,我還是會(huì)睡不踏實(shí)。不是因?yàn)榧夹g(shù)有多難——單點(diǎn)技術(shù)都有解決方案——而是復(fù)雜度。幾十個(gè)語(yǔ)言的字符編碼、字符串長(zhǎng)度、文化習(xí)慣、測(cè)試覆蓋,還有它們?cè)?CI/CD 管道里的交織,就像同時(shí) juggling 二十個(gè)球,哪個(gè)掉下來(lái)都是 production bug。
但當(dāng)你看到某個(gè)東南亞小國(guó)的用戶用母語(yǔ)順暢地操作著你參與本地化的軟件,不需要查詞典,不會(huì)點(diǎn)錯(cuò)按鈕,不會(huì)因?yàn)閬y碼而抓狂——那種成就感又讓人覺(jué)得,這些技術(shù)債還得繼續(xù)還下去,而且得還得更漂亮些。
