yapyak compiles
into the module that renders it. The source string is the key. On save, the yapyak plugin extracts every
call, writes an empty stub to each locale file, and fills the stub through a translator when one is configured.
Read the reference for the project's framework before writing setup or component code:
| Framework | Reference |
|---|
| React with Vite | references/react.md |
| React Router | references/react-router.md |
| TanStack Start | references/tanstack-start.md |
| Vue with Vite | references/vue.md |
| Nuxt | references/nuxt.md |
| Svelte with Vite | references/svelte.md |
| SvelteKit | references/sveltekit.md |
| Astro | references/astro.md |
Every
diagnostic, its cause, and its fix:
references/diagnostics.md
Install
Requirements: Node.js 22.22+, Vite 8+. Frameworks: React 19+, Vue 3.4+, Svelte 5+, Astro 7+, Nuxt 4.1+, React Router 7.9+, SvelteKit 3+, TanStack Start 1.168+.
| Framework | Install | Build config | Processor |
|---|
| React | npm install yapyak @yapyak/vite @yapyak/react
| plugins: [react(), yapyak()]
| from |
| React Router | add | plugins: [reactRouter(), yapyak()]
| |
| TanStack Start | add | plugins: [tanstackStart(), react(), yapyak()]
| |
| Vue | npm install yapyak @yapyak/vite @yapyak/vue
| plugins: [vue(), yapyak()]
| from |
| Nuxt | npx nuxi module add yapyak
| modules: ['@yapyak/nuxt']
| from |
| Svelte | npm install yapyak @yapyak/vite @yapyak/svelte
| plugins: [svelte(), yapyak()]
| from |
| SvelteKit | add | plugins: [sveltekit(), yapyak()]
| |
| Astro | npm install yapyak @yapyak/astro
| from @yapyak/astro/integration
| from |
Steps, in order:
- Install the packages. Add a translator package when the project translates with a model: , , , , or .
- Register the plugin in the build config, after the framework plugin. is a named export of .
- Create at the project root with the processor for the framework:
ts
import { defineConfig } from 'yapyak/config';
import { react } from '@yapyak/react/processor';
export default defineConfig({
processors: [react()]
});
- Add and to in . Nuxt adds the types on its own.
- Add the first target locale: . This creates and regenerates the type.
- Add to . It holds the orphan cache; the locale files are the committed source of truth.
Register one processor per framework in the project. The built-in parser reads
,
,
,
,
,
,
, and
without one.
Writing text
Wrap every user-facing string in
from
. Write the sentence in the source language exactly as the reader sees it; it is the key.
ts
// ✓ The sentence is the key
t('Save changes');
// ✗ A synthetic key has no source string to translate
t('common.buttons.save');
Call
inside the component or function that renders the text. A module-scope call evaluates once and never follows the active locale (
).
ts
// ✓ Reads the active locale on every render
function SaveButton() {
return t('Save changes');
}
// ✗ Evaluates once when the module loads
const label = t('Save changes');
Write the source as a static string literal and the params as an inline object literal. The compiler reads neither from a variable.
ts
// ✓ Static source, inline params
t('Hi {name}', { name });
// ✗ Template literal (YAP0002)
t(`Hi ${name}`);
// ✗ Variable source
t(label);
// ✗ Params in a variable (YAP0006)
t('Hi {name}', params);
Never concatenate around
→ put the moving part in a placeholder.
ts
// ✓ One message, one placeholder
t('{count} items in your cart', { count });
// ✗ Word order differs per language
t('You have') + ' ' + count + ' ' + t('items');
Call
in attribute positions too:
,
,
placeholder={t('Search')}
. The attribute name is part of the call-site context the translator receives.
Placeholders
Write
in the source and pass the value under the same name. TypeScript derives the required keys from the source string.
| Form | Example | Value type |
|---|
| Plain | | , rendered as its string form |
| Number | t('Total {n, number, integer}', { n })
| ; styles , , , |
| Date | t('Updated {when, date, long}', { when })
| or millisecond timestamp; styles , , , |
| Time | t('Doors open at {when, time, short}', { when })
| or millisecond timestamp; styles , , , |
| Plural | t('{count, plural, one {# item} other {# items}}', { count })
| |
| Ordinal | t('{place, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}', { place })
| |
| Select | t('{role, select, admin {Admin panel} other {Reader view}}', { role })
| ; a non-string is coerced with |
Name a placeholder with letters, digits, and underscores, starting with a letter or underscore (
). Non-ASCII letters are allowed.
- Write an branch ().
- Write inside a plural branch for the formatted count.
- Write , for an exact value; exact matches are tried before categories.
- Name plural branches with the CLDR keywords , , , , , ().
- Write only the branches the source language uses, and for English; the translator adds the branches each target locale needs.
- Nest freely: a branch is a full message and holds placeholders, plurals, and selects.
- Split a message that has become unreadable into two calls and let the component pick.
- Type a value as a union literal so every legal value has a branch.
Format a list with
outside the message and pass the result in as a placeholder. Format relative time with
outside the message.
Homonyms
Split two calls with the same source and different meanings with
:
ts
t.as('action', 'Open');
t.as('status', 'Open');
- Write the context as a short literal label (, , , ), never a variable ().
- Use for every occurrence of that source in the source file. Mixing and in one file is an error ().
- Use plain when the source has one meaning. A lone context is an error ().
Forced locale
- Write for the translation in a forced locale regardless of the active one. The locale argument is typed against .
- Chain
t.in('sv').as('action', 'Open')
or t.as('action').in('sv', 'Open')
when both apply.
- Complete the chain in one expression. A chain stored in a variable is an error ().
Rich text
Write tags inside the source and bind each tag to a handler in
from the framework's adapter package.
ts
t('Read our <link>privacy policy</link> for details.');
t('Line one<br/>line two');
| Tag | Form | Handler |
|---|
| Pair | | Receives the wrapped children |
| Void | | Receives nothing |
- Start a tag name with a letter and continue with letters or digits.
- Name a tag by meaning (, , ), never by HTML element; the element belongs in the handler.
- Bind a handler for every tag in the source.
- Never write attributes on a tag → put the in the handler.
- Walk the structure with from , which returns , when does not fit.
Take the handler shape from the framework's reference file.
Formatting outside messages
Format values outside a message with
from
. Every call reads the active locale.
| Method | Backed by | Note |
|---|
format.number(value, options)
| | requires ; requires |
format.dateTime(value, options)
| | and presets, or field options |
format.list(items, options)
| | Run each item through first |
format.relativeTime(value, unit, options)
| | Negative is past; gives |
format.displayName(code, type, options)
| | is , , , , , or |
| | Returns a scoped to a forced locale |
- Use for a value outside a message and an ICU sub-format for a value inside one.
- Type a currency code as from ; narrow user input with .
- Label a locale switcher with
format.in(value).displayName(value, 'language')
, never with a call per language.
- An unsupported currency, unit, time zone, or display-name code renders a fallback instead of throwing and reports , , , or .
The active locale
A locale is the BCP 47 tag of a file in
:
for
,
for
. Write tags as
lowercase-Titlecase-UPPERCASE
(
). The
type is the union of the files on disk and refreshes on
.
The active locale resolves in this order, first value wins: a persisted choice, a detected locale when
is on, then
.
| Export from | Use |
|---|
| The active locale, outside components |
| Switch and persist; ignored with a warning for an unknown value () |
| Every added locale, for a switcher |
| The locale when no persisted or detected locale matches |
| Type guard for a canonical string |
| Normalizes casing, returns or |
getLocaleFallbackChain(locale)
| ['zh-Hant-TW', 'zh-Hant', 'zh']
; is not appended |
| or for |
Read and switch the locale inside a component through the framework binding; never call
or
there.
| Framework | Binding |
|---|
| React | const [locale, setLocale] = useLocale()
; |
| Vue, Nuxt | ref (, or in a template); ref |
| Svelte, SvelteKit | ; |
| Astro | in frontmatter; the middleware binds it per request |
Narrow a string from a URL, a form, or a cookie with
or
before
.
Every field is optional. Export the result of
from
.
| Field | Default | Sets |
|---|
| | The language the source strings are written in. Set it only when it differs from |
| | The locale when no persisted or detected locale matches |
| | The directory of locale files, relative to the project root |
| | Directories, globs, or to scan; replaces the default |
| tests, , , , , , | Files to skip; replaces the default. Extend with [...DEFAULT_EXCLUDE, '**/*.vendor.ts']
|
| | One per framework |
| none | The translator that fills stubs |
| | New strings in one save above which the save loop writes stubs but skips the translator. turns auto-translation off |
preserveTranslationsOnSourceEdit
| without a translator, with one | Keep the translation when a source string is edited in place |
| | Where the active locale is stored between page loads |
| | Match on the server and in the browser on a first visit |
| | Keep and in step with the active locale on every change |
| Processor | Option | Use |
|---|
| | React Server Components: only files subscribe to the locale |
nuxt({ explicitImports: true })
| | Another global exists in the project |
| , , | none | |
Persistence
| Value | Options | Fits |
|---|
| | Kiosk, development build, locale carried by another system. A reload returns to |
| { type: 'cookie', name: 'locale', secure: false }
| SSR apps. The adapter reads the cookie per request; on the server buffers a |
| { type: 'local-storage', key: 'locale' }
| Client-only apps. Under SSR the first render uses and flips after hydration |
| { type: 'url', match: /[?&]lang=(?<locale>[^&]+)/ }
| Locale in the path (, the default reads the first segment) or in a query string |
- Under , switch by navigating. is a no-op and reports .
- Set on the cookie in production only; on plain HTTP the browser refuses the write.
Fixed-locale builds
- Set in the build config, never in . Every compiles to the literal of that locale and the runtime disappears from the bundle.
- Drive a CI matrix from an environment variable. With the variable unset, the build is a normal multi-locale build.
- Pass an added locale; the build throws otherwise.
| Framework | Where |
|---|
| Vite (React, Vue, Svelte, React Router, SvelteKit, TanStack Start) | yapyak({ fixedLocale: process.env.YAPYAK_LOCALE })
in |
| Astro | yapyak({ fixedLocale: process.env.YAPYAK_LOCALE })
in |
| Nuxt | yapyak: { fixedLocale: process.env.YAPYAK_LOCALE }
in |
Locale files
One JSON file per target locale in
. The source language has no file. Entries nest under the source file's path, then the source string:
json
{
"src/components/save-button.tsx": {
"Save changes": "Spara ändringar"
}
}
A homonym nests its contexts under the source:
json
{
"src/components/dialog.tsx": {
"Open": {
"action": "Öppna",
"status": "Öppen"
}
}
}
- An empty string is an empty stub. The runtime renders the source string for it.
- Hand-edit a value to correct or replace a translation. The running app updates through HMR.
- Keep every placeholder, tag, and ICU structure of the source in the translation (, , , , , , ). Placeholder names stay fixed; their position in the sentence is free.
- Never edit a key → change the source string in the component; the compiler rewrites the key.
- Never add an entry by hand → write the call; the save loop or creates the stub.
- Renames and moves of source files restore translations from the orphan cache in on the next save. Deleting loses nothing committed.
- Orphans stay in the locale file until removes them.
Fill empty stubs by hand only when the project has no translator or the user asks for it.
Translators
A translator fills empty stubs on save and in
. Requests go from the developer's machine to the provider. Set one in
:
ts
import { defineConfig } from 'yapyak/config';
import { anthropic } from '@yapyak/anthropic';
export default defineConfig({
translator: anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
glossary: { cart: { sv: 'kundvagn' } },
voice: 'Concise and friendly'
})
});
| Package | Factory | Key | Defaults that differ |
|---|
| | | |
| | | Extras , , ; other OpenAI-compatible endpoints through and |
| | | |
| | none | endpoint: 'http://localhost:11434/api/generate'
, , , , |
| | the signed-in CLI | , ; no HTTP options; falls back to the CLI's |
| Shared option | Default | Sets |
|---|
| per provider | Model identifier |
| none | One or two sentences of tone guidance |
| | Record<source term, Record<locale, translation>>
; pinned brand names, domain terms, and words the model gets wrong |
| | sends the source string only; adds the surrounding code |
| , under | Existing translations sent per request as style reference |
| | Sampling temperature |
| per provider | Output token cap |
| | Per-request timeout in milliseconds |
| | Retries on 408, 429, 5xx, and network failures |
| | Source strings per request |
| | Parallel requests |
| | Extra HTTP headers |
| per provider | API URL |
- The context is sent at every level.
- A voice or glossary change affects new translations only. Run or
yapyak retranslate "<source>"
to propagate it.
- Pick the translator by environment when CI has no signed-in CLI:
process.env.CI ? anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }) : claudeCode()
.
A custom translator is
createTranslator({ id, translate })
from
:
translate({ items, signal, sourceLocale, targetLocales })
returns one object per item, in item order, keyed by every target locale.
- Forward to the request.
- Throw , , , , ,
TranslatorInvalidResponseError
, or from .
- Retry inside the callback. yapyak runs it once.
CLI
Run through the package manager:
,
,
. Every command except
walks the source files first, so a
call added without the dev server running is still found.
| Command | Does | Exit code |
|---|
yapyak add <locale> [<locale>...]
| Creates the locale file, fills it through the translator, regenerates the type. Several tags in one run share requests | non-zero on an unrecognized tag |
yapyak translate [<locale>] [--force]
| Fills empty stubs. re-translates every entry and overwrites hand-edits | without a translator |
yapyak retranslate "<source>" [--locale <code>] [--as <ctx>] [--file <path>]
| Re-translates one source string; the flags compose | when nothing matches |
| Coverage per locale | when a translation is missing |
| Missing translations, compile-time diagnostics, ICU mismatches | on any finding |
| Lists orphans; deletes them | |
yapyak export [<locale>...] [--out <path>] [--split]
| Snapshot keyed by locale, then path, then source | non-zero on an unknown tag |
| Versions for a bug report | |
Boolean flags accept
:
.
and
strip color.
CI order:
with the provider key in the environment, then
, then the build.
never calls the translator.
Working in a project
- Write the call in the component. With the dev server running, the save writes the stub and the translator fills it. Without it, the CLI finds the call.
- Run before finishing. Fix every reported with references/diagnostics.md.
- Run when reports missing translations and the project has a translator. Fill the stubs by hand when it has none.
- Commit the locale files with the code. They are the source of truth; is a cache.
- Run after removing components, and once the list is right.
Diagnostics an agent most often triggers:
| Code | Cause | Fix |
|---|
| Template literal source | |
| , , | Params do not match the placeholders | Match the keys to the in the source |
| Params in a variable | Inline object literal |
| No branch | Add |
| and for one source in one file | on every occurrence |
| Chain stored in a variable | Complete or inline |
| Branch name is not a CLDR keyword | , , , , , , or |
| at module scope | Move the call into the function that renders it |
| on the server without an adapter | Register the SSR adapter from the framework reference |