站内那篇游戏本地化与多语言指南 已经把「本地化行业、语言分工、法务红线」讲透了,本节绝不重写它。它的视角是项目与商务:找谁翻、翻多少种、预算怎么排。本节的视角窄得多、也硬得多:把文化禁忌变成一份可以被程序逐条判定的规则表,让「这张图能不能发到德国」这种问题不再靠人脑记忆。前两章我们打通了多语言资源管线,但翻译正确不等于能发——同一张图、同一个数字,在 A 国是可爱,在 B 国是冒犯。
为什么「翻译完了」不等于「能发」
本地化最容易被低估的一环,是非文字的部分。一句德语翻译得再准,只要图标里画了一只小猪,整包在阿语区就可能被拒。禁忌往往藏在三类地方:美术素材(符号、手势、颜色、动物)、数字与排列(谐音、数字偏好)、文案措辞(宗教、政治、赌博暗示)。它们有一个共同点:都能被穷举成「标签 → 地区 → 处置」的三元组,因此都能被机读、被校验。
本节要做的就是这件事:给每个素材打上语义标签(gesture:thumbs_up、animal:pig、color:gold……),给每个地区配一张禁忌规则表,然后让程序把两者叉乘成一张命中矩阵,一眼看清哪些素材在哪些地区必须换、哪些只是建议换、哪些可以直接发。
禁忌分六类,处置分三档
禁忌按触发维度分成六类,每一类都能落到一个标签前缀上:
| 类别 | 标签示例 | 典型场景 |
|---|---|---|
| 符号 symbol | symbol:swastika | 政治 / 宗教符号,多数市场零容忍 |
| 手势 gesture | gesture:thumbs_up | 同一手势跨区含义相反 |
| 颜色 color | color:white_flower | 颜色与颜色搭配各有禁忌 |
| 数字 number | number:4 | 谐音忌讳(东亚 4) |
| 动物 animal | animal:pig animal:cow | 宗教 / 文化禁忌动物 |
| 措辞 word | word:gambling | 平台条款红线,与地区无关 |
处置分三档,这比「能 / 不能」的二元判断更有用:severity=1 阻断(该区不可发,必须先换素材,如卐形纹样之于德国、猪之于阿语区)、severity=2 警告(可发但建议换,需人工复核,如 OK 手势之于巴西、4 之于韩国)、severity=3 提示(建议而非必须,如 4 之于日本、金色之于中东——后者其实是正向偏好)。
规则表:region × kind × token × severity
规则表是纯数据。region="*" 表示全平台红线(对所有地区生效),其余按地区 id 生效:
{
"version": 1,
"game": "merge-island",
"observedAt": "2026-10",
"note": "文化禁忌与符号审查规则表。口径是「公开可观察机制」的建模假设:把各地区公开可查的符号 / 手势 / 颜色 / 数字 / 动物 / 措辞禁忌整理成可机读的规则,不是任何监管机构的逐条实测结论,也不构成法律意见。severity=1 阻断(该区不可发)、2 警告(需人工复核或换图)、3 提示(建议但非必须)。token 是美术 / 文案素材自带的语义标签;region='*' 表示全平台红线。真实审核结论与地区法规需真实业务环境,命令行不可复现(X)。全整数,无浮点。",
"regions": [
{ "id": "cn", "name": "中国大陆" },
{ "id": "jp", "name": "日本" },
{ "id": "kr", "name": "韩国" },
{ "id": "de", "name": "德国" },
{ "id": "mena", "name": "中东(阿语区)" },
{ "id": "in", "name": "印度" },
{ "id": "br", "name": "巴西" },
{ "id": "us", "name": "北美" }
],
"rules": [
{ "id": "any-gambling", "region": "*", "kind": "word", "token": "word:gambling", "severity": 1, "note": "赌博暗示是多数平台的硬红线,任何市场都不放" },
{ "id": "de-swastika", "region": "de", "kind": "symbol", "token": "symbol:swastika", "severity": 1, "note": "卐形纹样在德国属违宪符号,装饰回纹也要避" },
{ "id": "mena-thumbs", "region": "mena", "kind": "gesture", "token": "gesture:thumbs_up", "severity": 1, "note": "竖大拇指在部分阿语区等同冒犯手势" },
{ "id": "mena-pig", "region": "mena", "kind": "animal", "token": "animal:pig", "severity": 1, "note": "猪及其意象在伊斯兰文化中禁忌" },
{ "id": "in-cow", "region": "in", "kind": "animal", "token": "animal:cow", "severity": 1, "note": "牛在印度教中神圣,不可作为食物或玩物素材" },
{ "id": "br-ok", "region": "br", "kind": "gesture", "token": "gesture:ok_hand", "severity": 2, "note": "OK 手势在巴西有冒犯含义,建议换图" },
{ "id": "kr-four", "region": "kr", "kind": "number", "token": "number:4", "severity": 2, "note": "韩国对 4 敏感(电梯常无 4 层),关键位避免" },
{ "id": "jp-four", "region": "jp", "kind": "number", "token": "number:4", "severity": 3, "note": "4 谐音「死」,尽量避免但非硬性" },
{ "id": "jp-whiteflower", "region": "jp", "kind": "color", "token": "color:white_flower", "severity": 2, "note": "白花关联丧事,节庆素材要换色" },
{ "id": "cn-greenhat", "region": "cn", "kind": "color", "token": "color:green_hat", "severity": 2, "note": "「绿帽」文化禁忌,角色装饰避开" },
{ "id": "us-gun", "region": "us", "kind": "word", "token": "word:gun", "severity": 2, "note": "枪支意象在部分渠道受限,文案换词" },
{ "id": "mena-gold", "region": "mena", "kind": "color", "token": "color:gold", "severity": 3, "note": "金色在中东受欢迎,属正向提示(保留)" }
],
"assets": [
{ "id": "logo_island", "name": "主图标·小岛", "kind": "logo", "tokens": ["symbol:island", "color:green"] },
{ "id": "icon_thumb", "name": "按钮·点赞", "kind": "icon", "tokens": ["gesture:thumbs_up"] },
{ "id": "icon_piggy", "name": "角色·小猪", "kind": "icon", "tokens": ["animal:pig"] },
{ "id": "tex_knot", "name": "装饰·回纹", "kind": "texture", "tokens": ["symbol:swastika", "color:gold"] },
{ "id": "tex_flower", "name": "装饰·白花", "kind": "texture", "tokens": ["color:white_flower", "symbol:flower"] },
{ "id": "icon_four", "name": "徽章·四连", "kind": "icon", "tokens": ["number:4", "symbol:badge"] },
{ "id": "icon_cow", "name": "角色·奶牛", "kind": "icon", "tokens": ["animal:cow"] },
{ "id": "icon_ok", "name": "按钮·确认", "kind": "icon", "tokens": ["gesture:ok_hand"] },
{ "id": "card_bet", "name": "活动·下注卡", "kind": "text", "tokens": ["word:gambling", "color:gold"] }
]
}
D 层:把审查做成可跑的清单校验器
下面这段是 D 层校验脚本 sim/g06_taboo_scan.py。它读规则表,把「素材语义标签」与「地区规则」叉乘成命中矩阵,统计三档命中数、被阻断的 (素材, 地区) 对、以及每个素材可安全发行的地区,产出 JSON + CSV。
#!/usr/bin/env python3
# file: sim/g06_taboo_scan.py
# tier: D
# verify: /opt/homebrew/bin/python3 sim/g06_taboo_scan.py --config config/ --out out/
"""文化禁忌与符号审查清单校验(D 层:清单结构 / lint 校验)。
读 config/g06_taboo.json,把「素材自带的语义标签」与「各地区禁忌规则」做笛卡尔匹配,
得到一张 asset × region 命中矩阵:
命中集合(asset, region) = { rule | rule.region ∈ {"*", region} 且 rule.token ∈ asset.tokens }
findings = 命中三元组 (asset, region, rule) 的总数
block/warn/info = 按 severity(1/2/3) 分类的命中数
blockedPairs = 至少有一条 severity=1 命中的 (asset, region) 对(该区不可发)
cleanPairs = 零命中的 (asset, region) 对(该区可直接发)
shipRegions(asset) = 该素材可安全发行的地区(无 severity=1 命中)
口径:规则表是「公开可观察机制」的建模假设,不是监管机构的逐条实测结论,也不构成
法律意见。真实审核结论与地区法规需真实业务环境,命令行不可复现(X)。
产出 out/g06_taboo_scan.json + .csv。全整数,无浮点,无随机。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
SEV_NAME = {1: "block", 2: "warn", 3: "info"}
def match_rules(cfg, asset, region):
"""素材在指定地区命中的规则列表(region='*' 的规则对所有地区生效)。"""
toks = set(asset["tokens"])
return [r for r in cfg["rules"] if r["token"] in toks and r["region"] in ("*", region)]
def scan(cfg):
regions = [r["id"] for r in cfg["regions"]]
findings, pairs, asset_rows = [], [], []
for a in cfg["assets"]:
ship = []
for reg in regions:
hits = match_rules(cfg, a, reg)
sev = min([h["severity"] for h in hits], default=0)
pairs.append({"asset": a["id"], "region": reg, "hits": len(hits), "severity": sev,
"blocked": 1 if sev == 1 else 0, "clean": 1 if not hits else 0})
if sev != 1:
ship.append(reg)
for h in hits:
findings.append({"asset": a["id"], "region": reg, "rule": h["id"],
"kind": h["kind"], "severity": h["severity"]})
asset_rows.append({"id": a["id"], "name": a["name"], "kind": a["kind"],
"shipRegions": ship, "shipCount": len(ship),
"blockedCount": len(regions) - len(ship)})
return findings, pairs, asset_rows
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, "g06_taboo.json"), encoding="utf-8") as f:
cfg = json.load(f)
findings, pairs, asset_rows = scan(cfg)
by_sev = {name: sum(1 for f in findings if f["severity"] == v) for v, name in SEV_NAME.items()}
blocked_pairs = sum(1 for p in pairs if p["blocked"] == 1)
clean_pairs = sum(1 for p in pairs if p["clean"] == 1)
digest = (f"findings={len(findings)},block={by_sev['block']},warn={by_sev['warn']},"
f"info={by_sev['info']},blockedPairs={blocked_pairs},cleanPairs={clean_pairs}")
os.makedirs(a.out, exist_ok=True)
with open(os.path.join(a.out, "g06_taboo_scan.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["asset", "region", "rule", "kind", "severity"])
for fd in findings:
w.writerow([fd["asset"], fd["region"], fd["rule"], fd["kind"], fd["severity"]])
rep = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
"regionCount": len(cfg["regions"]), "assetCount": len(cfg["assets"]),
"ruleCount": len(cfg["rules"]), "findingCount": len(findings), "bySeverity": by_sev,
"blockedPairs": blocked_pairs, "cleanPairs": clean_pairs, "assetSummary": asset_rows,
"digest": digest, "csv": os.path.join(a.out, "g06_taboo_scan.csv")}
with open(os.path.join(a.out, "g06_taboo_scan.json"), "w", encoding="utf-8") as f:
json.dump(rep, f, ensure_ascii=False, indent=2)
print(json.dumps({"digest": digest, "regionCount": rep["regionCount"],
"assetCount": rep["assetCount"], "findingCount": len(findings),
"bySeverity": by_sev, "blockedPairs": blocked_pairs, "cleanPairs": clean_pairs,
"assetSummary": [{"id": r["id"], "shipCount": r["shipCount"],
"blockedCount": r["blockedCount"]} for r in asset_rows]},
ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
在本机跑 /opt/homebrew/bin/python3 sim/g06_taboo_scan.py --config config/ --out out/,9 个素材 × 8 个地区 = 72 对的结果是(同机相对量级,绝对数值不可移植):
digest = findings=18,block=12,warn=3,info=3,blockedPairs=12,cleanPairs=55
18 条命中里,12 条是阻断、3 条警告、3 条提示;72 对里有 12 对被阻断、55 对完全干净。逐素材看(shipCount 是可安全发行的地区数):
| 素材 | 可发行地区 | 被阻断地区 | 读法 |
|---|---|---|---|
| 主图标·小岛 | 8 | 0 | 零命中,全区可发 |
| 按钮·点赞 | 7 | 1 | 阿语区手势冒犯 |
| 角色·小猪 | 7 | 1 | 阿语区动物禁忌 |
| 装饰·回纹 | 7 | 1 | 德国卐形符号(金色在中东仅提示) |
| 装饰·白花 | 8 | 0 | 仅日本警告,无阻断 |
| 徽章·四连 | 8 | 0 | 韩警告、日提示,无阻断 |
| 角色·奶牛 | 7 | 1 | 印度牛禁忌 |
| 按钮·确认 | 8 | 0 | 仅巴西警告 |
| 活动·下注卡 | 0 | 8 | 触「赌博」全平台红线,一个地区都发不了 |
这张表最有价值的一行是最后一行:card_bet 的 shipCount=0——它不是「某个地区要改」,而是这张卡从设计上就不该存在。这就是把禁忌做成矩阵的意义:它把「感觉有点风险」变成「0 个地区可发」这种无法辩驳的结论。
P 层内核:跨语言对拍,证明判定没写错
一张表看不出「是判定对」还是「碰巧看着对」。本套用一份 P 层 TypeScript 内核 core/src/g06/taboo.ts 复算同一套匹配:同样的 region ∈ {"*", region}、同样的 token ∈ tokens、同样的三档取最小严重度。它读同一份 config/g06_taboo.json,产出与 Python 逐字符一致的摘要。
// file: core/src/g06/taboo.ts
// tier: P
// verify: node --test 'core/test/g06_*.test.ts'
//
// 文化禁忌与符号审查内核:与 sim/g06_taboo_scan.py 逐位等价的 TypeScript 对偶实现。
// 读同一份 config/g06_taboo.json;全整数、无浮点、无随机、禁 Math.random。
// 把「素材语义标签」× 「地区禁忌规则」匹配成一张 asset × region 命中矩阵。
export interface TabooRegion { readonly id: string; readonly name: string }
export interface TabooRule {
readonly id: string; readonly region: string; readonly kind: string;
readonly token: string; readonly severity: number; readonly note: string;
}
export interface TabooAsset {
readonly id: string; readonly name: string; readonly kind: string; readonly tokens: readonly string[];
}
export interface TabooConfig {
readonly version: number; readonly game: string;
readonly regions: readonly TabooRegion[];
readonly rules: readonly TabooRule[];
readonly assets: readonly TabooAsset[];
}
export interface Finding {
readonly asset: string; readonly region: string; readonly rule: string;
readonly kind: string; readonly severity: number;
}
export interface Pair {
readonly asset: string; readonly region: string; readonly hits: number;
readonly severity: number; readonly blocked: number; readonly clean: number;
}
export interface ScanResult {
readonly findings: readonly Finding[]; readonly pairs: readonly Pair[];
}
/** 素材在指定地区命中的规则(region='*' 的规则对所有地区生效)。 */
export function matchRules(cfg: TabooConfig, asset: TabooAsset, region: string): TabooRule[] {
const toks = new Set(asset.tokens);
return cfg.rules.filter((r) => toks.has(r.token) && (r.region === "*" || r.region === region));
}
/** 扫描整张矩阵:逐素材 × 逐地区求命中集合、严重度、可发行地区。 */
export function scan(cfg: TabooConfig): ScanResult {
const findings: Finding[] = [];
const pairs: Pair[] = [];
for (const a of cfg.assets) {
for (const reg of cfg.regions) {
const hits = matchRules(cfg, a, reg.id);
const severity = hits.length === 0 ? 0 : Math.min(...hits.map((h) => h.severity));
pairs.push({ asset: a.id, region: reg.id, hits: hits.length, severity,
blocked: severity === 1 ? 1 : 0, clean: hits.length === 0 ? 1 : 0 });
for (const h of hits) {
findings.push({ asset: a.id, region: reg.id, rule: h.id, kind: h.kind, severity: h.severity });
}
}
}
return { findings, pairs };
}
/** 跨语言对拍摘要:与 sim/g06_taboo_scan.py 的 digest 逐字符一致。 */
export function digest(cfg: TabooConfig): string {
const { findings, pairs } = scan(cfg);
let block = 0; let warn = 0; let info = 0;
for (const f of findings) {
if (f.severity === 1) block += 1; else if (f.severity === 2) warn += 1; else info += 1;
}
let blockedPairs = 0; let cleanPairs = 0;
for (const p of pairs) { if (p.blocked === 1) blockedPairs += 1; if (p.clean === 1) cleanPairs += 1; }
return `findings=${findings.length},block=${block},warn=${warn},info=${info},`
+ `blockedPairs=${blockedPairs},cleanPairs=${cleanPairs}`;
}
/** 某素材可安全发行的地区数(无 severity=1 命中)。 */
export function shipCount(cfg: TabooConfig, assetId: string): number {
const asset = cfg.assets.find((a) => a.id === assetId);
if (asset === undefined) return 0;
let n = 0;
for (const reg of cfg.regions) {
const sev = matchRules(cfg, asset, reg.id).reduce((m, h) => Math.min(m, h.severity), 4);
if (sev !== 1) n += 1;
}
return n;
}
配套测试 core/test/g06_taboo.test.ts 把 Python 落盘的摘要写成金标准,断言 TS 内核复算结果逐字符相等,并顺手验全平台红线、地区专属规则与同 token 分级。在本机跑 node --test 'core/test/g06_*.test.ts',本节 6 个用例全过:
✔ 跨语言一致:TS 命中矩阵摘要 == Python 金标准
✔ 矩阵规模:9 素材 × 8 地区 = 72 对,命中三元组 18 条
✔ 全平台红线:word:gambling 在全部 8 个地区都命中且阻断
✔ 地区专属:卐形纹样只在德国阻断,金色在中东是正向提示
✔ 干净素材:主图标在所有地区零命中,可发行地区数 = 8
✔ 同 token 分级:number:4 在韩国是警告(2)、在日本是提示(3)
ℹ tests 6 / pass 6 / fail 0
只要有一边的 region="*" 展开、token 包含判定或「取最小严重度」的口径写错,摘要立刻分叉。这与卷二「一份逻辑、两种宿主」
是同一个信念:同一张规则表,在 Python 校验器与 TS 内核里必须判得一模一样。
X 层:真实审核结论不可复现
必须把边界划清。上面算的是规则侧:给定一张禁忌表与一批素材标签,命中矩阵是什么。而下面这些属于 X 层,需真实业务环境,命令行不可复现:
- 各平台的真实审核结论(同一张图在不同平台、不同时期可能结论不同,且平台侧不透明);
- 各地区的真实法规与执法尺度(本节只做「公开可观察」的符号整理,不构成法律意见);
- 素材实际长什么样(标签是人工打的,与真实画面的偏差靠美术复核兜住)。
工程上正确的做法是:把可复现的规则层与不可复现的审核层分开——规则层保证「明显会踩的坑一个不漏」,审核层靠提审后的真实驳回结果反标定规则表(把被驳回的素材补进 assets、补一条 rules,矩阵立刻更新)。这也是本节与 6.2 的接口:审查的结论直接决定哪些素材需要做地区变体。
自检表
| 检查项 | 合格标准 |
|---|---|
| 三档而非二元 | 阻断 / 警告 / 提示分开,不把「建议改」当「不能发」 |
| 红线可复用 | region="*" 的全平台红线对所有地区生效 |
| 跨语言一致 | Python 校验器与 TS 内核摘要逐字符相同 |
| 边界清晰 | 真实审核与法规标 X;不写成「实测」,不提供法律意见 |
到这里,第 6 章的文化侧就立起来了:先有禁忌规则表,才有素材变体。下一节我们把这张矩阵的结论折算成钱——哪些素材要做地区变体、做几套、花多少,用一份成本仿真把「全做」和「只做性价比最高的那几个市场」摆在一起算。
阅读导航:上一节:5.3 文案膨胀与 UI 适配 · 下一节:6.2 美术与素材的本地化变体 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。