google-sheets-i18n
v1.1.0
Published
CLI package for syncing next-intl and react-i18next translations with Google Sheets
Maintainers
Readme
google-sheets-i18n
CLI package for syncing next-intl and react-i18next JSON translations with Google Sheets.
It is designed for projects where translations live in locale files such as src/messages/it.json, while editors work in a Google Sheet.
TL;DR
Install the package, add a few environment variables, then use:
npx google-sheets-i18n download
npx google-sheets-i18n upload --fill-empty
npx google-sheets-i18n find-new --dry-run --verbose
npx google-sheets-i18n translate-missing --language=en --dry-run
npx google-sheets-i18n translate-missing --language=de --provider=deepl
npx google-sheets-i18n translate-missing --language=en --to-sheetBest for projects that:
- store translations in one JSON file per locale
- use nested
next-intlmessages or flatreact-i18nextresources - manage translation editing in Google Sheets
What It Does
- Downloads translations from Google Sheets into local JSON files
- Uploads local translation keys and values into Google Sheets
- Supports nested JSON and stores sheet keys in dot notation like
nav.profile - Supports flat locale files whose keys may contain dots, via
TRANSLATIONS_KEY_SEPARATOR=false - Scans source files for
next-intl(useTranslations,getTranslations) orreact-i18next(useTranslation,getFixedT,i18n.t,<Trans i18nKey>) usage, selected withTRANSLATIONS_FRAMEWORK - Can draft missing local translations with OpenAI, DeepL, or Google Cloud Translation from your
DEFAULT_LANGUAGE - Warns about dynamic translation keys that cannot be resolved statically
Requirements
- Node.js
18+ - A Google service account with access to the spreadsheet
- A spreadsheet tab where:
- the first column is
key - the other columns are language codes like
it,en,de
- the first column is
Installation
Install the package as a dev dependency:
npm install --save-dev google-sheets-i18nWith pnpm:
pnpm add -D google-sheets-i18nWith yarn:
yarn add -D google-sheets-i18nReleases
Releases are automated with GitHub Actions and semantic-release.
Commits that affect package versions should follow Conventional Commits:
fix:publishes a patch releasefeat:publishes a minor releaseBREAKING CHANGE:orfeat!:publishes a major release
The release workflow runs on master and main, publishes to npm, updates package.json, and generates CHANGELOG.md.
To enable npm publishing, add the NPM_TOKEN repository secret in GitHub. GITHUB_TOKEN is provided automatically by GitHub Actions.
If you keep the current non-conventional commit history, the first automated release will happen only after the next release-worthy commit such as feat: or fix:. If you want to align history immediately, create the initial tag manually before enabling the workflow.
Quick Start
- Create a Google service account and enable the Google Sheets API.
- Share the spreadsheet with the service account email.
- Create a worksheet tab for translations.
- Set the first row headers, for example:
key | it | en | de- Copy your environment template:
cp .env.example .env- Fill the required variables in
.env. - Run one of the commands:
npx google-sheets-i18n download
npx google-sheets-i18n upload --fill-empty
npx google-sheets-i18n find-new --dry-run --verbose
npx google-sheets-i18n translate-missing --language=en
npx google-sheets-i18n translate-missing --language=en --to-sheetConfiguration
The package reads .env and .env.local from the current project directory.
Example:
GOOGLE_SHEET_ID=your_spreadsheet_id
GOOGLE_SHEET_TITLE=Translations
GOOGLE_SERVICE_ACCOUNT_EMAIL=service-account@project-id.iam.gserviceaccount.com
GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
TRANSLATIONS_DIR=src/messages
TRANSLATIONS_SOURCE_DIR=src
DEFAULT_LANGUAGE=it
TRANSLATION_PROVIDER=openai
TRANSLATION_BATCH_SIZE=25
OPENAI_API_KEY=your_openai_api_key
OPENAI_TRANSLATION_MODEL=gpt-5-miniRequired variables:
GOOGLE_SHEET_IDSpreadsheet ID from the Google Sheets URL.GOOGLE_SHEET_TITLEWorksheet tab name inside the spreadsheet, not the spreadsheet document title.TRANSLATIONS_DIRDirectory with locale JSON files such assrc/messages/it.json.TRANSLATIONS_SOURCE_DIRDirectory to scan for translation key usage, usuallysrc.DEFAULT_LANGUAGEExpected base locale for the project. It should exist locally.
Authentication options:
- Recommended:
GOOGLE_SERVICE_ACCOUNT_EMAIL+GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY - Alternative:
GOOGLE_SERVICE_ACCOUNT_JSON - Alternative:
GOOGLE_APPLICATION_CREDENTIALS
Optional variable:
TRANSLATIONS_PROJECT_ROOTUse this when you run the CLI from another directory and want it to operate on a different project root.TRANSLATIONS_FRAMEWORKWhich i18n libraryfind-newshould scan for. Supported values:next-intl,react-i18next(alias:i18next). Default:next-intl.TRANSLATIONS_KEY_SEPARATORSeparator used to map nested JSON to sheet keys. Default:.. Set it tofalse(alsonone,off,flat, or an empty value) to keep locale files flat and treat sheet keys literally, which is what you want when keys are whole sentences or contain dots.TRANSLATION_PROVIDEROptional translation driver fortranslate-missing. Supported values:openai,deepl,google. Default:openai.TRANSLATION_BATCH_SIZEOptional provider-agnostic batch size fortranslate-missing. Default:25.OPENAI_API_KEYRequired only whenTRANSLATION_PROVIDER=openai.OPENAI_TRANSLATION_MODELOptional model override for OpenAI. Default:gpt-5-mini.OPENAI_BASE_URLOptional API base URL override. Default:https://api.openai.com/v1.DEEPL_API_KEYRequired only whenTRANSLATION_PROVIDER=deepl.DEEPL_API_URLOptional DeepL API base URL. Default:https://api-free.deepl.com/v2.DEEPL_MODEL_TYPEOptional DeepL model type. Default:prefer_quality_optimized.GOOGLE_TRANSLATE_API_KEYRequired only whenTRANSLATION_PROVIDER=google.GOOGLE_TRANSLATE_API_URLOptional Google Translation Basic v2 endpoint. Default:https://translation.googleapis.com/language/translate/v2.GOOGLE_TRANSLATE_MODELOptional Google Translation model. Default:nmt.
Expected Local Structure
The package expects one JSON file per locale:
src/messages/
it.json
en.json
de.jsonNested JSON is supported. For example:
{
"nav": {
"profile": "Profilo"
}
}In Google Sheets this becomes:
key | it
nav.profile | ProfiloFlat locale files
react-i18next projects often keep one flat object per locale, where the key is the source phrase itself and may contain dots:
{
"Data di arrivo": "Arrivo",
"The birthday does not match the format d.m.Y.": "Formato non valido."
}Set TRANSLATIONS_KEY_SEPARATOR=false for those projects. Keys are then read from and written to the sheet verbatim, and download never splits them into nested objects. With nesting disabled, a nested object in a locale file is reported as an error instead of being flattened silently.
Expected Spreadsheet Structure
The worksheet tab must contain:
- a
keycolumn as the first header - one column per language, for example
it,en,de - one row per translation key
Example:
key | it | en
nav.profile | Profilo | Profile
breadcrumb.home | Home | HomeIf local JSON files contain languages that are missing in the sheet, the package adds those columns automatically during upload and find-new.
Commands
If you are not sure which command you need:
- The sheet changed and you want local files updated:
download - Local files changed and you want the sheet updated:
upload - You added new translation keys in code and they are missing in the sheet:
find-new - A locale file has missing translations and you want draft text generated automatically:
translate-missing
Quick summary:
| Command | What it is for |
| --- | --- |
| download | Pull translations from Google Sheets into local JSON files |
| upload | Push local JSON translations into Google Sheets |
| find-new | Find translation keys in code and add them to Google Sheets |
| translate-missing | Fill missing local translations with OpenAI, DeepL, or Google |
download
Copies translations from Google Sheets into your local locale files.
google-sheets-i18n downloadRun this when translators changed Google Sheets and you want the same data in local JSON files.
What happens:
- reads the sheet
- builds locale JSON files from the sheet data
- updates files in
TRANSLATIONS_DIR
Good to know:
- this command updates local files from the sheet
- if the sheet changed, your local files will change too
--dry-runonly shows what would be written
Options:
--dry-runShows which files would be written without changing them.--key-separator=<separator|false>OverridesTRANSLATIONS_KEY_SEPARATORfor a single run. Usefalseto write flat files.
upload
Copies local translations into Google Sheets.
google-sheets-i18n uploadRun this when local JSON files changed and you want those changes in Google Sheets.
Default behavior:
- adds missing keys as new rows
- does not overwrite existing sheet values
- does not fill existing empty cells unless you ask for it
What happens:
- reads local locale files
- adds missing keys to the sheet
- optionally fills empty cells in the sheet
- can fully update existing rows if you allow it
Good to know:
- plain
uploadis safe by default - it does not overwrite existing sheet values unless you ask it to
--fill-emptyonly fills blank cells--update-existingfully syncs local values into existing rows
Options:
--dry-runShows what would change without writing to Google Sheets.--fill-emptyFills only empty cells in existing rows.--update-existingFully syncs local values into existing rows, including non-empty cells.--key-separator=<separator|false>OverridesTRANSLATIONS_KEY_SEPARATORfor a single run.
Examples:
google-sheets-i18n upload --fill-empty
google-sheets-i18n upload --update-existingfind-new
Finds translation keys used in code and adds missing ones to Google Sheets.
google-sheets-i18n find-newRun this when developers added new translation keys in code and those keys are not yet in Google Sheets.
What it looks for depends on TRANSLATIONS_FRAMEWORK.
With next-intl (default):
useTranslations('namespace')getTranslations('namespace')- static calls like
t('title'), stored asnamespace.title
With react-i18next:
const {t} = useTranslation()andconst [t] = useTranslation(), including renames likeconst {t: tCard} = useTranslation()useTranslation().tandgetFixedT(...)i18n.t('key')andi18next.t('key')<Trans i18nKey="key" />- namespaces are ignored, because this tool maps one JSON file per locale, so keys are stored bare without a
namespace:prefix
Result:
- new keys found in source code are added to the sheet
- if a key exists locally, its values are written into language columns
- if a key does not exist locally, it is created in the sheet with empty values
What happens:
- scans your source files
- finds translation keys used in code
- adds missing keys to the sheet
Good to know:
- it works best with static keys
- dynamic keys are skipped on purpose
- if a key already exists locally, its current values are added to the new row
- if a key does not exist locally, the row is added with empty values
Options:
--dry-runShows how many keys would be added.--verboseShows every skipped dynamic key warning and a detailed list of missing local keys.--framework=<next-intl|react-i18next>OverridesTRANSLATIONS_FRAMEWORKfor a single run.
translate-missing
Generates draft translations for missing local values using DEFAULT_LANGUAGE as the source.
google-sheets-i18n translate-missing --language=enRun this when one locale has missing translations and you want draft text generated automatically.
Default behavior:
- translates only missing or empty values
- never overwrites existing non-empty translations
- uses the default locale JSON file as the source language
- writes results back into local JSON files
- with
--to-sheet, syncs only the translations generated in the current run into Google Sheets - uses the configured provider from
--providerorTRANSLATION_PROVIDER
What happens:
- uses
DEFAULT_LANGUAGEas the source locale - finds empty or missing values in target locales
- generates draft translations with the selected provider
- writes only the missing values
Good to know:
- this command fills gaps, not everything
- existing non-empty translations stay untouched
- placeholders like
{name}are checked after translation - machine translation should usually be reviewed by a human
--to-sheetsends only the newly generated values to Google Sheets
Options:
--dry-runShows how many keys would be translated without changing files.--verbosePrints the translated key list and skipped empty-source keys.--language=<locale>Target locale to translate. Repeat or comma-separate values like--language=en,de.--provider=<openai|deepl|google>Selects the translation driver for this run. Default:openai.--model=<model>OverridesOPENAI_TRANSLATION_MODELfor this run when--provider=openai.--batch-size=<number>OverridesTRANSLATION_BATCH_SIZEfor this run.--to-sheetAfter translation, fills matching empty cells in Google Sheets without overwriting existing sheet values.
Examples:
google-sheets-i18n translate-missing --language=en --dry-run
google-sheets-i18n translate-missing --language=en,de
google-sheets-i18n translate-missing --language=de --provider=openai --model=gpt-5-mini
google-sheets-i18n translate-missing --language=de --provider=deepl
google-sheets-i18n translate-missing --language=de --provider=google
google-sheets-i18n translate-missing --language=en --to-sheetNotes:
- placeholders such as
{name},{{name}},%s, and HTML/XML tags are validated after translation - existing translations are left untouched
- if you omit
--language, the command translates every existing locale file exceptDEFAULT_LANGUAGE --to-sheetrequires the same Google Sheets credentials asupload--to-sheetonly fills sheet cells for the keys translated in the current run, so it stays narrower than a fullupload --fill-emptyDeepLuppercases locale codes before calling the API, so locales likeen-GBbecomeEN-GBGoogleintegration uses the Cloud Translation Basic v2 REST API with an API key
Dynamic Keys
Static analysis works best for keys like:
const t = useTranslations('nav');
t('profile');Dynamic keys such as template strings with variables cannot be resolved safely:
t(`locales.${locale}`);These cases are skipped and reported as warnings. Re-run with --verbose to see the full list.
Recommended package.json Scripts
{
"scripts": {
"translations:download": "google-sheets-i18n download",
"translations:upload": "google-sheets-i18n upload --fill-empty",
"translations:sync": "google-sheets-i18n upload --update-existing",
"translations:find-new": "google-sheets-i18n find-new --dry-run --verbose",
"translations:translate:en": "google-sheets-i18n translate-missing --language=en",
"translations:translate:en:deepl": "google-sheets-i18n translate-missing --language=en --provider=deepl",
"translations:translate:en:sheet": "google-sheets-i18n translate-missing --language=en --to-sheet"
}
}Typical Workflow
- Translators update the Google Sheet.
- Run
google-sheets-i18n downloadto refresh local locale files. - Developers add new translation calls in code.
- Run
google-sheets-i18n find-newto add new keys to the sheet. - Optionally run
google-sheets-i18n translate-missing --language=ento draft local translations. - If you want the generated drafts in Google Sheets immediately, run
google-sheets-i18n translate-missing --language=en --to-sheet. - Run
google-sheets-i18n upload --fill-emptyto backfill any remaining missing sheet values from local JSON.
Troubleshooting
Missing required environment variables
If the CLI says that required environment variables are missing:
- make sure
.envexists in the project root - check that variable names match exactly
- if you run the command from another directory, set
TRANSLATIONS_PROJECT_ROOT
Sheet not found
If you get Sheet "..." not found:
GOOGLE_SHEET_IDmust be the spreadsheet document ID from the URLGOOGLE_SHEET_TITLEmust be the worksheet tab name inside that spreadsheet
Permission denied from Google
If Google rejects the request:
- verify that the Google Sheets API is enabled
- share the spreadsheet with the service account email
- make sure the private key and service account email belong to the same service account
Private key format issues
If authentication fails and you use inline credentials:
- keep the private key wrapped in quotes
- preserve
\nline breaks exactly as shown in the example - avoid trimming the
-----BEGIN PRIVATE KEY-----and-----END PRIVATE KEY-----lines
Dynamic keys were skipped
If find-new warns about skipped keys:
- static keys like
t('profile')are supported - dynamic expressions like
t(`locales.${locale}`)cannot be resolved safely - re-run with
--verboseto see every skipped location
find-new finds nothing
If find-new reports no keys even though the code clearly uses translations:
- check
TRANSLATIONS_FRAMEWORK— areact-i18nextproject scanned with the defaultnext-intlmatchers finds nothing --framework=react-i18nextoverrides it for one run
Download nested my flat keys
If download turned keys such as Format d.m.Y. into nested objects:
- set
TRANSLATIONS_KEY_SEPARATOR=falseand rundownloadagain
Machine translation failed
If translate-missing fails:
- verify that the API key for the selected provider is set
- check that the selected provider is one of
openai,deepl, orgoogle - if you use OpenAI, check that the selected model exists for your account
- try a smaller
TRANSLATION_BATCH_SIZE, for example10 - if a placeholder validation error appears, review the affected string before retrying
No files changed during download
If download finishes but nothing changes locally:
- the sheet values may already match your JSON files
- the worksheet may not contain language columns yet
- check that your language headers match local file names like
it.json->it
Duplicate keys in the sheet
If the CLI reports duplicate keys:
- the last matching row wins
- clean up duplicates in Google Sheets to avoid confusing sync behavior
Notes
- The package works with
next-intlstyle nested JSON files, and with flatreact-i18nextresources whenTRANSLATIONS_KEY_SEPARATOR=false. - Source scanning supports
.js,.jsx,.ts, and.tsx. - If duplicate keys exist in the sheet, the last row wins and a warning is shown.
- The package automatically reads
.envbefore validation, so it works well in CI and local development.
