Skip to content

Commit 7807ad5

Browse files
committed
Merge branch 'translation'
2 parents 194a457 + b1380ee commit 7807ad5

File tree

14 files changed

+214
-34
lines changed

14 files changed

+214
-34
lines changed

public/locales/en.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"menu": {
3+
"title": "Qaboom!",
4+
"1_player": "1 Player",
5+
"2_player": "2 Player",
6+
"high_scores": "High Scores",
7+
"credits": "Credits",
8+
"level": "Level {level}"
9+
},
10+
"credits": {
11+
"title": "Credits",
12+
"concept_development": "Concept & Development",
13+
"content_coord": "Content & Coordination",
14+
"music": "Music",
15+
"support": "Support",
16+
"graphic_design": "Arcade Machine Graphic Design",
17+
"building": "Arcade Machine Building",
18+
"funded_by": "Funded by",
19+
"part_of": "Part of quantum-arcade.org by"
20+
},
21+
"game": {
22+
"go": "GO!",
23+
"lvl": "Lvl {level}",
24+
"hold": "HOLD",
25+
"boom_text": {
26+
"small": "boom.",
27+
"medium": "BOOM!",
28+
"large": "QABOOM!",
29+
"all_clear": "ALL QLEAR!"
30+
},
31+
"enter_name": "Enter your name"
32+
}
33+
}

public/locales/fake.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"menu": {
3+
"title": "FFFFFF",
4+
"1_player": "FFFFFF",
5+
"2_player": "FFFFFF",
6+
"high_scores": "FFFFFF",
7+
"credits": "FFFFFF",
8+
"level": "{level} FFFFFF"
9+
},
10+
"credits": {
11+
"title": "FFFFFF",
12+
"concept_development": "FFFFFF",
13+
"content_coord": "FFFFFF",
14+
"music": "FFFFFF",
15+
"support": "FFFFFF",
16+
"graphic_design": "FFFFFF",
17+
"building": "FFFFFF",
18+
"funded_by": "FFFFFF",
19+
"part_of": "FFFFFF"
20+
},
21+
"game": {
22+
"go": "FFFFFF",
23+
"lvl": "FFF {level}",
24+
"hold": "FFFFFF",
25+
"boom_text": {
26+
"small": "ffff.",
27+
"medium": "FFFF!",
28+
"large": "FFFFFF!",
29+
"all_clear": "FFF FFFF!"
30+
},
31+
"enter_name": "FFFFFF"
32+
}
33+
}

src/Game.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { randomInt } from "mathjs";
1111
import { campaign } from "./levels";
1212
import Credits from "./nodes/Credits";
1313
import HighScoreScreen from "./nodes/HighScoreScreen";
14+
import { switchLanguage } from "./i18n";
1415

1516
const IDLE_TIMEOUT = 60 * 1000;
1617

@@ -35,6 +36,9 @@ export default class Game {
3536
if (e.key === inputs.refresh) {
3637
window.location.reload();
3738
}
39+
if (e.key === inputs.translate) {
40+
switchLanguage(app.stage);
41+
}
3842
});
3943

4044
const background = new Background();

src/i18n.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { AbstractText, Container } from "pixi.js";
2+
3+
let languages: string[] = [];
4+
const strings: Record<string, any> = {};
5+
let currentLang: string = "en";
6+
7+
export async function loadLanguages(langs: string[], startLang: string) {
8+
languages = langs;
9+
for (const code of languages) {
10+
const response = await fetch(`/locales/${code}.json`, {
11+
cache: "no-cache",
12+
});
13+
strings[code] = await response.json();
14+
}
15+
currentLang = startLang;
16+
}
17+
18+
export async function switchLanguage(node: any) {
19+
const currentIndex = languages.findIndex((code) => getCurrentLang() === code);
20+
const code = languages[(currentIndex + 1) % languages.length];
21+
changeLanguage(code);
22+
refreshI18nText(node);
23+
}
24+
25+
/**
26+
* Set a translatable element with the provided tag
27+
* that will be re-translated when `refreshI18Text` is called.
28+
*/
29+
export function setI18nKey(
30+
node: AbstractText,
31+
key: string,
32+
format: (t: string) => string = (t) => t
33+
) {
34+
(node as any).i18nKey = key;
35+
(node as any).i18nFormat = format;
36+
translate(node);
37+
}
38+
39+
/**
40+
* Retranslate all translateable text in the current language.
41+
*/
42+
export function refreshI18nText(node: any) {
43+
if (node instanceof Container) {
44+
for (let child of node.children) {
45+
refreshI18nText(child);
46+
}
47+
}
48+
if (node instanceof AbstractText) {
49+
translate(node);
50+
}
51+
}
52+
53+
export function getCurrentLang() {
54+
return currentLang;
55+
}
56+
57+
export function changeLanguage(newLang: string) {
58+
if (!Object.keys(strings).includes(newLang)) {
59+
throw new Error(`Unknown code: ${newLang}`);
60+
}
61+
currentLang = newLang;
62+
}
63+
64+
export function setFormat(node: AbstractText, format: (t: string) => string) {
65+
(node as any).i18nFormat = format;
66+
translate(node);
67+
}
68+
69+
function translate(node: AbstractText) {
70+
const key = (node as any).i18nKey;
71+
if (!key) {
72+
return;
73+
}
74+
const format = (node as any).i18nFormat;
75+
node.text = format(getNestedKey(strings[currentLang], key.split(".")));
76+
}
77+
78+
function getNestedKey(object: Record<string, any>, path: string[]) {
79+
const [head, ...tail] = path;
80+
const value = object[head];
81+
if (tail.length === 0) {
82+
return value;
83+
}
84+
if (value == null) {
85+
return undefined;
86+
}
87+
return getNestedKey(value, tail);
88+
}

src/inputs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface PlayerInput {
99
export const inputs = {
1010
refresh: "r",
1111
pause: "p",
12+
translate: "t",
1213
player1: {
1314
left: "a",
1415
right: "d",

src/main.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import Game from "./Game";
2+
import { loadLanguages } from "./i18n";
23

34
(async () => {
4-
// TODO pass in configuration and translations.
5+
const urlParams = new URLSearchParams(window.location.search);
6+
await loadLanguages(["en"], urlParams.get("lang") || "en");
7+
// load "fake" language for testing translations
8+
// await loadLanguages(["en", "fake"], urlParams.get("lang") || "en");
59
const game = new Game();
610
await game.start();
711
})();

src/nodes/Board.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { playScoreSound, playSound } from "../audio";
2727
import QubitPair from "./QubitPair";
2828
import GatePiece from "./GatePiece";
2929
import { animate } from "motion";
30+
import { setI18nKey } from "../i18n";
3031

3132
export const startingCell = new Point(Math.floor(BOARD_WIDTH / 2 - 1), 0);
3233
const RECT_MARGIN = PIECE_RADIUS / 2;
@@ -305,22 +306,22 @@ export default class Board extends GameNode {
305306
if (!(this.current instanceof MeasurementPiece)) {
306307
return;
307308
}
308-
const boomText = this.isEmpty()
309-
? "ALL QLEAR!"
309+
const boomKey = this.isEmpty()
310+
? "all_clear"
310311
: count >= 15
311-
? `QABOOM!`
312+
? "large"
312313
: count >= 6
313-
? "BOOM!"
314-
: "boom.";
314+
? "medium"
315+
: "small";
315316
const text = new HTMLText({
316-
text: boomText,
317317
style: new TextStyle({
318318
fontSize: Math.min(BOARD_WIDTH * CELL_SIZE * 1.25, 60 + 5 * count),
319319
fontFamily: TEXT_FONT,
320320
fontWeight: "bold",
321321
fill: theme.colors.primary,
322322
}),
323323
});
324+
setI18nKey(text, `game.boom_text.${boomKey}`);
324325
// Have the text above everything else
325326
text.zIndex = 100;
326327
text.anchor = { x: 0.5, y: 0.5 };

src/nodes/Countdown.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import GameNode from "./GameNode";
44
import { playScoreSound, playSound } from "../audio";
55
import { delay } from "../util";
66
import { pulse } from "../animations";
7+
import { setI18nKey } from "../i18n";
78

89
export default class Countdown extends GameNode {
910
text: HTMLText;
@@ -33,7 +34,7 @@ export default class Countdown extends GameNode {
3334
pulse(this.text, 1.5);
3435
await delay(1000 * (5 / 8));
3536
}
36-
this.text.text = "GO!";
37+
setI18nKey(this.text, "game.go");
3738
playSound("levelUp");
3839
await delay(1000);
3940
}

src/nodes/Credits.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import imaginaryLogoPath from "../assets/img/imaginary-logo.png";
77
import mpiLogoPath from "../assets/img/mpi-logo.png";
88
import { inputs } from "../inputs";
99
import { LayoutContainer } from "@pixi/layout/components";
10+
import { setI18nKey } from "../i18n";
1011

1112
export default class Credits extends GameNode {
1213
onFinish?: () => void;
@@ -40,7 +41,6 @@ export default class Credits extends GameNode {
4041

4142
const titleText = new HTMLText({
4243
layout: true,
43-
text: "Credits",
4444
style: {
4545
align: "center",
4646
fill: theme.colors.primary,
@@ -49,6 +49,7 @@ export default class Credits extends GameNode {
4949
fontSize: 72,
5050
},
5151
});
52+
setI18nKey(titleText, "credits.title");
5253
credits.addChild(titleText);
5354

5455
const columns = new Container({
@@ -77,28 +78,30 @@ export default class Credits extends GameNode {
7778
columns.addChild(column1);
7879
columns.addChild(column2);
7980

80-
column1.addChild(this.drawCredit("Concept & Development", "Nat Alison"));
81+
column1.addChild(
82+
this.drawCredit("credits.concept_development", "Nat Alison")
83+
);
8184

8285
column1.addChild(
8386
this.drawCredit(
84-
"Content & Coordination",
87+
"credits.content_coord",
8588
"Christian Stussak",
8689
"Andreas Matt",
8790
"Skye Rothstein"
8891
)
8992
);
90-
column1.addChild(this.drawCredit("Music", "Landis Seralian"));
93+
column1.addChild(this.drawCredit("credits.music", "Landis Seralian"));
9194

92-
column2.addChild(this.drawCredit("Support", "Karla Schön", "Oliver Schön"));
9395
column2.addChild(
94-
this.drawCredit("Arcade Machine Graphic Design", "Eric Londaits")
96+
this.drawCredit("credits.support", "Karla Schön", "Oliver Schön")
97+
);
98+
column2.addChild(
99+
this.drawCredit("credits.graphic_design", "Eric Londaits")
95100
),
96-
column2.addChild(
97-
this.drawCredit("Arcade Machine Building", "Retr-O-Mat")
98-
);
101+
column2.addChild(this.drawCredit("credits.building", "Retr-O-Mat"));
99102
}
100103

101-
drawCredit(title: string, ...names: string[]) {
104+
drawCredit(key: string, ...names: string[]) {
102105
const credit = new Container({
103106
layout: {
104107
display: "flex",
@@ -108,7 +111,6 @@ export default class Credits extends GameNode {
108111
});
109112
const titleText = new HTMLText({
110113
layout: true,
111-
text: title,
112114
style: {
113115
align: "center",
114116
fill: theme.colors.primary,
@@ -117,6 +119,7 @@ export default class Credits extends GameNode {
117119
fontSize: 40,
118120
},
119121
});
122+
setI18nKey(titleText, key);
120123
credit.addChild(titleText);
121124

122125
for (let [_i, name] of names.entries()) {
@@ -169,7 +172,6 @@ export default class Credits extends GameNode {
169172
const ministryLogoTexture = await Assets.load(ministryLogoPath);
170173
const titleText = new HTMLText({
171174
layout: true,
172-
text: "Funded by",
173175
style: {
174176
align: "center",
175177
fill: "black",
@@ -178,6 +180,7 @@ export default class Credits extends GameNode {
178180
fontSize: 40,
179181
},
180182
});
183+
setI18nKey(titleText, "credits.funded_by");
181184
fundedBy.addChild(titleText);
182185
fundedBy.addChild(this.drawImage(ministryLogoTexture, 250));
183186
logos.addChild(fundedBy);
@@ -194,7 +197,6 @@ export default class Credits extends GameNode {
194197
const mpiLogoTexture = await Assets.load(mpiLogoPath);
195198
const titleText2 = new HTMLText({
196199
layout: true,
197-
text: "Part of quantum-arcade.org by",
198200
style: {
199201
align: "center",
200202
fill: "black",
@@ -203,6 +205,7 @@ export default class Credits extends GameNode {
203205
fontSize: 40,
204206
},
205207
});
208+
setI18nKey(titleText2, "credits.part_of");
206209
partOf.addChild(titleText2);
207210
const partOfSprites = new Container({
208211
layout: {

src/nodes/HighScoreScreen.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import GameNode from "./GameNode";
44
import { HEIGHT, TEXT_FONT, theme, WIDTH } from "../constants";
55
import { getScores, type Score } from "../storage";
66
import { inputs } from "../inputs";
7+
import { setI18nKey } from "../i18n";
78

89
// The high score screen displayed in the main menu
910
// TODO: deduplicate from `ScoreScreen`
@@ -27,14 +28,14 @@ export default class HighScoreScreen extends GameNode {
2728
);
2829
const scores = getScores();
2930
const highScoresLabel = new HTMLText({
30-
text: "High Scores",
3131
style: {
3232
fill: theme.colors.primary,
3333
fontFamily: TEXT_FONT,
3434
fontSize: 72,
3535
fontWeight: "bold",
3636
},
3737
});
38+
setI18nKey(highScoresLabel, "menu.high_scores");
3839
highScoresLabel.position.y = -300;
3940
highScoresLabel.anchor = { x: 0.5, y: 0.5 };
4041
this.view.addChild(highScoresLabel);

0 commit comments

Comments
 (0)