多语言支持几乎是所有走向海外市场的应用都绕不开的需求。界面上的一句提示语可以放在语言包里,但商品名称、文章标题、分类标签这类动态数据必须存储在数据库中,怎么设计表结构才能优雅地容纳多种语言,是很多团队在实际项目中反复争论的话题。SQLite作为轻量级嵌入式数据库,被广泛用于移动端和小型服务,本文将以一个真实的商品数据场景为例,系统讲解几种可行的存储方案以及各自的取舍。

一、三种主流的多语言存储模型
在动手建表之前,先想清楚数据的变化频率和语言数量,这一点直接决定了该选哪种模型。常见的做法有三种:冗余字段、独立翻译表和JSON存储。
第一种是冗余字段方式,也就是在主表里直接为每种语言开一列,例如name_zh、name_en、name_ja。这种方式最直观,查询时不需要任何关联,直接SELECT对应的列即可,性能最好。但它的致命弱点是扩展性差:每新增一种语言就要执行一次ALTER TABLE加列,所有相关的INSERT和UPDATE语句都要改,ORM映射也随之变动。如果你的应用只固定支持两三种语言,且未来不太可能增加,这种方式反而是最省事的。
第二种是翻译表方式,把可翻译的内容抽到一张独立的表里,主表只保留与语言无关的字段。这是业内最推荐的标准做法,WordPress、Magento等知名系统的多语言设计本质上都是这个思路。它的扩展性极强,新增语言只需要插入数据行,表结构完全不用动。代价是查询时需要JOIN,或者用子查询取值,SQL会稍微复杂一些,不过配合合理的索引,性能完全可以接受。
第三种是用JSON字段存储所有语言版本,例如把{"zh":"手机","en":"Phone"}整段塞进一列。SQLite从3.9版本开始内置了JSON1扩展,支持json_extract函数,可以直接从JSON文本中取值甚至建索引。这种方式适合写多读少的场景,但查询语法相对繁琐,且在数据量大时索引利用不如传统列高效,一般只推荐作为过渡方案或轻量场景使用。
二、翻译表方案的完整实现
下面以商品表为例,给出翻译表方案的完整建表语句。主表products只存SKU、价格、分类等与语言无关的数据,翻译表product_translations则存储每种语言的名称和描述,并用复合主键保证一种语言一条记录。
-- 主表:只存与语言无关的字段
CREATE TABLE products (
product_id INTEGER PRIMARY KEY AUTOINCREMENT,
sku TEXT NOT NULL UNIQUE,
price REAL NOT NULL,
category_id INTEGER NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
-- 翻译表:一种语言对应一行翻译
CREATE TABLE product_translations (
product_id INTEGER NOT NULL,
lang TEXT NOT NULL, -- 语言代码,如 zh、en、ja
name TEXT NOT NULL,
description TEXT DEFAULT '',
PRIMARY KEY (product_id, lang),
FOREIGN KEY (product_id) REFERENCES products(product_id)
ON DELETE CASCADE
);
-- 常用查询:取出指定语言的商品
CREATE INDEX idx_trans_lang ON product_translations(lang, name);
INSERT INTO products (sku, price, category_id) VALUES ('SKU1001', 599.00, 3);
INSERT INTO product_translations (product_id, lang, name, description)
VALUES (1, 'zh', '智能手机', '高性能旗舰机型');
INSERT INTO product_translations (product_id, lang, name, description)
VALUES (1, 'en', 'Smart Phone', 'High performance flagship');
日常查询时,直接按语言代码取值即可。这里有一个非常实用的技巧:使用COALESCE函数实现回退逻辑。比如某些商品的日语翻译还没补齐,就自动降级到英文,避免界面上出现空白。
-- 带回退逻辑的查询:优先日语,缺失时回退到英语
SELECT p.product_id, p.price,
COALESCE(t_ja.name, t_en.name, 'N/A') AS name
FROM products p
LEFT JOIN product_translations t_ja
ON p.product_id = t_ja.product_id AND t_ja.lang = 'ja'
LEFT JOIN product_translations t_en
ON p.product_id = t_en.product_id AND t_en.lang = 'en';
如果不希望两次JOIN影响性能,也可以换一种写法:利用IN一次性把目标语言和回退语言都查出来,再在外层用排序加去重挑选最优先的那条。这种方式在商品列表这种大批量查询场景下通常表现更好,因为扫描次数从两次降到一次。
-- 单次JOIN配合排序取最优翻译,适合列表页
SELECT product_id, price, name
FROM (
SELECT p.product_id, p.price, t.name,
ROW_NUMBER() OVER (
PARTITION BY p.product_id
ORDER BY CASE t.lang WHEN 'ja' THEN 1 WHEN 'en' THEN 2 ELSE 3 END
) AS rn
FROM products p
JOIN product_translations t ON p.product_id = t.product_id
WHERE t.lang IN ('ja', 'en')
) tmp
WHERE rn = 1;
三、排序、检索与维护中的坑
多语言方案落地后,还有几个细节问题容易被忽视。第一个是排序规则。SQLite默认按照字符的二进制编码排序,对中文来说就是按Unicode码点排,结果完全不符合拼音习惯。解决思路有两种:一是查询时把数据拉到应用层,由编程语言按locale排序;二是额外维护一个拼音列或排序键列,写入时由应用生成,查询时直接按该列ORDER BY。SQLite本身可以通过ICU扩展支持区域感知排序,但需要自行编译,嵌入式场景下通常不建议这么折腾。
第二个是模糊搜索。不同语言对大小写的处理不一样,英语用户习惯不区分大小写搜索,SQLite提供了LIKE的case_sensitive_like编译选项和GLOB,但更通用的做法是在写入时同时保存一个小写化的辅助列,查询前先把关键词转成小写再匹配,这样行为可控且索引友好。日文假名还有全半角问题,同样建议在写入阶段统一归一化,不要把清洗逻辑散落在各个查询里。
第三个是翻译数据的批量维护。运营团队通常通过CSV或后台界面维护多语言内容,写入时要处理好部分翻译缺失的情况。推荐采用INSERT OR REPLACE的幂等写入方式,配合事务包裹批量导入,既保证一致性,又能在某条翻译有问题时不影响整批数据。导出时则可以按语言分组,方便各语种译者并行工作。
-- 批量幂等导入翻译数据
BEGIN TRANSACTION;
INSERT OR REPLACE INTO product_translations (product_id, lang, name, description)
VALUES
(1, 'ja', 'スマートフォン', '高性能フラッグシップモデル'),
(2, 'ja', 'ワイヤレスイヤホン', 'ノイズキャンセリング搭載');
COMMIT;
最后补充一点架构层面的建议:语言代码务必使用标准化的BCP 47格式,如zh-CN、en-US,避免团队里混用zh和cn这类自定义写法,否则后期数据清洗会非常痛苦。同时建议在翻译表上预留updated_at字段,方便做翻译缓存失效和增量同步。只要表结构设计得当,SQLite完全能撑起一个支持十几种语言的国际化应用,而且迁移到MySQL或PostgreSQL时,这套翻译表模型几乎可以原样平移。
SQLite多语言存储国际化设计i18n修改时间:2026-09-10 19:32:38