Use Google Sheets as a Translation Database for Your Web App (Apps Script + Next.js) 封面圖片

Hayrullah Kar

我看過的每一個 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-intli18next。在部署之間,如果還想要一個讀取即時資料的預覽環境,在端點前設置 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 團隊製作。