URL-based i18n with configurable languages and a CLI for extracting translation keys.
The dictionary format is designed to be LLM-friendly. Just hand your dictionary.json to an AI and ask it to translate — done. No complex tooling, no translation service integrations, no manual key mapping.
npm install say-dictionary
# or
pnpm add say-dictionary// root.tsx or app entry
import { init } from 'say-dictionary';
import dictionary from './dictionary.json';
init(dictionary);import { say, getLanguage, setLanguage } from 'say-dictionary';
// Get translated text
say("Order Now"); // Returns "Order Now" or "Panta núna" based on URL
// ICU formatting (only when vars are provided)
say("You have {count, plural, one {# pizza} other {# pizzas}}", {
count: 2,
});
// Get current language from URL (null if no language prefix)
getLanguage(); // "is" or null
// Navigate to different language
setLanguage('is'); // Redirects to /is/current-path{
"Order Now": { "en": "Order Now", "is": "Panta núna" },
"Welcome": { "en": "Welcome", "is": "Velkomin" },
"You have {count, plural, one {# pizza} other {# pizzas}}": {
"is": "Þú átt {count, plural, one {# pizzur} other {# pizzur}}"
}
}Languages are automatically detected from the dictionary keys.
ICU message formatting is opt-in and only runs when you pass variables to
say(key, vars). If a translation is missing for the current language, the key
itself is treated as the ICU message.
Example:
say(
"Today is the {day, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} of March",
{ day: 3 }
);
// "Today is the 3rd of March"
say("Event on {date, date, ::MMMM d}", { date: new Date("2026-03-02") });
// "Event on March 2"The language is detected from the first path segment:
/is/about→ Icelandic (say()returns translation)/about→ No language prefix (say()returns the key itself)
For SSR frameworks, use ssrLang() to prevent hydration mismatch:
// app/[lang]/page.tsx
export default async function Page({ params }) {
const { lang } = await params;
return <Home lang={lang} />;
}
// app/page.tsx
"use client";
import { ssrLang, say } from 'say-dictionary';
export default function Home({ lang }: { lang?: string }) {
ssrLang(lang ?? null);
return <h1>{say("Hello")}</h1>;
}This is optional — only needed for SSR. Client-side apps (Vite, CRA) work without it.
Extract translation keys from your source files:
npx say-dictionary extract -l en,is -i ./app -o ./dictionary.jsonOptions:
-l, --lang- Comma-separated list of languages-i, --in- Source directory to scan-o, --out- Output dictionary file
Output dictionary.json:
{
"Order Now": { "en": "Order Now", "is": "" },
"Welcome": { "en": "Welcome", "is": "" }
}The first language (en) gets the key as its value. Hand this to an LLM to fill in the translations.
MIT