plugins/lisa-phaser-copilot/skills/phaser-i18n/SKILL.md
This skill should be used when localizing a Phaser 4 game — a typed string catalog so no user-facing text is hardcoded, runtime locale switching that re-renders open text, interpolation/pluralization, and the BitmapText vs Text trade-offs (glyph coverage, RTL, CJK) localization forces. Use it when adding any player-facing string, building a language selector, or fixing missing-glyph/hardcoded-text issues. Pairs with phaser-services, phaser-accessibility, and phaser-asset-pipeline.
npx skillsauth add codyswanngt/lisa phaser-i18nInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
No user-facing string is hardcoded in a scene. All player-visible text comes from a typed string catalog keyed by typed constants, so a missing or misspelled key is a compile error and every string has a home for translation. The catalog is a small typed wrapper (no heavy dependency required); locale switching re-renders any open text. Announced strings ([[phaser-accessibility]]) and service messages ([[phaser-services]]) draw from the same catalog.
One module owns the locales and the lookup. The key type is derived from the default locale so every locale must cover the same keys:
// src/i18n/catalog.ts
const en = {
"menu.play": "Play",
"menu.settings": "Settings",
"hud.score": "Score: {score}",
"result.cleared": "Level {level} cleared!",
"lives": "{n} life|{n} lives", // singular|plural
} as const;
const es: Record<keyof typeof en, string> = {
"menu.play": "Jugar", "menu.settings": "Ajustes",
"hud.score": "Puntos: {score}", "result.cleared": "¡Nivel {level} superado!",
"lives": "{n} vida|{n} vidas",
};
export type StringKey = keyof typeof en;
const locales = { en, es } as const;
export type Locale = keyof typeof locales;
t() function: interpolation + pluralization// src/i18n/i18n.ts
let current: Locale = "en";
export function setLocale(l: Locale) { current = l; EventCenter.emit(GameEvent.LocaleChanged); }
export function getLocale() { return current; }
export function t(key: StringKey, params?: Record<string, string | number>): string {
let s = (locales[current][key] ?? locales.en[key]) as string; // fall back to en, never crash
if (s.includes("|") && params && "n" in params) // pick plural form
s = (Number(params.n) === 1 ? s.split("|")[0] : s.split("|")[1]);
return s.replace(/\{(\w+)\}/g, (_, k) => String(params?.[k] ?? `{${k}}`));
}
Usage is always t(...) with a typed key — never a raw string in a scene:
this.add.bitmapText(x, y, Font.UI, t("hud.score", { score: 0 }));
this.announce(t("result.cleared", { level })); // [[phaser-accessibility]] live region
The plural/interpolation rules are pure functions — put them in src/logic/**
so Vitest covers them ([[phaser-testing]]).
Changing language must update text that is already on screen. Emit a
LocaleChanged event on the EventsCenter ([[phaser-services]]); each scene with
visible text subscribes and re-applies t() to its labels, then removes the
listener in shutdown (the on/off discipline).
create() {
const refresh = () => this.scoreText.setText(t("hud.score", { score: this.score }));
EventCenter.on(GameEvent.LocaleChanged, refresh);
this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => EventCenter.off(GameEvent.LocaleChanged, refresh));
}
Persist the chosen locale via SaveService and apply it on boot; default from
navigator.language when there is no saved choice.
The performance advice "use BitmapText for hot text" (the official game-object-components skill)
collides with i18n: a bitmap font only contains the glyphs it was generated with.
Rule of thumb: BitmapText for high-churn numeric/short HUD with a covered glyph
set; Text for translated prose and any locale whose script the bitmap font does
not include. For RTL locales (Arabic/Hebrew), use canvas Text with
rtl: true/right alignment and lay out mirrored — BitmapText does not shape RTL.
t(StringKey, …) call — no inline literals in
scenes/entities (this mirrors the no-raw-string-keys discipline).src/logic/**; the catalog and t() live
in src/i18n/**.Verified by switching locale at runtime and confirming on-screen text updates
live (no reload), a missing key fails bun run typecheck, and pluralization unit
tests pass for n=0/1/many. For non-Latin locales, confirm glyphs render (no tofu
boxes) — that is the signal you need a per-script BMFont page or canvas Text.
</content>
tools
Configure the official SonarQube plugin + MCP as Lisa's single Sonar substrate across every supported coding agent. Installs/updates the SonarQube CLI, authenticates (browser login on a dev machine, or SONARQUBE_CLI_TOKEN headless), selects the Test Manager target, runs `sonar integrate <agent>` for each supported agent (Claude, Codex, Cursor, Copilot, Antigravity) and wires the MCP for OpenCode, then writes only non-secret policy to .lisa.config.json. Separate from the CI SonarCloud scan gate, which is unchanged.
tools
Configure the official SonarQube plugin + MCP as Lisa's single Sonar substrate across every supported coding agent. Installs/updates the SonarQube CLI, authenticates (browser login on a dev machine, or SONARQUBE_CLI_TOKEN headless), selects the Test Manager target, runs `sonar integrate <agent>` for each supported agent (Claude, Codex, Cursor, Copilot, Antigravity) and wires the MCP for OpenCode, then writes only non-secret policy to .lisa.config.json. Separate from the CI SonarCloud scan gate, which is unchanged.
tools
Configure the official SonarQube plugin + MCP as Lisa's single Sonar substrate across every supported coding agent. Installs/updates the SonarQube CLI, authenticates (browser login on a dev machine, or SONARQUBE_CLI_TOKEN headless), selects the Test Manager target, runs `sonar integrate <agent>` for each supported agent (Claude, Codex, Cursor, Copilot, Antigravity) and wires the MCP for OpenCode, then writes only non-secret policy to .lisa.config.json. Separate from the CI SonarCloud scan gate, which is unchanged.
tools
Configure the official SonarQube plugin + MCP as Lisa's single Sonar substrate across every supported coding agent. Installs/updates the SonarQube CLI, authenticates (browser login on a dev machine, or SONARQUBE_CLI_TOKEN headless), selects the Test Manager target, runs `sonar integrate <agent>` for each supported agent (Claude, Codex, Cursor, Copilot, Antigravity) and wires the MCP for OpenCode, then writes only non-secret policy to .lisa.config.json. Separate from the CI SonarCloud scan gate, which is unchanged.