Use Google Sheets as a Translation Database for Your Web App (Apps Script + Next.js)
Every i18n setup I've seen has the same three-way standoff. Developers want type-safe JSON in the repo. Translators want a familiar tool, not a pull request. Product wants to fix a typo without a deploy. So you either pay $50–$500/month for a localization SaaS, or you copy-paste strings between a translator's spreadsheet and your JSON files until something silently breaks. For projects under ~1,000 keys, there's a better middle: the spreadsheet is the database. Translators edit a Google Sheet; an Apps Script endpoint serves it as clean locale JSON; your app pulls that at build time. Here's the whole pattern, with the code. Why a sheet beats a translation service for small projects A localization SaaS earns its price at scale — dozens of translators, thousands of keys, screenshots and review workflows. A 300-key marketing site doesn't have that problem; it has a coordination problem. A Sheet solves coordination for free: translators already know it, it has revision history and suggested edits built in, and product can change a string in ten seconds. You only add the two things a raw sheet lacks — a clean JSON API and a fallback for missing translations. The schema: one tab, one row per key A strings tab, with the key in column A and one column per locale: key en tr es fr hero.title Welcome Hoş geldiniz Bienvenido Bienvenue hero.cta Get started Başla Empezar Commencer Use dot-notation keys ( hero.title ) so the JSON nests naturally in your i18n library. Keep a tiny meta tab too: B1 = default locale ( en ), B3 = version ( 1.0.0 ). The Apps Script endpoint Deploy this as a Web App (same mechanics as any Apps Script webhook ). doGet serves one locale — or all of them — as JSON, and the fallback lives right in the query: an empty cell resolves to the default locale, so a half-translated key never ships blank. // Code.gs const SHEET_ID = ' your-sheet-id ' ; function doGet ( e ) { const locale = ( e . parameter . locale || ' all ' ). toLowerCase (); const result = buildLoca