我见过的每一个 i18n 方案都有相同的三方僵局。开发者想要仓库中类型安全的 JSON,译者想要熟悉的工具,而非拉取请求,产品想要在不部署的情况下修复错字。于是你要么每月支付 50–500 美元购买本地化 SaaS,要么在译者的表格和 JSON 文件之间复制粘贴字符串,直到出现静默错误。
对于少于约 1,000 个键的项目,有一个更好的折中方案:表格本身就是数据库。译者编辑 Google Sheet;Apps Script 端点将其作为干净的区域设置 JSON 提供;你的应用在构建时拉取这些数据。下面是整个模式及代码。
为什么对小型项目来说,表格优于翻译服务
本地化 SaaS 在规模化场景下——数十名译者、数千个键、截图和审阅工作流——才值这个价。一个 300 键的营销站点没有这种问题,它有的是协调问题。Sheet 免费解决了协调问题:译者已经熟悉它,它内置修订历史和建议编辑,产品可以在十秒内更改字符串。你只需添加原始表格所缺乏的两个功能——干净的 JSON API 和缺失翻译的回退。
模式:一个标签页,每行一个键
一个 strings 标签页,A 列是键,其余列分别为各区域设置:
| key | en | tr | es | fr |
|---|---|---|---|---|
| hero.title | Welcome | Hoş geldiniz | Bienvenido | Bienvenue |
| hero.cta | Get started | Başla | Empezar | Commencer |
使用点号表示法键(hero.title),这样 JSON 就能自然嵌套到你的 i18n 库中。还需保留一个小型 meta 标签页: B1 = 默认区域设置(en), B3 = 版本(1.0.0)。
Apps Script 端点
将其作为 Web App 部署(与任何 Apps Script webhook 机制相同)。doGet 以 JSON 形式提供一个或全部区域设置,回退逻辑直接放在查询中:空单元格解析为默认区域设置,因此半翻译的键永远不会以空白形式发布。
// Code.gs
const SHEET_ID = 'your-sheet-id';
function doGet(e) {
const locale = (e.parameter.locale || 'all').toLowerCase();
const result = buildLocaleData(locale);
return ContentService
.createTextOutput(JSON.stringify(result))
.setMimeType(ContentService.MimeType.JSON);
}
function buildLocaleData(locale) {
const ss = SpreadsheetApp.openById(SHEET_ID);
const data = ss.getSheetByName('strings').getDataRange().getValues();
const headers = data[0]; // ['key','en','tr','es','fr']
const localeIdx = {};
headers.forEach((h, i) => {
if (i > 0) localeIdx[String(h).toLowerCase()] = i;
});
const meta = ss.getSheetByName('meta');
const defaultLocale = meta.getRange('B1').getValue() || 'en';
const version = meta.getRange('B3').getValue() || '1.0.0';
if (locale !== 'all' && !localeIdx[locale]) {
return { error: 'Unknown locale', available: Object.keys(localeIdx) };
}
const targets = locale === 'all' ? Object.keys(localeIdx) : [locale];
const out = { locale, defaultLocale, version, strings: {} };
if (locale === 'all') targets.forEach(l => out.strings[l] = {});
for (let row = 1; row < data.length; row++) {
const key = data[row][0];
if (!key) continue;
if (locale === 'all') {
targets.forEach(l => {
out.strings[l][key] =
data[row][localeIdx[l]] || data[row][localeIdx[defaultLocale]];
});
} else {
out.strings[key] =
data[row][localeIdx[locale]] || data[row][localeIdx[defaultLocale]];
}
}
return out;
}
Enter fullscreen mode Exit fullscreen mode
我在发布前对 buildLocaleData 进行了单元测试——关键的测试用例包括:空单元格回退到默认区域设置、跳过空白键行,以及未知区域设置返回可用区域设置列表而非 500 错误。
前端集成:构建时同步
不要在运行时调用端点——在构建时一次性拉取 JSON 并提交到 public/locales。你的应用发布静态文件,Sheet 宕机永远不会导致站点瘫痪。
// scripts/sync-locales.ts
import fs from 'fs';
import path from 'path';
const URL = process.env.LOCALE_SOURCE_URL!;
const LOCALES = ['en', 'tr', 'es', 'fr'];
async function sync() {
for (const locale of LOCALES) {
const res = await fetch(`${URL}?locale=${locale}`);
const data = await res.json();
const out = path.join(process.cwd(), 'public', 'locales', `${locale}.json`);
fs.writeFileSync(out, JSON.stringify(data.strings, null, 2));
console.log(`✓ ${locale}: ${Object.keys(data.strings).length} strings`);
}
}
sync();
Enter fullscreen mode Exit fullscreen mode
将其挂到 prebuild 中,像处理其他 JSON 一样把输出交给 next-intl 或 i18next。两次部署之间,如果还需要预览环境读取实时数据,在端点前加 5 分钟边缘缓存即可。
缓存、版本控制与捕获缺失键
meta 标签页中的 version 字段允许你有意识地进行缓存失效,而非猜测。每日触发器能将“译者漏译了三条字符串”从生产事故转化为一封邮件:
// Missing-keys alert — run on a daily time trigger
function alertMissingKeys() {
const ss = SpreadsheetApp.openById(SHEET_ID);
const data = ss.getSheetByName('strings').getDataRange().getValues();
const headers = data[0];
const missing = [];
for (let r = 1; r < data.length; r++) {
headers.forEach((h, c) => {
if (c > 0 && !data[r][c]) missing.push(`${data[r][0]} → ${h}`);
});
}
if (missing.length) {
MailApp.sendEmail('[email protected]',
`${missing.length} missing translations`,
missing.join('\n'));
}
}
Enter fullscreen mode Exit fullscreen mode
何时停止使用 Sheets 并付费使用 Lokalise
请诚实地评估上限。Sheets 能舒适支撑到大约 1,000 个键;Apps Script 提供约 50–100 次/秒的请求,这对于构建时同步完全够用。真正的限制是人数:超过 5 名左右活跃译者 时,你会需要完善的角色、截图和审阅队列——这时可以将表格导出为 CSV 并迁移到 Lokalise 或 Crowdin。在此规模以下,工具本身就是协调成本,而非价值所在。
常见陷阱
- 不要在运行时拉取。 构建时同步意味着 Sheet 宕机不会破坏你的线上站点。提交 JSON。
- 把回退逻辑放在查询中,而不是客户端。 空单元格解析为默认区域设置(如上所示)比散布在各组件中的回退逻辑更简单、更安全。
-
跳过空白键行。 译者会留下空行;用
if (!key) continue;守卫,否则它们会在 JSON 中变成""键。 -
有意识地管理版本。 基于
version字段缓存,而不是时间戳,这样修复错字不会强制全量重建,而需要重建时也不会被跳过。 - 复数:每种形式一行。 对于超出简单计数的场景,存储 ICU MessageFormat 字符串并在客户端解析——不要自己发明复数列。
总结
对于不到一千个键的项目,i18n 不需要订阅——它需要一个共享的可编辑源和一个干净的 JSON API。Google Sheet 是前者;约 40 行 Apps Script 是后者。译者得到熟悉的工具,开发者得到 JSON,产品得到即时编辑,且不会发布空白内容。
生产版——边缘缓存层、版本固定以及完整的 Next.js 集成——已在 MageSheet 博客 上发布。
由 MageSheet 团队构建。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.