Platform admin
Translation manager (edit any language)
The Translation manager at /admin/translations
(super-admin only) lets you edit the value of any string in any
shipped language and fill in missing strings — entirely from the UI,
with changes live on the next request. No redeploy, no editing JSON
files.
How it works — files stay the baseline, the DB wins
The app ships ~2,500 strings across 130+ languages as
lang/{locale}.json files. Those files remain the
canonical set of keys (the English source string is
the key) and the shipped defaults. Your edits are stored as
overrides in the translation_overrides
table and layered on top at runtime — an override wins for its
(locale, key). The original files are never modified, so
a deploy can ship new baseline translations without clobbering your
customisations, and resetting a string simply removes the override.
Overrides apply everywhere the app reads a string: the admin + customer
SPA, the visitor widget, and the marketing pages all resolve through
the same override-aware loader. The bare __() helper
(validation messages, emails) is override-aware too.
Editing a language
- Open
/admin/translations— each language shows a progress bar (translated / total), a missing count, and an edited (override) count. Click a language to open its editor. - Use the search box to find a string by its English source or current value, and the All / Missing / Edited tabs to narrow the list. Missing = a key with no translation in this language yet; Edited = a key you've overridden.
- Type the translation into the field and click Save. The change is live immediately.
- Reset (the ↺ button, shown on edited rows) removes your override and restores the shipped file value.
Auto-translate with DeepL
When a DeepL API key is configured, each language editor gets an
Auto-translate button that fills the language with
DeepL machine translation. Set the key from
Settings → System → DeepL translation (paste it and
Save — it is stored encrypted and never shown again, only a
“configured” indicator), or via the DEEPL_API_KEY
environment variable. Free keys end in :fx and use the
free endpoint, Pro keys use the paid endpoint — the app picks the
right host automatically, or set DEEPL_API_URL to override.
Without a key the button is hidden and you translate by hand.
DeepL covers ~30 languages, far fewer than the 130+ the app ships, so the button only appears for languages DeepL supports (e.g. German, Dutch, French, Spanish, Portuguese, Japanese, Chinese). Unsupported languages show a note instead — edit those manually.
- Auto-translate missing (default) translates only the strings still showing English — anything already translated by hand or a prior DeepL run is skipped, so you don't re-spend the quota. Tick everything to force a full re-translation pass.
- Translation runs in the background. In production refresh the editor after a moment to see the results; results land live but badged “Auto · unreviewed”.
- Review: use the Needs review tab to see the machine translations. Edit any string (your edit becomes a reviewed human override and is never overwritten by a later auto-pass), or click the ✓ to Approve it as-is. Approve all in the toolbar clears every unreviewed string in the language at once.
Safety rails: placeholders (:count, {name},
plural one|many forms) are protected and never translated;
human edits are never clobbered by a machine pass; and a per-run
character ceiling (DEEPL_MAX_CHARS_PER_RUN, default
200,000) protects a free-tier monthly quota from a single full pass.
A key is mandatory for real output. Without a configured DeepL key the
Auto-translate button is hidden and the endpoint refuses to run — it
will not write anything. If you ever see placeholder values shaped like
[nl] Open (an early build wrote these when run without a
key), clear them with the cleanup command — it deletes only the
[locale] … placeholders and leaves your real edits intact:
php artisan translations:purge-fake --dry-run # preview
php artisan translations:purge-fake # delete the [locale] fakes
php artisan translations:purge-fake --locale=nl # one language only
To reset a whole language back to the shipped translations — removing
every override for it (hand edits included), e.g. to start Dutch
over after a bad run — use translations:clear:
php artisan translations:clear nl --dry-run # preview
php artisan translations:clear nl # asks to confirm
php artisan translations:clear nl --force # no prompt
Marketing page copy is translatable too
The editor doesn't only cover the app's shipped strings — it also lists the editable marketing copy (the homepage hero, pricing, FAQ, and anything you customised in the marketing content editor). Those strings aren't shipped translation keys, so previously they fell back to English on the public site (you'd see a mixed-language hero). They now appear as normal rows here: translate or override them and the marketing page renders them in the visitor's language. URLs, icons, colors, and price glyphs are deliberately excluded — only human copy is offered.
SEO meta too. The per-page title and
meta description (the <title> tag,
the search-result snippet, and the Open Graph / Twitter title +
description social cards) are listed here as well, one row per marketing
page. Translate them and each page's meta renders in the visitor's
language. The {brand} token is preserved automatically — keep
it in your translation where you want your site name to appear. The
social-share image is an asset, not text, so it isn't translated.
Chat widget chrome too. The visitor widget's preset
starter-prompt chips and launcher label
(e.g. the SaaS preset's “What does it cost?” and “Ask about the product”)
are listed here as well. Translate them and the widget renders each in the
visitor's language — the widget follows the host page's
<html lang> (or the embed's data-locale),
so a single agent on a multilingual site speaks each page's language. A
chip you customised on the agent yourself is left as you wrote it unless
you add a translation for it here. The widget's fixed labels (Send, Close,
the handoff banners) are already part of the app's shipped strings above.
Sign-in pages too. The login / register / password-reset / 2FA / email-verification screens — including the decorative side-panel copy (“Welcome back to the conversations.”, the feature cards, “Back to site”) and each page's heading + subtitle — are translatable here and follow the visitor's language like everything else. They render in your active marketing theme's auth shell (Harvest, Aurora, or Prism), and all three are covered.
Notes
- You edit the values for the existing key set; you can't invent brand-new keys here, because only keys the app actually renders (the English source strings) are ever looked up. New keys appear when developers add new English strings to the source.
- English (
en) is the source language. You can still override English wording if you want to reword the product copy. - Octane note: the SPA, widget, and marketing surfaces reflect an
edit on the very next request (a shared-cache version bump
invalidates every worker). Server-side
__()in long-lived workers (validation/emails) picks the change up on the next worker that loads the locale.