照例先划边界。站内 Steam 发布专题 (166 篇)是 PC 买断制发行口径,与 H5 小游戏无关;卷一「小游戏市场全景」 讲的是国内平台的分发机制与设计约束(包体、首包、能力矩阵),不涉及海外本地化。本节都不重做,只做一件事:拆开同品类竞品的本地化深度,找出「竞品没做、但玩家在等」的语言缺口。卷一 2.2 竞品拆解方法 已经给过一套与本地化无关的竞品坐标系(核心循环 / 变现 / 素材三维打分);本节把维度换成本地化——界面文案、配音、文化美术、商店文案、支付本地化,看的是「竞品把本地化做到什么程度」。
竞品拆解不是抄,是找「没被满足的语言」
上一节把平台读成了排序机制,这一节把镜头拉到竞品。竞品拆解有两种视角:一种是通用视角(玩法、变现、素材做得怎么样),卷一 2.2 已经用三维打分讲透;另一种是本地化视角——同一款竞品,在英语市场做得很好,但它做了泰语吗?它的配音是英语一条到底,还是每语言都配?它的支付接没接本地钱包?机会往往就藏在「需求大、但竞品覆盖少」的交叉处:一个语言若玩家需求高、六个竞品里却只有一两个做了,那就是一块没被占住的空地。
这套视角和「做不做小语种」的取舍不同——那是一个投入产出决策,见「小语种取舍」 。本节先回答更靠前的问题:竞品到底把本地化做到了什么程度,缺口在哪里。
本地化拆解矩阵(D 层)
下面这段是 D 层配置:五个本地化维度的权重(五项之和恒 1000)、八个语言的需求占比(八项之和恒 1000),以及六个竞品的语言支持与各维度深度分。数值是按同品类公开可观察的产品形态整理的建模输入,标为公开可观察(观察时点 2026-10),不是任何竞品后台的真实数据。
{
"version": 1,
"game": "merge-island",
"observedAt": "2026-10",
"note": "竞品本地化拆解与本地化机会配置。口径是「公开可观察机制」的建模假设(观察时点 2026-10,非实测):把同品类竞品的本地化深度与各语言的市场供需整理成可机读配置,不是任何竞品后台的真实数据。dimensions 五个本地化维度:uiText=界面文案、voice=配音、culturalArt=文化美术、storeListing=商店文案、paymentLocal=支付本地化。dimensionWeights 五项之和恒 1000(千分比)。languages.demandPermille=该语言在目标受众中的需求占比(千分比,八项之和恒 1000)。competitors[].langs=已支持的语言;competitors[].scores=各维度本地化深度(0–1000)。localizationScore(comp)=floor(Σ scores×weight/1000);coveragePermille(lang)=支持该语言的竞品数×1000/竞品总数;opportunity(lang)=demand×(1000−coverage)/1000。真实竞品语言支持与真实需求分布需真实业务环境,命令行不可复现(X)。全整数,无浮点。",
"dimensions": ["uiText", "voice", "culturalArt", "storeListing", "paymentLocal"],
"dimensionWeights": { "uiText": 300, "voice": 100, "culturalArt": 250, "storeListing": 200, "paymentLocal": 150 },
"languages": [
{ "id": "en", "name": "英语", "demandPermille": 300 },
{ "id": "ja", "name": "日语", "demandPermille": 140 },
{ "id": "pt", "name": "葡萄牙语(巴西)", "demandPermille": 120 },
{ "id": "es", "name": "西班牙语(拉美)", "demandPermille": 100 },
{ "id": "ko", "name": "韩语", "demandPermille": 90 },
{ "id": "de", "name": "德语", "demandPermille": 90 },
{ "id": "fr", "name": "法语", "demandPermille": 80 },
{ "id": "th", "name": "泰语", "demandPermille": 80 }
],
"competitors": [
{ "id": "merge-world", "name": "合成世界", "langs": ["en", "ja", "ko", "de", "fr"], "scores": { "uiText": 900, "voice": 500, "culturalArt": 700, "storeListing": 850, "paymentLocal": 800 } },
{ "id": "idle-tycoon", "name": "放置大亨", "langs": ["en", "ja", "ko", "es", "pt"], "scores": { "uiText": 850, "voice": 300, "culturalArt": 600, "storeListing": 800, "paymentLocal": 700 } },
{ "id": "block-puzzle", "name": "方块谜题", "langs": ["en", "de", "fr", "es"], "scores": { "uiText": 800, "voice": 200, "culturalArt": 550, "storeListing": 750, "paymentLocal": 650 } },
{ "id": "bubble-blast", "name": "泡泡爆破", "langs": ["en", "pt", "es"], "scores": { "uiText": 700, "voice": 100, "culturalArt": 400, "storeListing": 600, "paymentLocal": 500 } },
{ "id": "dragon-merge", "name": "龙岛合成", "langs": ["en", "ja", "ko", "th", "pt"], "scores": { "uiText": 880, "voice": 600, "culturalArt": 750, "storeListing": 820, "paymentLocal": 780 } },
{ "id": "farm-story", "name": "农场物语", "langs": ["en", "de", "fr", "ja"], "scores": { "uiText": 820, "voice": 400, "culturalArt": 620, "storeListing": 700, "paymentLocal": 600 } }
]
}
读这张表要抓住两点。第一,语言支持差异很大:英语六个竞品全支持,日语四个,德语 / 法语 / 韩语 / 西语 / 葡语各三个,泰语只有一个——小语种在竞品侧基本是空白。第二,各维度深度不齐:六个竞品的界面文案都在 700 分以上,但配音最高只有 600——配音是普遍的短板,因为它的成本最高、收益最难量化。这两点合起来就是本节要找的机会。
S 层:拆竞品 + 算本地化机会
下面这段是 S 层仿真脚本 sim/g14_competitor_localization.py。它读配置,先算每个竞品的本地化加权分与最弱维度,再算每个语言的覆盖率与「需求 × 缺口」机会分,产出 JSON + CSV + SVG:
#!/usr/bin/env python3
# file: sim/g14_competitor_localization.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/g14_competitor_localization.py --config config/ --out out/
"""竞品本地化拆解与本地化机会仿真(S 层)。
读 config/g14_competitor_localization.json,做两件事:
1) 竞品本地化拆解:每个竞品按五个维度的加权分算 localizationScore,并指出其最弱维度
(soft spot,最低加权贡献)——这是「竞品在本地方案上最薄的一块」。
2) 本地化机会:每个语言的覆盖率 coveragePermille = 支持它的竞品数×1000/竞品总数;
opportunity = demand×(1000−coverage)/1000,即「需求大、但竞品都没做」的语言排前面。
再给一个对照口径(demandRank 纯需求排序),展示「缺口口径」与「需求口径」的分歧:
小语种在缺口口径下排前、在需求口径下排后,这正是本地化取舍的两端。
口径:竞品语言支持与需求分布是「公开可观察机制」口径的建模假设(观察时点 2026-10),
非实测;真实数据需真实业务环境,命令行不可复现(X)。产出 out/g14_competitor_localization.json + .csv + .svg。
全整数、地板除,无浮点。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET
WEIGHT_SUM = 1000
def loc_score(comp: dict, weights: dict) -> int:
return sum(comp["scores"][d] * weights[d] for d in weights) // WEIGHT_SUM
def weakest_dim(comp: dict, weights: dict) -> str:
"""最弱维度 = 原始深度分最低者;并列取 dimensions 顺序首个。"""
return min(weights, key=lambda d: comp["scores"][d])
def draw_svg(path: str, rows: list[dict]) -> int:
"""纯 Python SVG:每个语言的本地化机会分横条。"""
W, ROW, PAD, BAR = 780, 34, 200, 430
H = 66 + 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": "28", "font-size": "15",
"fill": "#111827"}).text = "各语言的本地化机会分(需求 × 竞品缺口)"
hi = max([r["opportunity"] for r in rows] + [1])
for i, r in enumerate(rows):
y = 50 + i * ROW
ET.SubElement(svg, "text", {"x": "16", "y": str(y + 16), "font-size": "12",
"fill": "#374151"}).text = f'{r["name"]}(覆盖 {r["coveragePermille"]}‰)'
ET.SubElement(svg, "rect", {"x": str(PAD), "y": str(y + 3), "height": "16", "rx": "3",
"width": str(BAR * r["opportunity"] // hi), "fill": "#0d9488"})
ET.SubElement(svg, "text", {"x": str(PAD + BAR + 8), "y": str(y + 16), "font-size": "11",
"fill": "#6b7280"}).text = f'{r["opportunity"]}'
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, "g14_competitor_localization.json"), encoding="utf-8") as f:
cfg = json.load(f)
dims = cfg["dimensions"]
weights = cfg["dimensionWeights"]
comps = cfg["competitors"]
n = len(comps)
# ---- 1) 竞品本地化拆解 ----
comp_rows = []
for c in comps:
wd = weakest_dim(c, weights)
comp_rows.append({"id": c["id"], "name": c["name"], "langCount": len(c["langs"]),
"locScore": loc_score(c, weights), "weakestDim": wd,
"weakestScore": c["scores"][wd]})
comp_rows.sort(key=lambda r: (-r["locScore"], r["id"]))
# ---- 2) 本地化机会 ----
lang_rows = []
for lang in cfg["languages"]:
covered = sum(1 for c in comps if lang["id"] in c["langs"])
coverage = covered * WEIGHT_SUM // n
gap = WEIGHT_SUM - coverage
opportunity = lang["demandPermille"] * gap // WEIGHT_SUM
lang_rows.append({"id": lang["id"], "name": lang["name"], "demandPermille": lang["demandPermille"],
"covered": covered, "coveragePermille": coverage, "gapPermille": gap,
"opportunity": opportunity})
opp_order = sorted(lang_rows, key=lambda r: (-r["opportunity"], r["id"]))
demand_order = sorted(lang_rows, key=lambda r: (-r["demandPermille"], r["id"]))
comp_digest = ",".join(f'{r["id"]}:{r["locScore"]}' for r in comp_rows)
opp_digest = ",".join(f'{r["id"]}:{r["opportunity"]}' for r in opp_order)
digest = f"loc:{comp_digest}|opp:{opp_digest}"
os.makedirs(a.out, exist_ok=True)
svg = draw_svg(os.path.join(a.out, "g14_competitor_localization.svg"), opp_order)
with open(os.path.join(a.out, "g14_competitor_localization.csv"), "w", encoding="utf-8",
newline="") as f:
w = csv.writer(f)
w.writerow(["lang", "name", "demandPermille", "covered", "coveragePermille",
"gapPermille", "opportunity", "oppRank", "demandRank"])
for i, r in enumerate(opp_order):
dr = next(j for j, x in enumerate(demand_order) if x["id"] == r["id"]) + 1
w.writerow([r["id"], r["name"], r["demandPermille"], r["covered"], r["coveragePermille"],
r["gapPermille"], r["opportunity"], i + 1, dr])
rep = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
"dimensions": dims, "dimensionWeights": weights,
"competitors": comp_rows, "languages": lang_rows,
"opportunityOrder": [r["id"] for r in opp_order],
"demandOrder": [r["id"] for r in demand_order],
"digest": digest, "svg": os.path.join(a.out, "g14_competitor_localization.svg"),
"svgBytes": svg}
with open(os.path.join(a.out, "g14_competitor_localization.json"), "w", encoding="utf-8") as f:
json.dump(rep, f, ensure_ascii=False, indent=2)
print(json.dumps({"digest": digest, "competitors": comp_rows,
"opportunityOrder": [r["id"] for r in opp_order],
"demandOrder": [r["id"] for r in demand_order],
"languages": [{"id": r["id"], "coveragePermille": r["coveragePermille"],
"opportunity": r["opportunity"]} for r in opp_order]},
ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
在本机跑 /opt/homebrew/bin/python3 sim/g14_competitor_localization.py --config config/ --out out/,六个竞品 + 八个语言的结果是(同机相对量级,绝对数值不可移植):
digest = loc:dragon-merge:792,merge-world:785,idle-tycoon:700,farm-story:671,block-puzzle:645,bubble-blast:515|opp:th:66,pt:60,es:50,ja:46,de:45,ko:45,fr:40,en:0
逐竞品看本地化拆解(最弱维度 是原始深度分最低的一项):
| 竞品 | 支持语言 | 本地化分 | 最弱维度 | 读法 |
|---|---|---|---|---|
| 龙岛合成 | 5 | 792 | 配音(600) | 综合最强,但配音仍是短板 |
| 合成世界 | 5 | 785 | 配音(500) | 商店文案强(850),配音拖后腿 |
| 放置大亨 | 5 | 700 | 配音(300) | 文化美术中等(600) |
| 农场物语 | 4 | 671 | 配音(400) | 语言少一种 |
| 方块谜题 | 4 | 645 | 配音(200) | 配音几乎没做 |
| 泡泡爆破 | 3 | 515 | 配音(100) | 各维度全面偏弱 |
这张表最该记住的是最弱维度一列全是「配音」——六个竞品无一例外。这不是巧合:配音的成本是界面文案的几十倍、收益却最难量化,所以几乎所有竞品都只做「文案本地化」不做「配音本地化」。对一款轻度小游戏来说,这恰恰是一个可选的差异化点(当然要先算清成本,见「本地化成本与排期」 )。
再看语言侧的机会榜(覆盖率 = 支持该语言的竞品占比):
| 语言 | 需求‰ | 覆盖竞品数 | 覆盖率‰ | 缺口‰ | 机会分 |
|---|---|---|---|---|---|
| 泰语 | 80 | 1 | 166 | 834 | 66 |
| 葡语(巴西) | 120 | 3 | 500 | 500 | 60 |
| 西语(拉美) | 100 | 3 | 500 | 500 | 50 |
| 日语 | 140 | 4 | 666 | 334 | 46 |
| 德语 | 90 | 3 | 500 | 500 | 45 |
| 韩语 | 90 | 3 | 500 | 500 | 45 |
| 法语 | 80 | 3 | 500 | 500 | 40 |
| 英语 | 300 | 6 | 1000 | 0 | 0 |
缺口口径 vs 需求口径:小语种的两难
这张机会榜有一个刺眼的结论:泰语排第一(机会分 66),英语排最后(0)。但换个口径看,结论完全反过来——按纯需求排序,英语(300)第一、日语(140)第二,泰语(80)垫底。同一个语言,在两个口径下一个排第一、一个排最后,这就是本地化取舍的两难:
- 缺口口径(
opportunity = 需求 × 缺口)告诉你「哪里没人做」——泰语缺口 834‰、几乎无人覆盖,所以机会分最高; - 需求口径(
demandPermille)告诉你「哪里盘子大」——英语需求 300‰,是泰语的近四倍,但六个竞品全做了、缺口为 0。
工程上的做法是两个口径一起看,而不是二选一:opportunity 高 + demand 不低的语言(如葡语 120‰、西语 100‰)是性价比最高的落点——既有足够的盘子,又有明显的缺口;而泰语这种「缺口极大但盘子很小」的语言,要么配合平台侧的小语种扶持一起做,要么先放进观察队列。这正是「小语种取舍」
要回答的投入产出问题——本节只负责把「缺口在哪」算清楚。
这也接上了上一节的结论:平台按什么排序决定谁上榜(14.1),竞品在哪个语言留了缺口决定你从哪进(14.2)。两者合起来回答「去哪打」。
X 层:真实竞品语言支持与需求不可复现
必须把边界划清。上面算的是拆解模型:给定一份竞品语言表与需求分布,机会分是多少。而下面这些属于 X 层,需真实业务环境,命令行不可复现:
- 各竞品的真实语言支持与本地化深度(产品会随版本更新扩语言,本节只作「公开可观察机制」引用并标注观察时点 2026-10);
- 各语言的真实玩家需求分布(同一款游戏在不同地区、不同时期的语言需求差异极大,无法用一组常数刻画);
- 真实的竞品本地化投入(配音、文化美术的实际成本与团队规模,竞品不公开)。
工程上正确的做法是:把可复现的拆解模型与不可复现的真实数据分开——模型回答「如果竞品覆盖是这样,机会在哪」,真实数据用来在发行后反标定配置(把实测的语言占比与竞品覆盖填回 config,机会榜立刻反映现实)。
自检表
| 检查项 | 合格标准 |
|---|---|
| 拆解有维度 | 五个本地化维度齐备,权重和恒 1000,非单一「语言数」 |
| 缺口可算 | 覆盖率与缺口都用整数千分比,可复算 |
| 双口径对照 | 缺口口径与需求口径并列,不把两难藏起来 |
| 最弱维度可见 | 每个竞品给出最弱维度,暴露共性短板 |
| 与取舍分工 | 只算「缺口在哪」,投入产出交 8.3,不越界 |
| 边界清晰 | 真实语言支持与需求标 X,不写成「实测」 |
竞品拆解最危险的错觉,是把「竞品语言多」当成「没机会了」。真正决定你从哪进的,是竞品没覆盖、但玩家需求不低的那块交叉空地——缺口大而盘子也够大的语言,才是性价比最高的落点。下一节把视角从「竞品」抬到「品类」:同一个品类,在北美、日本、韩国、东南亚的表现差异有多大,以及这个差异到底稳不稳。
阅读导航:上一节:14.1 各平台榜单结构 · 下一节:14.3 品类在海外市场的差异 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。