UI Strings

UI Strings — Live App Translations

The UI strings system manages interface translations for all PsycatGames apps. Strings are organized by key, translated into 22 languages, and served to apps via a live API. Apps can pull updated translations without an app store release.


How It Works

  1. Keys are created in the content tool (e.g., generalPlay, settingsTitle)
  2. Each key has translations in 22 locales (en, de, fr, es, etc.)
  3. Apps fetch translations at runtime via the client API
  4. Apps bundle a build-time snapshot as offline fallback
  5. At runtime, the app checks the API for newer translations and caches locally

Client API Endpoint

GET https://content-api.vanilla.nl/v1/ui-strings?locale=de

Authentication: Same HMAC-SHA256 as all other endpoints (see API Integration).

Response:

{
  "strings": {
    "generalPlay": "Spielen",
    "generalBack": "Zurück",
    "settingsTitle": "Einstellungen"
  },
  "locale": "de",
  "hash": "a1b2c3d4e5f6g7h8"
}

ETag caching: The response includes an ETag header with the hash. Send If-None-Match: <hash> on subsequent requests — the server returns 304 Not Modified if translations haven’t changed, saving bandwidth.

Cache behavior: Responses are cached server-side for 4 hours. Changes to translations will be reflected within that window.


Flutter Integration

1. Add the API call

Fetch translations using the same HMAC client as other API calls:

Future<Map<String, String>> fetchUIStrings(String locale) async {
  final response = await vanillaApi.get(
    '/v1/ui-strings',
    queryParameters: {'locale': locale},
  );

  if (response.statusCode == 304) {
    return {}; // No changes, use cached version
  }

  final data = json.decode(response.body);
  return Map<String, String>.from(data['strings']);
}

2. Cache locally

Store the fetched strings + hash in shared preferences or local storage:

class UIStringCache {
  static const _prefsKey = 'ui_strings';
  static const _hashKey = 'ui_strings_hash';

  Future<void> save(Map<String, String> strings, String hash) async {
    final prefs = await SharedPreferences.getInstance();
    prefs.setString(_prefsKey, json.encode(strings));
    prefs.setString(_hashKey, hash);
  }

  Future<Map<String, String>?> load() async {
    final prefs = await SharedPreferences.getInstance();
    final stored = prefs.getString(_prefsKey);
    if (stored == null) return null;
    return Map<String, String>.from(json.decode(stored));
  }

  Future<String?> getHash() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(_hashKey);
  }
}

3. Use in the app

Create a simple lookup that falls back to bundled strings:

class AppStrings {
  final Map<String, String> _live;
  final Map<String, String> _bundled;

  AppStrings(this._live, this._bundled);

  String get(String key) => _live[key] ?? _bundled[key] ?? key;
}

4. Refresh on app start

Future<void> refreshUIStrings() async {
  final cache = UIStringCache();
  final currentHash = await cache.getHash();

  try {
    final response = await vanillaApi.get(
      '/v1/ui-strings',
      queryParameters: {'locale': currentLocale},
      headers: currentHash != null
          ? {'If-None-Match': currentHash}
          : null,
    );

    if (response.statusCode == 200) {
      final data = json.decode(response.body);
      final strings = Map<String, String>.from(data['strings']);
      await cache.save(strings, data['hash']);
    }
    // 304 = no changes, keep using cached version
  } catch (e) {
    // Network error, keep using cached/bundled version
  }
}

Build-Time Bundling

For offline fallback, export strings at build time and include them in the app bundle.

Flat JSON export (current format)

GET https://content-backend.vanilla.nl/v1/ui-strings/export?format=json

Returns:

{
  "en": { "generalPlay": "Play", "generalBack": "Back" },
  "de": { "generalPlay": "Spielen", "generalBack": "Zurück" }
}

Download this during your CI/CD build and include it as an asset.

ARB export (Flutter i18n standard)

GET https://content-backend.vanilla.nl/v1/ui-strings/export?format=arb&locale=en

Returns a proper ARB file for flutter gen-l10n:

{
  "@@locale": "en",
  "generalPlay": "Play",
  "greeting": "Hello {name}!",
  "@greeting": {
    "placeholders": { "name": { "type": "String" } }
  }
}

To export all locales, omit the locale parameter — you’ll get a ZIP file with app_en.arb, app_de.arb, etc.


Key Naming Convention

Keys use camelCase with a prefix indicating the feature area:

PrefixAreaExample
generalShared across appsgeneralPlay, generalBack, generalCancel
iapIn-app purchasesiapPremium, iapRestore
notificationsPush notificationsnotificationTitle1
multiplayerMultiplayer featuresmultiplayerPickCategory
superwallPaywall UIcountNewQuestions
App-specificPer-game stringscharadesSkip, imposterVote

Tags

Each key is tagged with its source sheet (e.g., general, cardgames, charades). Use tags to filter keys in the content tool and to identify which keys belong to which app.

Base tags shared across all apps: general, iap, multiplayer, notifications, superwall


Adding New Strings

  1. Go to UI Strings tab in the content tool
  2. Click Create Key
  3. Enter the key name (camelCase, e.g., myFeatureTitle)
  4. Add tags (e.g., your app name)
  5. Enter the English text
  6. Click Create
  7. Expand the row to add translations manually, or select keys and use Bulk AI Translate to auto-translate to all 22 locales
  8. The translations are immediately available via the API

Supported Locales

bg, cs, da, de, el, en, es, fi, fr, hr, hu, id, it, nb, nl, pl, pt, ro, ru, sv, tr, uk