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.