Documentation

Quickstart

Install @foony/translate, wrap your first string, and ship a translated build.

Foony Translate goes from an English-only React app to committed per-locale JSON in five steps. No manual keys, no runtime fetches: translations are plain JSON files in your repo.

1. Install

npm install @foony/translate

One package covers the React runtime (@foony/translate/react) and the foony-translate CLI.

2. Wire the provider

The SDK holds no locale state and loads nothing. Your app decides the locale and passes the parsed locale JSON to TranslateProvider. This works identically in SPAs, SSR, and Astro islands.

import { TranslateProvider } from '@foony/translate/react';
import fr from './translations/fr.json';
import de from './translations/de.json';

const translationsByLocale = { de, fr } as const;

export function App({ locale }: { locale: 'en' | 'fr' | 'de' }) {
  return (
    <TranslateProvider locale={locale} translations={translationsByLocale[locale]}>
      <Routes />
    </TranslateProvider>
  );
}

For English (the source locale) pass no translations. On any lookup miss the English source renders unchanged, so a half-translated app degrades to English, never to a broken key.

3. Wrap your strings

Static JSX goes in <T>. Dynamic values go in <Var> (or <Num>/<DateTime> for formatted values) and never leave your app. Conditional copy goes in <Branch>/<Plural> so every variant is translated.

import { T, Var, Plural } from '@foony/translate/react';

<T>
  Welcome back, <Var name="user">{user.name}</Var>! You have{' '}
  <Plural n={keys} one="one API key" other="several API keys" />.
</T>

For plain strings (placeholders, aria-labels, titles) use useT:

const t = useT();
<input placeholder={t('Search projects')} aria-label={t('Search projects')} />

Identity is a content hash of the serialized source plus translator context, so identical strings anywhere in your codebase share one translation and nothing needs a manual key.

4. Create a project and a key

  1. Sign up and create a project. Target locales are not set here: they come from the locales array in foony-translate.json, so they live in code review like everything else.
  2. On the project’s API keys tab create a prod key and put it in your CI secrets or shell as FOONY_TRANSLATE_API_KEY. The key never lives in the config file.

5. Translate

npx foony-translate init        # writes foony-translate.json
export FOONY_TRANSLATE_API_KEY=...
npx foony-translate translate   # scan, upload, wait, write translations/<locale>.json

The CLI parses your sources with a real AST, uploads only entries it has not seen before, waits for the LLM worker, and writes one JSON file per locale. Commit those files: they are the translations.

Re-running translate on unchanged content is free. Only new or edited source words count against your monthly allotment.

Keep it enforced in CI

check verifies every scanned entry has a translation in every locale, offline, with no API key:

npx foony-translate check

It exits non-zero and prints file:line for anything missing, so a PR that adds a string without running translate fails before review. See the CLI reference for a full workflow.

Next

  • Components: every runtime API, and the rules that keep grammar correct.
  • CLI: config reference, commands, and the CI recipe.