Troubleshooting
Every entry here is a failure mode someone actually hit. Each one lists the symptom, why it happens, and the fix — most come down to one of three things: the phrase isn't in the project, the SDK never initialized, or a placeholder was spelled for the wrong context.
Nothing translates — every phrase stays in the source language
When everything shows source text, the SDK almost certainly failed to initialize. It fails soft by design (your UI must never break), so the only loud signal is the browser console — check it first. The usual causes, in order of likelihood:
- CORS. The SDK's first request is
authorize-project; if your API doesn't allowlist the app's origin, the browser blocks it and init dies right there. Remember an origin is the exact scheme + host + port —localhost:5173andlocalhost:5174are different origins, and dev servers drift ports when their usual one is taken. Allowlist every port you actually develop on. - Wrong credentials. A missing or mistyped project id / API key fails the same authorize call. The console shows the failing request and URL.
- You're looking at the base locale. In the base locale the "translation" is the source text — nothing visibly changes. Switch to a target locale to test.
# The SDK's first call is an authorize-project request.
# If the browser blocks it, NOTHING translates. Check the console for:
Access to fetch at 'https://your-api/api/authorize-project/...'
from origin 'http://localhost:5173' has been blocked by CORS policy
# Fix: allowlist the app's EXACT origin (scheme + host + port) on the API.
CORS_ALLOWED_ORIGINS=http://localhost:5173,https://app.example.com One phrase won't translate (but others do)
The phrase in your code is the lookup key — the phrase is the key. If a single phrase renders as source text while its neighbors translate, that key isn't in the project's catalog:
- It was never registered. Registration happens through token discovery, and only a write key can do it. If the phrase was born while the app ran on a read-only key, it was never sent anywhere — run the screen once in development with a write key, or add the phrase in the dashboard.
- The text changed. Any edit — a word, punctuation, even whitespace — makes it a new phrase with no translations yet. The old translations still exist under the old key. This bites content blocks especially: a
<Translate>block is tokenized from its markup, so editing the block's text or structure changes what's looked up, and the block quietly falls back to source text until the new form is registered and translated. - Machine translation hasn't caught up. A freshly registered phrase can lag its translations by a moment, and a locale outside the project's target list never gets one at all. Check the phrase in the dashboard: registered? translated into this locale?
Why don't missing phrases throw an error?
By design. In production a translation system must degrade to readable source text, never to a broken page — so an unregistered phrase renders its source and the SDK stays quiet. The flip side: on a read-only key a missing phrase is silent. Nothing is logged, nothing is registered, and the first you hear of it is a user seeing English. The discipline that prevents it is the key split — develop with a write key so every phrase your UI renders gets discovered, then ship read-only.
# Development — write key: new phrases are discovered,
# registered, and machine-translated as you build.
VITE_LANGSYS_API_KEY=write-key
# Production — read-only key: fetches the finished catalog only.
# Unregistered phrases silently render their source text.
VITE_LANGSYS_API_KEY=read-only-key My placeholder renders literally — "Hi %name%" or "Hi {name}"
- In
t()calls, placeholders are single-brace{name}, and interpolation only runs when you pass the params:t('Hello, {name}!', 'Greetings', { name }). - In
<Translate>/<Phrase>markup under React, Vue, or Svelte, spell placeholders%name%. The framework compiler consumes a bare{name}as an expression before the SDK ever sees it;%name%survives compilation and the SDK normalizes it back to{name}at capture. Plain{name}is fine in vanilla HTML, where there’s no template syntax to collide with. - Params missing? If no
paramsare supplied, the template renders as-is — that's the literal%name%you're seeing.
It works, then breaks after I switch locales back and forth
Update the SDK. Real, fixed bugs in this area: returning to the initial/SSR locale left the previous catalog active (fixed in langsys-js-typescript 0.2.2), and content-block Translate/Phrase consumers could hang forever on a server-seeded catalog (fixed in 0.2.3). If you see stale-locale strings or blocks that never resolve, check your installed version against the base SDK's changelog before debugging your own code.
Server-side: the page 500s instead of degrading
The PHP SDK's client throws when constructed without credentials — so a Laravel (or plain PHP) app with an empty LANGSYS_API_KEY fails at container resolution, before any translation runs. Set the credentials (or, for an app that must boot without them, bind a stand-in client the way the demo apps do). Once credentials exist, runtime API failures do degrade: an unreachable or refusing API renders source text and reports the exception — it never takes the page down.
Server-side: the first request hangs, then everything is fast
The PHP SDK's HTTP timeout is 30 seconds, and an API instance waking from idle can approach it on the very first call. That request either completes slowly (and warms the catalog cache — everything after is served from cache) or times out and renders source text. If your first page load after a quiet period is slow, this is why; it's the API warming up, not your integration.
Server-side: I rendered with a write key but the phrase isn't in the dashboard
Discovered phrases are queued during the request and sent after the response goes out — Laravel's terminable middleware. Refresh the dashboard a moment after the page loads, not during. On long-lived worker runtimes (Octane, FrankenPHP, RoadRunner) make sure you're on the SDK's provided integration — the plain PHP shutdown hook never fires between worker requests.
Server rendering: translations from one request bleed into another
The JS base SDK holds one locale's catalog at a time — it's a module-level singleton. A Node server rendering different locales for concurrent requests will race. Fetch the catalog per request and seed the client from it, the pattern in the Next.js, Nuxt and SvelteKit guides, or run one locale per process.
Still stuck? Open an issue on the SDK's repo — typescript, react, vue, svelte, php, or laravel — with the console output (or server log) of the failing request. Or write to [email protected].