Experience Platform
Translations

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() hook

Source 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||plural syntax (pipe-pipe separator), expanded to CLDR plural categories per language
  • Nesting: Dot-notation keys for organisation (checkout.title, checkout.button)
  • Versioning: $meta.version tracks 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

  1. On mount, fetches translations for current locale from Experience API
  2. Caches in memory + localStorage (offline support)
  3. t(key, params?) looks up key, replaces {{param}} interpolations
  4. setLocale(code) fetches new locale, updates all rendered strings
  5. 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.json

On push to main:

  1. Action reads the source strings file
  2. Sends to Experience API
  3. Experience diffs against stored version
  4. New/changed strings queued for LLM translation
  5. Suggestions appear in dashboard for review

API Endpoints

MethodPathAuthDescription
POST/api/v1/translations/webhookWebhook SecretGitHub webhook receiver
POST/api/v1/translations/:appId/uploadBearerManual string upload
GET/api/v1/translations/:appId/:localeAPI Key/BearerGet translations for locale
GET/api/v1/translations/:appId/localesAPI Key/BearerList available locales
PUT/api/v1/translations/:appId/:locale/:keyBearerOverride a translation
POST/api/v1/translations/:appId/configureBearerSet target languages
GET/api/v1/translations/:appId/statusBearerTranslation progress
GET/api/v1/translations/:appId/recommendBearerRecommended languages

See the full API Reference and Data Models for schemas.


© 2026 Shell Technology. All rights reserved.