我看過的每一個 i18n 設定都有同樣的三方僵局。開發者想要存放於儲存庫中的型別安全 JSON。翻譯者想要熟悉的工具,而不是提交 Pull Request。產品團隊想要在不需要部署的情況下修正錯字。因此,你不是支付每月 50 至 500 美元的在地化 SaaS 費用,就是在翻譯者的試算表與 JSON 檔案之間複製貼上字串,直到某個東西無聲地壞掉。
對於大約 1,000 個鍵以下的專案,有一個更好的折衷方案:試算表「就是」資料庫。翻譯者編輯 Google 試算表;Apps Script 端點將其作為乾淨的地區設定 JSON 提供服務;你的應用程式在建置時拉取該資料。這裡是完整的模式,以及程式碼。
為什麼試算表比翻譯服務更適合小型專案
在地化 SaaS 在規模達到一定程度後才值得付費——數十名翻譯者、數千個鍵、螢幕截圖和審核工作流程。一個 300 鍵的行銷網站沒有這種問題;它有的是「協調」問題。試算表免費解決協調問題:翻譯者已經熟悉它,它內建修訂歷史和建議編輯功能,產品團隊可以在十秒內更改字串。你只需要補足原始試算表所缺少的兩件事——乾淨的 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。你的應用程式發佈靜態檔案,而試算表中斷永遠不會讓你的網站停機。
// 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
何時停止使用試算表並付費使用 Lokalise
誠實面對上限。試算表在達到大約 1,000 個鍵 之前仍然舒適;Apps Script 以每秒約 50-100 個請求的速度提供服務,而這是建置時同步永遠不會接近的。真正的限制是人:超過 約 5 名活躍翻譯者 後,你會需要適當的角色、螢幕截圖和審核佇列——那時你會將試算表匯出為 CSV 並移至 Lokalise 或 Crowdin。在此之下,工具就是協調成本,而不是價值。
陷阱
- 不要在執行時擷取。 建置時同步意味著試算表中斷不會破壞你的即時網站。提交 JSON。
- 將備援放在查詢中,而不是客戶端。 空白儲存格解析為預設地區設定(如上)比分散在各元件中的備援邏輯更簡單、更安全。
-
跳過空白鍵的列。 翻譯者會留下空白列;用
if (!key) continue;防護,否則它們會變成 JSON 中的""鍵。 -
有意識地進行版本控制。 在
version欄位上進行快取,而不是時間戳記,這樣修正錯字就不會強制完整重建,而當你需要重建時也不會跳過。 - 複數:每個形式一行。 對於超出簡單計數的任何內容,儲存 ICU MessageFormat 字串並在客戶端解析它們——不要發明你自己的複數欄位。
總結
對於不到一千個鍵的專案,i18n 不需要訂閱——它需要一個共享的可編輯來源和乾淨的 JSON API。Google 試算表是前者;約 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.