7.3 本地化成本与排期

本地化要花多少钱、要排多久?本节把「按字数翻译 + 按小时审校 + 按键重排」三种计价方式折算成一个可复算的整数模型——每种语言的成本、工期,以及整个项目的总额与关键路径,全部由一份 config 驱动。S 层 Python 仿真把结果落成 JSON、CSV 与一张堆叠条形图,让「多做一个语种要加多少」变成一个能算出来的数。

站内那篇独立游戏多语言本地化指南 讲的是去哪找译员、怎么谈价、有哪些行业惯例,本节不重复这些采购流程,只做一件它不做的事:把「做几个语种」这件事算成一个可复算的数——每种语言要花多少、要排多久,整个项目一共多少、卡在哪条路径上。这是「本地化决策」的量化那一半:第 7.1 节伪本地化与溢出检测 保证排版不炸,第 7.2 节语言 QA 与母语审校 保证内容过关,本节回答钱与时间。

三种计价方式

本地化的钱不是一种算法,而是三种叠在一起:

  • 按字数翻译:译员按源词数报价,不同语种有难度系数(日、阿通常贵于英、葡);
  • 按小时审校:审校按工时计费,每千词要几个工时,多语种可能还要多轮;
  • 按键重排:翻译改动会撑长文案,UI 需要重排,按「改动的键数 × 单价」算。

三者相加才是该语种的变量成本;再叠一笔一次性管线成本(i18n 框架、抽取工具、伪本地化脚本——这部分与语言数无关,做 1 种和做 5 种一样贵)。工期同理:翻译周数由「词数 ÷ 单译员周产能 ÷ 并行译员数」决定,审校、重排、QA 各占固定周数,最后加一段缓冲。

一份 config 说清所有单价

把上面所有参数写进 config/g07_cost.json:顶部是全局单价与产能,locales 里每种语言给词数、键数、难度系数、审校轮数。改一个数,全盘重算:

{
  "version": 1,
  "game": "merge-island",
  "observedAt": "2026-10",
  "note": "本地化成本与排期模型。口径是「公开可观察机制」的建模假设:把「按字数翻译 + 按小时审校 + 按键重排」三种计价方式整理成可复算的整数模型,不是任何供应商的报价实测。translateFenPerWord=每词翻译基价(分);localeFactorPermille=语种难度系数(千分比,基准=1000);reviewFenPerHour=审校人时价(分);reviewHoursPer1000Words=每千词审校工时;layoutFenPerKey=每键重排价(分);layoutKeysChangedPermille=需重排的键占比(千分比);pipelineFenOnce=一次性 i18n 管线工程成本(分,与语言数无关);wordsPerTranslatorWeek=单译员每周产能;translators=并行译员数;prepWeeks/layoutWeeks/qaWeeks=固定阶段周数;bufferPermille=缓冲占比(千分比)。真实报价、真实产能与真实交付周期需真实业务环境,命令行不可复现(X)。全整数,无浮点。",
  "translateFenPerWord": 80,
  "reviewFenPerHour": 15000,
  "reviewHoursPer1000Words": 3,
  "layoutFenPerKey": 500,
  "layoutKeysChangedPermille": 600,
  "pipelineFenOnce": 800000,
  "wordsPerTranslatorWeek": 5000,
  "translators": 2,
  "prepWeeks": 1,
  "layoutWeeks": 1,
  "qaWeeks": 1,
  "bufferPermille": 200,
  "locales": [
    { "id": "en-US", "name": "英语(美国)", "words": 3200, "keys": 420, "localeFactorPermille": 900, "reviewPasses": 1 },
    { "id": "de-DE", "name": "德语(德国)", "words": 3450, "keys": 440, "localeFactorPermille": 1200, "reviewPasses": 2 },
    { "id": "ja-JP", "name": "日语(日本)", "words": 3100, "keys": 440, "localeFactorPermille": 1400, "reviewPasses": 2 },
    { "id": "ar-SA", "name": "阿拉伯语(沙特)", "words": 3100, "keys": 470, "localeFactorPermille": 1500, "reviewPasses": 2 },
    { "id": "pt-BR", "name": "葡萄牙语(巴西)", "words": 3200, "keys": 420, "localeFactorPermille": 950, "reviewPasses": 1 }
  ]
}

S 层:把三种计价折算成成本与工期

下面这段是 S 层(数值仿真层)脚本 sim/g07_cost.py。它读上面这份 config,对每种语言算出三段成本与总工期,再汇总项目总额与关键路径。所有除法都是整数下取整,上取整统一用 (a+b-1)//b——没有浮点,所以结果在任何机器上都一样:

#!/usr/bin/env python3
# file: sim/g07_cost.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/g07_cost.py --config config/ --out out/
"""本地化成本与排期仿真:读 config/g07_cost.json,把「按字数翻译 + 按小时审校 + 按键重排」
三种计价方式折算成每种语言的成本与工期,并给出项目总额与关键路径。

    translate_fen = words * translateFenPerWord * localeFactorPermille // 1000
    review_fen    = ceil(words * reviewHoursPer1000Words / 1000) * reviewFenPerHour * reviewPasses
    layout_fen    = keys * layoutKeysChangedPermille // 1000 * layoutFenPerKey
    locale_total  = translate_fen + review_fen + layout_fen
    base_weeks    = prepWeeks + ceil(words / (wordsPerTranslatorWeek*translators)) + reviewPasses
                    + layoutWeeks + qaWeeks
    buffer_weeks  = ceil(base_weeks * bufferPermille / 1000);total_weeks = base_weeks + buffer_weeks
    project_total = pipelineFenOnce + Σ locale_total;critical_path = max(total_weeks)

口径:单价、产能与工期是「公开可观察机制」的建模假设,不是任何供应商的报价实测。真实
报价与真实交付周期需真实业务环境,命令行不可复现(X)。产出 out/g07_cost.json + .csv + .svg。
全整数、上取整用 (a+b-1)//b,无浮点,无随机。
"""
from __future__ import annotations

import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET


def ceil_div(a: int, b: int) -> int:
    return (a + b - 1) // b


def cost_of(cfg: dict, loc: dict) -> dict:
    words, keys = loc["words"], loc["keys"]
    translate = words * cfg["translateFenPerWord"] * loc["localeFactorPermille"] // 1000
    hours = ceil_div(words * cfg["reviewHoursPer1000Words"], 1000)
    review = hours * cfg["reviewFenPerHour"] * loc["reviewPasses"]
    layout = keys * cfg["layoutKeysChangedPermille"] // 1000 * cfg["layoutFenPerKey"]
    total = translate + review + layout
    trans_weeks = ceil_div(words, cfg["wordsPerTranslatorWeek"] * cfg["translators"])
    base = cfg["prepWeeks"] + trans_weeks + loc["reviewPasses"] + cfg["layoutWeeks"] + cfg["qaWeeks"]
    buffer = ceil_div(base * cfg["bufferPermille"], 1000)
    return {"id": loc["id"], "name": loc["name"], "words": words, "keys": keys,
            "translateFen": translate, "reviewFen": review, "layoutFen": layout,
            "totalFen": total, "fenPerWord": total // words,
            "transWeeks": trans_weeks, "bufferWeeks": buffer, "totalWeeks": base + buffer}


def draw_svg(path: str, rows: list) -> int:
    """每种语言一根堆叠横条:翻译 / 审校 / 重排三段,标注总价与工期。"""
    W, PAD, ROW = 780, 240, 46
    H = 100 + ROW * len(rows)
    svg = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(W),
                             "height": str(H), "viewBox": f"0 0 {W} {H}"})
    ET.SubElement(svg, "rect", {"width": str(W), "height": str(H), "fill": "#ffffff"})
    ET.SubElement(svg, "text", {"x": "16", "y": "30", "font-size": "15",
                                "fill": "#111827"}).text = "各语言本地化成本(翻译 + 审校 + 重排,单位:分)"
    span = W - PAD - 170
    hi = max(r["totalFen"] for r in rows) or 1
    for i, r in enumerate(rows):
        y = 56 + i * ROW
        wt = span * r["translateFen"] // hi
        wr = span * r["reviewFen"] // hi
        wl = span * r["layoutFen"] // hi
        ET.SubElement(svg, "text", {"x": "16", "y": str(y + 18), "font-size": "12",
                                    "fill": "#374151"}).text = r["id"]
        ET.SubElement(svg, "rect", {"x": str(PAD), "y": str(y + 4), "width": str(wt),
                                    "height": "20", "rx": "3", "fill": "#2563eb"})
        ET.SubElement(svg, "rect", {"x": str(PAD + wt), "y": str(y + 4), "width": str(wr),
                                    "height": "20", "rx": "3", "fill": "#f59e0b"})
        ET.SubElement(svg, "rect", {"x": str(PAD + wt + wr), "y": str(y + 4), "width": str(wl),
                                    "height": "20", "rx": "3", "fill": "#10b981"})
        ET.SubElement(svg, "text", {"x": str(PAD + wt + wr + wl + 8), "y": str(y + 18),
                                    "font-size": "11", "fill": "#6b7280"}).text = (
            f'{r["totalFen"]} 分 · {r["totalWeeks"]} 周')
    os.makedirs(os.path.dirname(path), exist_ok=True)
    ET.ElementTree(svg).write(path, encoding="utf-8", xml_declaration=True)
    return len(ET.tostring(svg))


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--config", default="config/")
    ap.add_argument("--out", default="out/")
    a = ap.parse_args()
    with open(os.path.join(a.config, "g07_cost.json"), encoding="utf-8") as f:
        cfg = json.load(f)

    rows = [cost_of(cfg, loc) for loc in cfg["locales"]]
    variable = sum(r["totalFen"] for r in rows)
    project = cfg["pipelineFenOnce"] + variable
    critical = max(r["totalWeeks"] for r in rows)
    digest = ";".join(f'{r["id"]}:{r["totalFen"]}:{r["totalWeeks"]}' for r in rows)

    os.makedirs(a.out, exist_ok=True)
    svg = draw_svg(os.path.join(a.out, "g07_cost.svg"), rows)
    with open(os.path.join(a.out, "g07_cost.csv"), "w", encoding="utf-8", newline="") as f:
        w = csv.writer(f)
        w.writerow(["locale", "words", "keys", "translateFen", "reviewFen", "layoutFen",
                    "totalFen", "fenPerWord", "transWeeks", "bufferWeeks", "totalWeeks"])
        for r in rows:
            w.writerow([r["id"], r["words"], r["keys"], r["translateFen"], r["reviewFen"],
                        r["layoutFen"], r["totalFen"], r["fenPerWord"], r["transWeeks"],
                        r["bufferWeeks"], r["totalWeeks"]])
    rep = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
           "localeCount": len(rows), "pipelineFenOnce": cfg["pipelineFenOnce"],
           "variableFen": variable, "projectFen": project, "criticalPathWeeks": critical,
           "rows": rows, "digest": digest, "csv": os.path.join(a.out, "g07_cost.csv"),
           "svg": os.path.join(a.out, "g07_cost.svg"), "svgBytes": svg}
    with open(os.path.join(a.out, "g07_cost.json"), "w", encoding="utf-8") as f:
        json.dump(rep, f, ensure_ascii=False, indent=2)
    print(json.dumps({"digest": digest, "variableFen": variable, "projectFen": project,
                      "criticalPathWeeks": critical,
                      "rows": [(r["id"], r["totalFen"], r["fenPerWord"], r["totalWeeks"])
                               for r in rows]}, ensure_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

在本机跑 /opt/homebrew/bin/python3 sim/g07_cost.py --config config/ --out out/,5 种语言的成本与工期是(同机相对量级,绝对数值不可移植):

digest = en-US:506400:6;de-DE:793200:8;ja-JP:779200:8;ar-SA:813000:8;pt-BR:519200:6
variableFen = 3411000        # 5 语种变量成本合计(分)
projectFen  = 4211000        # + 一次性管线 800000 分
criticalPathWeeks = 8

逐语言读成本

把每条记录拆开看,钱花在哪里一目了然:

语言总成本(分)每词成本(分)工期(周)
en-US5064001586
pt-BR5192001626
ja-JP7792002518
de-DE7932002298
ar-SA8130002628

两处值得注意。第一,最贵的不一定是最难的:de-DE 总价比 ja-JP 高(词更多、难度系数 1200),但每词成本反而更低(229 < 251)——因为日语难度系数 1400 更高,词少而单价贵。看总价要配着看单价,否则会把「量大」误判成「价高」。第二,工期并不随成本线性走:英、葡是 6 周,其余 3 种都是 8 周——差在审校轮数(1 轮 vs 2 轮),一轮审校就是一周。

拆开看:钱花在哪一段

把每种语言的三段成本拆开(单位:分),才能看清成本结构:

语言翻译审校重排合计
en-US230400150000126000506400
pt-BR243200150000126000519200
ja-JP347200300000132000779200
de-DE331200330000132000793200
ar-SA372000300000141000813000

翻译永远是最大的一段(约占 42%~47%),但审校的占比波动最大:英、葡只审 1 轮,审校约占 29%;德、日、阿审 2 轮,审校占比升到 37%~42%。这说明「审校轮数」是仅次于翻译量的成本杠杆——加一轮审校,往往比多翻几百词还贵。重排那段则相对稳定,因为它是按「键数 × 固定比例」算的,和语种难度无关。

关键路径不是最贵的那条

项目总额 4211000 分里,一次性管线占了 800000 分(约 19%),剩下才是按语种走的变量成本 3411000 分。这个拆分有直接的决策含义:管线是沉没的,做第 1 种语言最贵(要摊整条管线),从第 2 种起边际成本骤降——这正是「要么不做,要么多做几个」的经济学依据。

而 criticalPathWeeks = 8 取的是所有语言里最长的工期,不是最贵的那条。成本上 ar-SA 最高,工期上和 ja-JP、de-DE 一样是 8 周;如果只盯着钱,很容易误以为「砍掉最贵的语种就能提前」,其实砍掉 ar-SA 工期一秒不减。成本看总和,工期看最大值——两个口径不能混。

X 层:真实报价不可复现

最后划清边界。上面算的是模型侧:给定一套单价、产能与工期假设,成本与排期是什么。而下面这些属于 X 层,需真实业务环境,命令行不可复现:

  • 真实的译员 / 审校报价(不同地区、不同语对、不同交期,价格差几倍,没有可复现的单价表);
  • 真实的产能与交付周期(译员档期、返工、节假日都影响实际周数);
  • 真实的谈判结果(批量折扣、长期合作价,都是个案)。

工程上正确的做法是:把可复算的模型层与不可复现的商务层分开——模型层让你在谈判前就知道「这套假设下要花多少、卡在哪」,商务层再拿真实报价替换假设。本节把「做几个语种」算成了一个能重算的数;下一章我们离开工程侧,去看每个语种上线后表现怎么样——度量口径与事件创意。

阅读导航:上一节:7.2 语言 QA 与母语审校 · 下一节:8.1 分语种度量口径 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「minigame」更多文章

  1. 附录 F:全书公式、参数与数据集索引
  2. 9.3 同意管理与数据出境
  3. 9.2 COPPA 与儿童向游戏的限制