Translations
A translation management system where builders provide strings in their native language, and the platform translates them into target languages with LLM-powered suggestions — all managed via the dashboard UI.
Key principles:
- All AI translation is billed via Norman tokens — charged to the app owner's token balance
- Auto-translation on new language detection: when a user visits with an unsupported language, a job automatically translates (if builder enabled). Translations served from API immediately; PR created on app repo for bundle update.
- Norman tokens power ALL AI features across the Experience Platform (translations, app analysis, etc.)
Flow
Builder writes code with string keys
│
▼
Commits source strings JSON to repo
│
▼
GitHub webhook fires on push to main
│
▼
Experience Service receives webhook
│
▼
Diff against previous version
(only new/changed strings)
│
▼
LLM Translation Engine (via Norman)
generates suggestions for each target language
│
▼
Suggestions appear in Dashboard UI
│
▼
Builder/translator reviews in UI:
- Approve suggestion ✅
- Edit and approve ✏️
- Reject and write manually ❌
│
▼
Approved translations stored in DB
│
▼
Available via API:
GET /api/v1/translations/:appId/:locale
│
▼
Consumed by useTranslation() hookSource String Format
Builders maintain a JSON file in their repo:
{
"$meta": {
"locale": "en",
"version": "1.2.0"
},
"strings": {
"common.save": "Save",
"common.cancel": "Cancel",
"common.loading": "Loading...",
"checkout.title": "Your Cart",
"checkout.button": "Complete Purchase",
"checkout.empty": "Your cart is empty",
"checkout.item_count": "{{count}} item||{{count}} items",
"profile.greeting": "Hello, {{name}}!",
"profile.member_since": "Member since {{date}}",
"errors.not_found": "Page not found",
"errors.generic": "Something went wrong. Please try again."
}
}Features
- Interpolation:
{{variable}}placeholders are preserved across translations - Plurals:
singular||pluralsyntax (pipe-pipe separator), expanded to CLDR plural categories per language - Nesting: Dot-notation keys for organisation (
checkout.title,checkout.button) - Versioning:
$meta.versiontracks string file version
Translation Memory
The platform maintains a translation memory per app:
- Previously translated strings are NOT re-translated unless the source changes
- If a source string is edited, only that string is re-translated
- Translation memory can be shared across apps (common strings like "Save", "Cancel")
- Memory reduces LLM costs and ensures consistency
Dashboard Translation UI
┌─────────────────────────────────────────────────────────────────┐
│ Translations — Monet.live │
│ │
│ Target Languages: 🇫🇷 French 🇩🇪 German 🇪🇸 Spanish [+ Add] │
│ │
│ Progress: FR 92% ████████░ DE 87% ███████░░ ES 64% ██████░░░ │
│ │
│ ┌─────────────┬──────────────────┬──────────────────┬────────┐ │
│ │ Key │ English (source) │ French │ Status │ │
│ ├─────────────┼──────────────────┼──────────────────┼────────┤ │
│ │ common.save │ Save │ Enregistrer │ ✅ │ │
│ │ common.back │ Go back │ Retourner ✨ │ 🔍 │ │
│ │ checkout. │ Complete │ — │ ⏳ │ │
│ │ button │ Purchase │ │ │ │
│ └─────────────┴──────────────────┴──────────────────┴────────┘ │
│ │
│ ✅ = Approved 🔍 = Suggested (needs review) ⏳ = Pending │
│ ✨ = LLM suggestion │
│ │
│ Click any cell to edit inline. Ctrl+Enter to approve. │
└─────────────────────────────────────────────────────────────────┘Dashboard Features
- Side-by-side source vs translation
- Inline editing (click to edit, Ctrl+Enter to approve)
- Status indicators: approved, suggested, pending, overridden
- Bulk approve (approve all suggestions at once)
- Filter: show only pending, only changed, search by key
- Language recommendation based on user analytics (geo data from tracking)
React Integration
import { useTranslation } from '@shellapps/experience-react';
function CheckoutPage() {
const { t, locale, setLocale, locales } = useTranslation();
return (
<div>
<h1>{t('checkout.title')}</h1>
<p>{t('checkout.item_count', { count: 3 })}</p>
<button>{t('checkout.button')}</button>
{/* Language switcher */}
<select value={locale} onChange={e => setLocale(e.target.value)}>
{locales.map(l => (
<option key={l.code} value={l.code}>{l.name}</option>
))}
</select>
</div>
);
}How useTranslation Works
- On mount, fetches translations for current locale from Experience API
- Caches in memory + localStorage (offline support)
t(key, params?)looks up key, replaces{{param}}interpolationssetLocale(code)fetches new locale, updates all rendered strings- Falls back to source language if a translation is missing
See the React SDK for full details.
GitHub Integration
Install a webhook or GitHub Action:
# .github/workflows/translate.yml
name: Sync Translations
on:
push:
branches: [main]
paths: ['src/strings/**']
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shellapps/translate-action@v1
with:
api-key: ${{ secrets.EXPERIENCE_API_KEY }}
source-path: src/strings/en.jsonOn push to main:
- Action reads the source strings file
- Sends to Experience API
- Experience diffs against stored version
- New/changed strings queued for LLM translation
- Suggestions appear in dashboard for review
API Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /api/v1/translations/webhook | Webhook Secret | GitHub webhook receiver |
POST | /api/v1/translations/:appId/upload | Bearer | Manual string upload |
GET | /api/v1/translations/:appId/:locale | API Key/Bearer | Get translations for locale |
GET | /api/v1/translations/:appId/locales | API Key/Bearer | List available locales |
PUT | /api/v1/translations/:appId/:locale/:key | Bearer | Override a translation |
POST | /api/v1/translations/:appId/configure | Bearer | Set target languages |
GET | /api/v1/translations/:appId/status | Bearer | Translation progress |
GET | /api/v1/translations/:appId/recommend | Bearer | Recommended languages |
See the full API Reference and Data Models for schemas.