简介:这是一份面向Go程序开发者的语言代码工具包,用于解决国际化与本地化开发中常见的语言标识符映射问题。包内完整收录了国际标准组织制定的语言代码体系,包含所有语言的标准名称、本地名称以及对应的双字母代码,并提供了编码列表、名称列表、本地名称列表和完整语言列表四种内置数据,可在程序初始化时快速载入并直接读取。资源包共包含十个文件,其中五个为Go语言源文件,兼具核心逻辑与测试用例;两个为说明文档,便于理解使用方式;其余包括忽略规则、持续集成配置及许可证文件,整个压缩包大小仅为九千字节,非常轻量。目前已有九百三十二人学习或下载,适合中高级Go开发者将其引入多语言网站、内容管理系统或语言识别工具,作为快速获取语言数据的可靠依赖。同时也可作为学习如何用Go组织静态数据与设计简洁API的参考实例,省去手工维护语言映射表的额外成本。
1. ISO 639-1 不是一张静态表:先搞清它解决什么问题
做国际化时,语言列表是最容易被低估的模块。ISO 639-1 语言和代码列表,说白了就是一套给“语言”分配编码的标准:英语是 en,中文是 zh,日语是 ja。大多数项目里,它能解决三类具体问题:语言选择器里到底展示哪些候选、用户偏好用什么格式存进数据库、以及从第三方登录或浏览器请求里拿到的语言标签怎么归一化。我见过太多项目把语言代码当成字符串随手存,大小写混乱、简繁不分、繁体用户被送到英文界面,最后全是历史债务。这篇笔记就从标准边界讲起,一路落到可跑的代码和五条血泪经验,目标是让你今天就能把语言列表接进项目。
2. 语言代码三兄弟:639-1/639-2/639-3 怎么选不翻车
2.1 从两字母到三字母再到全覆盖的语言代码谱系
ISO 639 家族不是一套代码,而是几套代际不同的标准。最早也最常用的是 639-1 的 Alpha-2 代码,两个小写字母,比如 fr、de、es。这套代码的容量非常有限,26 个字母排列组合只有 676 个位置,这决定了它只能覆盖主要语言,不可能容纳全世界几千种语言。
于是后面补了 639-2 的 Alpha-3 代码,三个字母,能覆盖几百种语言。这又分两套:terminology(术语)和 bibliographic(书目)两种形式,比如德语在书目体系里是 ger,在术语体系里是 deu。老系统的元数据里经常混用这两套,匹配时容易踩坑。再往后是 639-3,同样三字母但覆盖面到七千多种语言,包含大量只在特定地区使用的语言和方言,标识符也基本与 639-2 的术语形式重合。
实际开发里最常见的误区是:想在 639-1 列表里找到“客家话”“粤语”这种条目,找不到就认为数据不全。其实不是缺失,而是这套标准的设计目标就不是覆盖它们。理解这个谱系,你才能判断该用哪一代代码。
2.2 一张表看清三种代码的适用场景
| 维度 | ISO 639-1(Alpha-2) | ISO 639-2(Alpha-3) | ISO 639-3(Alpha-3) |
|---|---|---|---|
| 代码长度 | 2 个字母 | 3 个字母 | 3 个字母 |
| 覆盖规模 | 一百多种主要语言 | 四百多种 | 七千多种 |
| 典型字段叫法 | alpha2 | alpha3 / alpha3-bib / alpha3-term | id / code |
| 适合干什么 | 界面语言、用户设置项、浏览器请求里的语言标签 | 图书馆元数据、老系统中继格式、部分 NLP 语料标注 | 学术研究、长期存档、对少数民族语言做结构化记录 |
| 现实中的坑 | 中文只给一个 zh,简繁不分 | 同一种语言有 term 和 bib 两套写法 | 数据量太大,做下拉框直接超载 |
我一般这样判断:凡是用户能看到的界面语言,优先用 639-1;凡是做数据分析或文档归档,才需要往下看 639-2 或 639-3。项目初期只做国际化,根本不需要三字母代码。
2.3 实际项目里最常见的数据形态与获取路径
网上能搜到的 639-1 语言列表,主流形态是 JSON 数组,每项长这样:
{ "code": "zh", "name": "Chinese", "nativeName": "中文" }, { "code": "en", "name": "English", "nativeName": "English" }有些数据源会额外带 alpha3、scope(语言类型是个人语言还是宏观语言)、iso639_2_bibliographic 这类衍生字段。核心字段就三个:code、英文名、原生名。code存进数据库作为主键;nativeName用于语言选择器展示;name用于兜底和搜索。
关于获取路径,行业里比较可靠的做法是直接取 Unicode CLDR 导出的语言列表,或者从打包好的 npm 包(比如语言代码映射库)里读取。不建议手动维护这份表,语言代码虽然更新慢但不是不变,废弃代码、拼写修正都在发生,手工维护迟早漏掉。数据落地时记得把字段名对齐成自己项目的风格,别直接塞进去就完事。
3. 用 ISO 639-1 列表做语言选择器:最小可跑通的 Python 落地方案
3.1 先造一份干净的本地数据
拿到原始 JSON 后,哪怕只在一个小脚本里用,我建议都先清洗一遍,别拿网上的原始文件直接进代码。清洗做三件事:去掉没有code的空行、去掉未知字段、把code强制转小写。下面是一个最小清洗脚本:
import json from pathlib import Path raw = json.loads(Path("iso639-1-raw.json").read_text(encoding="utf-8")) cleaned = [] for item in raw: code = item.get("code") or item.get("alpha2") if not code: continue cleaned.append({ "code": code.lower(), "name": item.get("name", ""), "nativeName": item.get("nativeName", item.get("native_name", "")), }) Path("iso639-1.json").write_text( json.dumps(cleaned, ensure_ascii=False, indent=2), encoding="utf-8" ) print(f"清洗完成,共 {len(cleaned)} 条")这段代码解决了数据源里字段名不统一的问题:有人给alpha2,有人给code;原生名有的是nativeName,有的是native_name。输出统一成三个字段,后面对接逻辑才不会到处都是if。
参数上要注意两点:ensure_ascii=False保证中文原生名不被转义成\u序列,文件体积更小且可读;indent=2只是给人看的,如果这份 JSON 要直接打进前端资源,建议去掉缩进再发布。
3.2 把 BCP 47 用户标签归一到 639-1:一段能用的解析函数
浏览器或第三方登录带过来的语言通常不是干净的zh,而是zh-CN、zh-Hant-TW、sr-Latn-RS这种 BCP 47 标签。最稳妥的归一化手段是取第一个连字符前的 segment 并转小写:
import re from functools import lru_cache alpha2_set = frozenset() @lru_cache(maxsize=16) def normalize_lang_tag(tag: str) -> str | None: if not tag or not tag.strip(): return None primary = tag.strip().split("-")[0].lower() if re.fullmatch(r"[a-z]{2}", primary) and primary in alpha2_set: return primary return None这里split("-")[0]是 BCP 47 的 primary language subtag,两字母的情况基本对应 639-1;re.fullmatch用来挡住en-US这种合法标签里混进来的非法主语言。lru_cache加不加都行,但如果这段函数会被循环调用(比如批量处理用户列表),缓存能省掉大量重复正则计算。
注意我说的是“基本对应”:BCP 47 主语言也有三字母形式,对应 639-2/639-3 代码,这类标签比如cmn-Hans-CN,在这里不会被匹配到。这是刻意设计,后面回退链会处理。
3.3 回退链:没有 639-1 代码时项目怎么兜底
用户标签里的主语言可能根本没有 639-1 代码,典型的如粤语(yue)、客家话(hak)。这时我不会直接返回None把人踢到英语,而是维护一张项目自己的回退映射表:
# 非 639-1 主语言到界面语言的回退 LANG_FALLBACK = { "yue": "zh", # 粤语没有 639-1 代码,按中文处理 "hak": "zh", "cmn": "zh", # 普通话的 639-3 代码 "nan": "zh", # 闽南语 "bho": "hi", # 博杰普尔语按印地语处理 } def resolve_interface_lang(tag: str) -> str: primary = normalize_lang_tag(tag) if primary: return primary subtag = tag.strip().split("-")[0].lower() if tag else "" return LANG_FALLBACK.get(subtag, "en")这张表的设计原则是“用户能看懂优先于代码准确”:与其给一个没有翻译资源的 yue 标签,不如落到 zh,让用户至少看到完整的中文界面。这个方案的副作用是,如果产品以后要单独支持粤语,必须提前把 yue 从回退表里挪走,换成真实的语言资源键。工程上是把回退表独立成配置文件,不要和业务代码揉在一起。
4. 列表字段的工程化取舍:displayName、原生名称排序与首屏渲染
4.1 字段别只存一个 name,把英文名和原生名分开
很多入门项目会在数据库里只存一个 display_name 字段,里面填“Chinese”或“中文”看心情。这是后面排序、搜索全部失控的根源。正确的工程化做法是至少保留两个展示字段:英文名(用于无本地化资源时兜底)和原生名(用于语言选择器展示)。
| 字段 | 示例 | 用途 |
|---|---|---|
code | zh | 存储、匹配、API 参数 |
name | Chinese | 搜索兜底、英文界面展示 |
nativeName | 中文 | 语言选择器展示、按原生名排序 |
dir | ltr/rtl | 控制选择器自身布局 |
最后这个dir字段特别容易被忽略。阿拉伯语、希伯来语这些语言本身是 RTL,语言选择器如果是个下拉面板,列出的原生名需要针对 RTL 语言单独设置方向,否则阿拉伯语条目在英文左对齐的列表里会显示得很难看。这个字段在纯 639-1 列表里不存在,需要从 CLDR 的数据里补进来,但一张表里存上不亏。
4.2 排序、搜索与去重的三个参数细节
排序是第一个坑。拿 code 排序,结果就是 ar、de、en、fr 这种,看起来毫无规律;拿英文名排序,中文用户看到的也不是自己熟悉的顺序。我一般按nativeName排序,但这又引出编码问题:Python 默认的字符串排序按 Unicode 码点走,中文会排在几乎最后面,而且声调顺序完全不对。
轻量方案是让后端把这些数据按浏览器语言排序后直接吐 JSON,前端不参与排序。如果必须在前端排序,就按用户当前界面语言做 locale-aware 排序,比如界面是英语就用localeCompare的en参数,界面是中文就传zh。列表本身只有一百来条,排序计算量完全可以忽略。
搜索也有一个参数细节:搜索框要对name和nativeName同时匹配,但匹配优先级应该是原生名 > 英文名。比如用户敲“ch”时,“中文”别排到“Chinese”前面;但用户敲“zh”时,“中文”要名列前茅,因为“zh”既是拼音起始字符也是代码。多语言搜索里的相关度是技术活,这里给出的方案只是够用版本。
去重则要注意数据源拼接时产生的重复:同一份 JSON 里可能出现en和eng两条记录,前者来自 639-1 数据源,后者来自 639-3 衍生字段。清洗时要把 639-1 之外的代码全部滤掉,过滤条件就一条:字段长度等于 2 且全为小写字母。
4.3 从列表到控件:数据量虽小,缓存策略也要定
一百多条 JSON 看起来小,但有个问题是每一条带原生名称后,文件大小会到五六 KB。如果页面在首屏要渲染语言选择器,这个数据可以直接打进 bundle 或 localStorage,不用每次进页面重新请求:
CACHE_KEY = "app.lang_list.v1" def get_lang_list(): cached = cache.get(CACHE_KEY) if cached: return cached data = fetch_lang_list_from_api() cache.set(CACHE_KEY, data, ttl=86400 * 7) return data这里的缓存键带上了版本号v1,一旦列表内容调整,改个版本号就能让所有客户端重新拉取,这是最容易被忽略的参数。TTL 设一周是平衡方案:639-1 的更新频率远低于一周一次,设成一小时纯属浪费;设成永久不发版又会让用户永远看不到修正。另外如果项目是纯前端,把列表作为静态 JSON 文件打包进构建产物,比调 API 更省事,也天然带缓存刷新机制。
5. ISO 639-1 项目落地避坑清单:五条血泪经验
5.1 现象:数据库里存的 "ZH",解析时怎么都不匹配
某跨平台系统的用户表里,语言偏好字段清一色大写,ZH、EN、JA。后端做语言匹配时原样拿这串去查 639-1 列表,结果一条都查不到,所有人被扔进默认英语。原因:ISO 639-1 的规范写法就是小写字母,大写字符串在比较前必须做归一。
解决:在写入侧强制code.lower(),并在数据库字段上做唯一索引,避免脏数据二次进入。已经存在的存量数据用一个一次性脚本批量修正:
UPDATE users SET lang_code = LOWER(lang_code) WHERE lang_code <> LOWER(lang_code);这段 SQL 先跑一次,后面新代码从入口处拦截。
5.2 现象:用户选了“中文”,保存后变成英文界面
某管理系统把语言偏好存成zh,但项目里的翻译资源键是zh-Hans和zh-Hant。用户选“中文”后,前端的多语言框架用zh找到不任何资源,于是按兜底规则落到en。现象就是用户明明选了母语,界面反而变成英文。
原因:混淆了 ISO 639-1 语言代码和 BCP 47 语言环境标签。639-1 的zh只表示“中文”这个语言,不含简繁信息;而实际渲染界面需要的是带地域或文字子标签的完整 locale 标识。
解决:偏好存储拆成两个字段,language字段只存 639-1 主代码用于统计列表展示,locale字段存zh-Hans这类完整标签用于运行时加载资源。不要试图用一个字段同时承担两个职责。
5.3 现象:列表里查到“粤语”有代码,但项目里就是没有
有开发者从某语言数据包里查到了yue这个 code,兴奋地加进语言选择器,结果用户选完,界面所有文案回到英文。原因:这个数据包混入了 639-3 的语言条目,而项目的翻译资源只覆盖了 639-1 里的语言。yue在标准上有代码,在业务资源里没有对应文件。
解决:语言选择器展示的列表必须来自“项目实际支持的资源键”,而不是来自语言代码表。做法是把资源目录下的可用 locale 枚举出来,再反向去 639-1 表里取显示名称。资源没配的语言,代码表里有也不该展示。
5.4 现象:废弃代码 iw / ji / in 从老系统里冒出来
老系统导出数据里频繁出现iw(希伯来语的旧代码)、ji(意第绪语旧代码)、in(印度尼西亚语旧代码)。这些代码在 ISO 639-1 历史上真实存在,但后来被替换成he、yi、id。直接拿它们查语言列表会落空。
原因:标准本身发生过修订,旧代码从规范中移除,但老系统的存量数据不会自动迁移。
解决:在归一化函数里维护一张废弃代码映射表,解析时先查映射再走正常逻辑:
LEGACY_CODE_MAP = { "iw": "he", "ji": "yi", "in": "id", } def normalize_legacy(code: str) -> str: return LEGACY_CODE_MAP.get(code, code)建议把这张表放进 3.2 节的归一化函数里,作为解析前的前置钩子,而不是散落在各个业务调用处。
5.5 现象:语言选择器里英语排第一,用户找不到自己的语言
选语言的下拉框按英文名排序,结果阿拉伯语、保加利亚语全跑到中前部,中文因为首字母 C 排第三大量用户还得翻页。这类问题在数据量小的时候不明显,一旦支持语言超过二十个,就非常影响转化。原因:排序键选错,以及默认值设置太粗暴。
解决:排序改按原生名称在当前界面语言下的排序规则;第二个参数是“非当前用户语言不要置顶”。倒不是说不能默认英语,而是默认语言选择要和识别出的浏览器语言一致,识别不到才用英语兜底。这比任何排序都更能让用户快速找到自己的语言。
6. 把静态列表变成项目里的活工具:生成校验函数与类型定义
拿到 ISO 639-1 列表后,不要只停留在“能查到 code”这一层。我习惯把它生成为一套可调用的校验与解析工具,并在构建期跑一遍单元测试,防止后续改动资源文件时把语言代码改坏。下面这段脚本展示如何从清洗后的 JSON 生成代码模块:
import json from pathlib import Path table = json.loads(Path("iso639-1.json").read_text(encoding="utf-8")) alpha2 = {item["code"] for item in table} template = f'''"""ISO 639-1 语言代码工具。""" ALPHA2_CODES = {repr(sorted(alpha2))} def is_valid_lang_code(code: str) -> bool: return code in ALPHA2_CODES def primary_subtag(tag: str) -> str | None: if not tag or "-" not in tag: return tag.lower() if tag else None return tag.split("-")[0].lower() ''' Path("lang_util.py").write_text(template, encoding="utf-8")生成后的模块要配几组断言验证行为符合预期:
assert is_valid_lang_code("zh") is True assert is_valid_lang_code("ZH") is False # 大小写敏感是故意的 assert primary_subtag("sr-Latn-RS") == "sr"这三条断言分别锁住三个行为:正常代码能识别、大写代码不被静默放过、BCP 47 标签能正确抽取主语言。之前某项目在构建管道里加了这类校验后,代码大小写回归问题再没出现过。把这些工具代码放进共享公共库,语言列表就从一个 JSON 文件变成了一套可维护的工程资产,后续无论接多语言 SDK 还是做语言检测都会省心不少。希望帮到你。
本文还有配套的精品资源,点击获取