Languages (i18n)

AllGame Club is written in English and translated into German. Every visible string in the portal and in the games comes from a language file, so adding a language or a game never means editing PHP views or portal code.

How the language is chosen

In this order, the first match wins (App\Core\I18n::boot(), called once per request in index.php):

  1. ?lang=de in the URL. The choice is stored in the agc_lang cookie for a year and, for logged-in members, in users.locale.
  2. The X-Locale header, which the game SDK sends with every API call.
  3. The agc_lang cookie.
  4. The member's saved preference (Account → Profile → Language).
  5. The host name: i18n.hosts in the config maps a domain to a locale ('allgameclub.de' => 'de'); any other .de domain also opens in German.
  6. The browser's Accept-Language header.
  7. i18n.default (en).

The language switcher (header, mobile menu, footer, auth pages) links to the same page with ?lang=. Once a locale has its own domain, set i18n.urls ('de' => 'https://allgameclub.de') and the switcher and the hreflang links use the domains instead.

Configuration (config/config.php):

$config['i18n']['locales'] = ['en', 'de'];
$config['i18n']['hosts'] = ['allgameclub.de' => 'de'];
// $config['i18n']['urls'] = ['en' => 'https://allgameclub.com', 'de' => 'https://allgameclub.de'];

Portal strings

lang/{locale}/{group}.php returns a nested array. Keys are addressed as group.key (nested: account.profile.title).

GroupCovers
commonbuttons, pills, flash labels, relative times, date formats, month/day names
navheader, user menu, footer
home, games, auth, account, community, membership, developers, errorsone group per area
pagesstudio pages, FAQ, contact, legal page titles
contentteam bios, timeline, values, jobs, press facts, FAQ entries (arrays, merged by index in App\Services\Team)
validationvalidator messages and field labels
mailsubjects and bodies of transactional mails
playplay-session messages
gameserverEnglish sentence → translation for game server modules

In views and controllers:

__('games.show.left_today', ['time' => '12:00'])   // placeholders: {time}
__('common.members_online', ['count' => 3])        // "one|many" forms picked by count
lang()                                             // 'de'
game_text($game, 'tagline')                        // the manifest's i18n block, English otherwise
category_label('board'), unit_label('points')

English is the source. A key missing in German falls back to English; a key missing everywhere renders as the key itself (and is logged when app.debug is on). Long pages are translated as whole view copies: app/Views/pages/privacy.de.php wins over privacy.php when the locale is de (View::make tries {template}.{locale}.php first).

Emails are rendered in the recipient's language: wrap the send call in I18n::withLocale($user['locale'] ?? null, fn() => Mailer::send(...)).

Game strings

Every game ships lang/en.json and lang/de.json next to its entry file:

{
  "ui":  { "title": "Nine Mills", "start_game": "Start game" },
  "app": { "your_move": "Your move", "players": "{count} player|{count} players" }
}
  • Markup: <button data-i18n="ui.start_game">Start game</button>. The SDK replaces the text once the dictionary is loaded; the inline English stays as the fallback. Also data-i18n-html, data-i18n-placeholder, data-i18n-title, data-i18n-aria-label.
  • Code: ag.t('app.your_move'), ag.t('app.players', { count: 3 }), AllGame.tr('key', 'Fallback text'), AllGame.i18n.get('game.steps') for arrays. ag.locale is the active locale.
  • Manifest: translated name, tagline, description and controls go into game.json:
"i18n": { "de": { "name": "Vier gewinnt", "tagline": "…", "description": "…", "controls": ["…"] } }

Run php bin/console.php games:sync after changing the manifest; the portal reads the block through game_text().

  • Server modules keep their English messages in code. $fail('not_found', 'Match not found.') passes the sentence through I18n::literal(), which looks it up in lang/{locale}/gameserver.php. Add new sentences there.
  • Fairground games (games/_kirmes): the framework translates its own UI through kirmes.* keys and the game's config (title, tagline, goal, steps, scoring, HUD labels) through game.* keys. The config is the English fallback, so a fairground game only needs lang/de.json; copy games/_kirmes/lang/de.example.json.

The SDK loads lang/{locale}.json (and lang/en.json as fallback) before AllGame.init() resolves, sends X-Locale on every API call, and the play shell passes &lang= to the iframe.

Adding a language

  1. Create lang/{xx}/ by copying lang/en/ and translating (keep the keys; plural forms stay one|many).
  2. Add xx to i18n.locales; optionally a host in i18n.hosts.
  3. Add lang/{xx}.json to each game and an i18n.{xx} block to each game.json.
  4. Add lang/{xx}/gameserver.php for the server messages.
  5. Copy the legal pages: app/Views/pages/{terms,privacy,imprint}.{xx}.php.

App\Core\I18n::name('xx') holds the display names of common locales.

Last updated October 12, 2026.