## Description:
I’ve expanded MLS (Multi-Language Support) coverage in the game.
All in-game elements except for chat messages are now translatable.
## Please complete the following:
- [ ] I have added screenshots for all UI updates
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:
aotumuri
This commit is contained in:
Aotumuri
2025-05-15 18:53:26 +09:00
committed by GitHub
parent 005f0ddd03
commit 2fe99dc2ce
9 changed files with 280 additions and 57 deletions
+73
View File
@@ -319,5 +319,78 @@
"saving_for_mirv": "[P1] is saving up to launch a MIRV.",
"mirv_ready": "[P1] has enough gold to launch a MIRV!"
}
},
"build_menu": {
"desc": {
"atom_bomb": "Small explosion",
"hydrogen_bomb": "Large explosion",
"mirv": "Huge explosion, only targets selected player",
"missile_silo": "Used to launch nukes",
"sam_launcher": "Defends against incoming nukes",
"warship": "Captures trade ships, destroys ships and boats",
"port": "Sends trade ships to allies to generate gold",
"defense_post": "Increase defenses of nearby borders",
"city": "Increase max population"
},
"not_enough_money": "Not enough money"
},
"win_modal": {
"died": "You died",
"your_team": "Your team won!",
"other_team": "{team} team has won!",
"you_won": "You Won!",
"other_won": "{player} has won!",
"exit": "Exit Game",
"keep": "Keep Playing"
},
"leaderboard": {
"title": "Leaderboard",
"hide": "Hide",
"rank": "Rank",
"player": "Player",
"owned": "Owned",
"gold": "Gold",
"troops": "Troops"
},
"player_info_overlay": {
"type": "Type",
"bot": "Bot",
"nation": "Nation",
"player": "Player",
"team": "Team",
"d_troops": "Defending troops",
"a_troops": "Attacking troops",
"gold": "Gold",
"ports": "Ports",
"cities": "Cities",
"missile_launchers": "Missile launchers",
"sams": "SAMs",
"health": "Health",
"attitude": "Attitude"
},
"relation": {
"hostile": "Hostile",
"distrustful": "Distrustful",
"neutral": "Neutral",
"friendly": "Friendly",
"default": "Default"
},
"control_panel": {
"pop": "Pop",
"gold": "Gold",
"troops": "Troops",
"workers": "Workers",
"attack_ratio": "Attack Ratio"
},
"player_panel": {
"gold": "Gold",
"troops": "Troops",
"traitor": "Traitor",
"embargo": "Embargo against you",
"nuke": "Nukes sent by them to you",
"start_trade": "Start trading",
"stop_trade": "Stop trading",
"yes": "Yes",
"no": "No"
}
}
+73
View File
@@ -319,5 +319,78 @@
"saving_for_mirv": "[P1]はMIRVを発射するために貯金している。",
"mirv_ready": "[P1]はMIRVを発射できるだけのお金を持っている!"
}
},
"build_menu": {
"desc": {
"atom_bomb": "小規模な爆発",
"hydrogen_bomb": "大規模な爆発",
"mirv": "指定したプレイヤーのみを狙う超大規模な爆発",
"missile_silo": "核ミサイルの発射に使用される",
"sam_launcher": "飛来する核ミサイルを迎撃する",
"warship": "貿易船を捕獲し、敵の船やボートを破壊する",
"port": "同盟国に貿易船を送り、ゴールドを生成する",
"defense_post": "近くの国境の防御を強化する",
"city": "最大人口を増加させる"
},
"not_enough_money": "資金不足"
},
"win_modal": {
"died": "あなたは死んでしまった",
"your_team": "あなたのチームの勝利!",
"other_team": "{team}チームが勝利しました。",
"you_won": "勝利!",
"other_won": "{player}の勝利!",
"exit": "ゲームから退出",
"keep": "観戦する"
},
"leaderboard": {
"title": "ランキング",
"hide": "隠す",
"rank": "順位",
"player": "プレイヤー",
"owned": "領土",
"gold": "ゴールド",
"troops": "兵士"
},
"player_info_overlay": {
"type": "タイプ",
"bot": "ボット",
"nation": "国家",
"player": "プレイヤー",
"team": "チーム",
"d_troops": "防衛兵士数",
"a_troops": "攻撃兵士数",
"gold": "資金",
"ports": "港",
"cities": "都市",
"missile_launchers": "ミサイル格納庫",
"sams": "SAM",
"health": "体力",
"attitude": "態度"
},
"relation": {
"hostile": "敵対的",
"distrustful": "不信",
"neutral": "中立",
"friendly": "友好的",
"default": "デフォルト"
},
"control_panel": {
"pop": "人口",
"gold": "資金",
"troops": "兵士",
"workers": "労働者",
"attack_ratio": "攻撃比率"
},
"player_panel": {
"gold": "資金",
"troops": "兵士",
"traitor": "裏切り者かどうか",
"embargo": "あなたへの禁輸措置があるかどうか",
"nuke": "相手からあなたへの核攻撃数",
"start_trade": "貿易を開始",
"stop_trade": "貿易を停止",
"yes": "はい",
"no": "いいえ"
}
}
+27 -12
View File
@@ -10,6 +10,7 @@ import atomBombIcon from "../../../../resources/images/NukeIconWhite.svg";
import portIcon from "../../../../resources/images/PortIcon.svg";
import samlauncherIcon from "../../../../resources/images/SamLauncherIconWhite.svg";
import shieldIcon from "../../../../resources/images/ShieldIconWhite.svg";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import { Cell, PlayerActions, UnitType } from "../../../core/game/Game";
import { TileRef } from "../../../core/game/GameMap";
@@ -22,6 +23,7 @@ interface BuildItemDisplay {
unitType: UnitType;
icon: string;
description?: string;
key?: string;
countable?: boolean;
}
@@ -30,56 +32,65 @@ const buildTable: BuildItemDisplay[][] = [
{
unitType: UnitType.AtomBomb,
icon: atomBombIcon,
description: "Small explosion",
description: "build_menu.desc.atom_bomb",
key: "unit_type.atom_bomb",
countable: false,
},
{
unitType: UnitType.MIRV,
icon: mirvIcon,
description: "Huge explosion, only targets selected player",
description: "build_menu.desc.mirv",
key: "unit_type.mirv",
countable: false,
},
{
unitType: UnitType.HydrogenBomb,
icon: hydrogenBombIcon,
description: "Large explosion",
description: "build_menu.desc.hydrogen_bomb",
key: "unit_type.hydrogen_bomb",
countable: false,
},
{
unitType: UnitType.Warship,
icon: warshipIcon,
description: "Captures trade ships, destroys ships and boats",
description: "build_menu.desc.warship",
key: "unit_type.warship",
countable: true,
},
{
unitType: UnitType.Port,
icon: portIcon,
description: "Sends trade ships to allies to generate gold",
description: "build_menu.desc.port",
key: "unit_type.port",
countable: true,
},
{
unitType: UnitType.MissileSilo,
icon: missileSiloIcon,
description: "Used to launch nukes",
description: "build_menu.desc.missile_silo",
key: "unit_type.missile_silo",
countable: true,
},
// needs new icon
{
unitType: UnitType.SAMLauncher,
icon: samlauncherIcon,
description: "Defends against incoming nukes",
description: "build_menu.desc.sam_launcher",
key: "unit_type.sam_launcher",
countable: true,
},
{
unitType: UnitType.DefensePost,
icon: shieldIcon,
description: "Increase defenses of nearby borders",
description: "build_menu.desc.defense_post",
key: "unit_type.defense_post",
countable: true,
},
{
unitType: UnitType.City,
icon: cityIcon,
description: "Increase max population",
description: "build_menu.desc.city",
key: "unit_type.city",
countable: true,
},
],
@@ -347,7 +358,9 @@ export class BuildMenu extends LitElement implements Layer {
class="build-button"
@click=${() => this.onBuildSelected(item)}
?disabled=${!this.canBuild(item)}
title=${!this.canBuild(item) ? "Not enough money" : ""}
title=${!this.canBuild(item)
? translateText("build_menu.not_enough_money")
: ""}
>
<img
src=${item.icon}
@@ -355,8 +368,10 @@ export class BuildMenu extends LitElement implements Layer {
width="40"
height="40"
/>
<span class="build-name">${item.unitType}</span>
<span class="build-description">${item.description}</span>
<span class="build-name">${translateText(item.key)}</span>
<span class="build-description"
>${translateText(item.description)}</span
>
<span class="build-cost" translate="no">
${renderNumber(
this.game && this.game.myPlayer() ? this.cost(item) : 0,
+12 -5
View File
@@ -1,5 +1,6 @@
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import { GameView } from "../../../core/game/GameView";
import { ClientID } from "../../../core/Schemas";
@@ -210,7 +211,9 @@ export class ControlPanel extends LitElement implements Layer {
>
<div class="hidden lg:block bg-black/30 text-white mb-4 p-2 rounded">
<div class="flex justify-between mb-1">
<span class="font-bold">Pop:</span>
<span class="font-bold"
>${translateText("control_panel.pop")}:</span
>
<span translate="no"
>${renderTroops(this._population)} /
${renderTroops(this._maxPopulation)}
@@ -224,7 +227,9 @@ export class ControlPanel extends LitElement implements Layer {
>
</div>
<div class="flex justify-between">
<span class="font-bold">Gold:</span>
<span class="font-bold"
>${translateText("control_panel.gold")}:</span
>
<span translate="no"
>${renderNumber(this._gold)}
(+${renderNumber(this._goldPerSecond)})</span
@@ -234,8 +239,9 @@ export class ControlPanel extends LitElement implements Layer {
<div class="relative mb-4 lg:mb-4">
<label class="block text-white mb-1" translate="no"
>Troops: <span translate="no">${renderTroops(this._troops)}</span> |
Workers:
>${translateText("control_panel.troops")}:
<span translate="no">${renderTroops(this._troops)}</span> |
${translateText("control_panel.workers")}:
<span translate="no">${renderTroops(this._workers)}</span></label
>
<div class="relative h-8">
@@ -266,7 +272,8 @@ export class ControlPanel extends LitElement implements Layer {
<div class="relative mb-0 lg:mb-4">
<label class="block text-white mb-1" translate="no"
>Attack Ratio: ${(this.attackRatio * 100).toFixed(0)}%
>${translateText("control_panel.attack_ratio")}:
${(this.attackRatio * 100).toFixed(0)}%
(${renderTroops(
this.game?.myPlayer()?.troops() * this.attackRatio,
)})</label
+8 -7
View File
@@ -1,6 +1,7 @@
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { translateText } from "../../../client/Utils";
import { EventBus, GameEvent } from "../../../core/EventBus";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { ClientID } from "../../../core/Schemas";
@@ -242,7 +243,7 @@ export class Leaderboard extends LitElement implements Layer {
? ""
: "hidden"}"
>
Leaderboard
${translateText("leaderboard.title")}
</button>
<div
class="leaderboard ${this._leaderboardHidden ? "hidden" : ""}"
@@ -252,7 +253,7 @@ export class Leaderboard extends LitElement implements Layer {
class="leaderboard-close-button"
@click=${() => this.hideLeaderboard()}
>
Hide
${translateText("leaderboard.hide")}
</button>
<button
class="leaderboard-top-five-button"
@@ -266,11 +267,11 @@ export class Leaderboard extends LitElement implements Layer {
<table>
<thead>
<tr>
<th>Rank</th>
<th>Player</th>
<th>Owned</th>
<th>Gold</th>
<th>Troops</th>
<th>${translateText("leaderboard.rank")}</th>
<th>${translateText("leaderboard.player")}</th>
<th>${translateText("leaderboard.owned")}</th>
<th>${translateText("leaderboard.gold")}</th>
<th>${translateText("leaderboard.troops")}</th>
</tr>
</thead>
<tbody>
+46 -15
View File
@@ -1,5 +1,6 @@
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import {
PlayerProfile,
@@ -158,6 +159,21 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
}
}
private getRelationName(relation: Relation): string {
switch (relation) {
case Relation.Hostile:
return translateText("relation.hostile");
case Relation.Distrustful:
return translateText("relation.distrustful");
case Relation.Neutral:
return translateText("relation.neutral");
case Relation.Friendly:
return translateText("relation.friendly");
default:
return translateText("relation.default");
}
}
private renderPlayerInfo(player: PlayerView) {
const myPlayer = this.myPlayer();
const isFriendly = myPlayer?.isFriendly(player);
@@ -171,24 +187,25 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
const relation =
this.playerProfile?.relations[myPlayer.smallID()] ?? Relation.Neutral;
const relationClass = this.getRelationClass(relation);
const relationName = Relation[relation];
const relationName = this.getRelationName(relation);
relationHtml = html`
<div class="text-sm opacity-80">
Attitude: <span class="${relationClass}">${relationName}</span>
${translateText("player_info_overlay.attitude")}:
<span class="${relationClass}">${relationName}</span>
</div>
`;
}
let playerType = "";
switch (player.type()) {
case PlayerType.Bot:
playerType = "Bot";
playerType = translateText("player_info_overlay.bot");
break;
case PlayerType.FakeHuman:
playerType = "Nation";
playerType = translateText("player_info_overlay.nation");
break;
case PlayerType.Human:
playerType = "Player";
playerType = translateText("player_info_overlay.player");
break;
}
@@ -208,33 +225,44 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
${player.name()}
</div>
${player.team() != null
? html`<div class="text-sm opacity-80">Team: ${player.team()}</div>`
? html`<div class="text-sm opacity-80">
${translateText("player_info_overlay.team")}: ${player.team()}
</div>`
: ""}
<div class="text-sm opacity-80">Type: ${playerType}</div>
<div class="text-sm opacity-80">
${translateText("player_info_overlay.type")}: ${playerType}
</div>
${player.troops() >= 1
? html`<div class="text-sm opacity-80" translate="no">
Defending troops: ${renderTroops(player.troops())}
${translateText("player_info_overlay.d_troops")}:
${renderTroops(player.troops())}
</div>`
: ""}
${attackingTroops >= 1
? html`<div class="text-sm opacity-80" translate="no">
Attacking troops: ${renderTroops(attackingTroops)}
${translateText("player_info_overlay.a_troops")}:
${renderTroops(attackingTroops)}
</div>`
: ""}
<div class="text-sm opacity-80" translate="no">
Gold: ${renderNumber(player.gold())}
${translateText("player_info_overlay.gold")}:
${renderNumber(player.gold())}
</div>
<div class="text-sm opacity-80" translate="no">
Ports: ${player.units(UnitType.Port).length}
${translateText("player_info_overlay.ports")}:
${player.units(UnitType.Port).length}
</div>
<div class="text-sm opacity-80" translate="no">
Cities: ${player.units(UnitType.City).length}
${translateText("player_info_overlay.cities")}:
${player.units(UnitType.City).length}
</div>
<div class="text-sm opacity-80" translate="no">
Missile launchers: ${player.units(UnitType.MissileSilo).length}
${translateText("player_info_overlay.missile_launchers")}:
${player.units(UnitType.MissileSilo).length}
</div>
<div class="text-sm opacity-80" translate="no">
SAMs: ${player.units(UnitType.SAMLauncher).length}
${translateText("player_info_overlay.sams")}:
${player.units(UnitType.SAMLauncher).length}
</div>
${relationHtml}
</div>
@@ -256,7 +284,10 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
<div class="text-sm opacity-80">${unit.type()}</div>
${unit.hasHealth()
? html`
<div class="text-sm opacity-80">Health: ${unit.health()}</div>
<div class="text-sm opacity-80">
${translateText("player_info_overlay.health")}:
${unit.health()}
</div>
`
: ""}
</div>
+18 -9
View File
@@ -7,6 +7,7 @@ import donateTroopIcon from "../../../../resources/images/DonateTroopIconWhite.s
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
import targetIcon from "../../../../resources/images/TargetIconWhite.svg";
import traitorIcon from "../../../../resources/images/TraitorIconWhite.svg";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import {
AllPlayers,
@@ -254,7 +255,9 @@ export class PlayerPanel extends LitElement implements Layer {
<div class="grid grid-cols-2 gap-2">
<div class="flex flex-col gap-1">
<!-- Gold -->
<div class="text-white text-opacity-80 text-sm px-2">Gold</div>
<div class="text-white text-opacity-80 text-sm px-2">
${translateText("player_panel.gold")}
</div>
<div
class="bg-opacity-50 bg-gray-700 rounded p-2 text-white"
translate="no"
@@ -265,7 +268,7 @@ export class PlayerPanel extends LitElement implements Layer {
<div class="flex flex-col gap-1">
<!-- Troops -->
<div class="text-white text-opacity-80 text-sm px-2">
Troops
${translateText("player_panel.troops")}
</div>
<div
class="bg-opacity-50 bg-gray-700 rounded p-2 text-white"
@@ -278,26 +281,32 @@ export class PlayerPanel extends LitElement implements Layer {
<!-- Attitude section -->
<div class="flex flex-col gap-1">
<div class="text-white text-opacity-80 text-sm px-2">Traitor</div>
<div class="text-white text-opacity-80 text-sm px-2">
${translateText("player_panel.traitor")}
</div>
<div class="bg-opacity-50 bg-gray-700 rounded p-2 text-white">
${other.isTraitor() ? "Yes" : "No"}
${other.isTraitor()
? translateText("player_panel.yes")
: translateText("player_panel.no")}
</div>
</div>
<!-- Embargo -->
<div class="flex flex-col gap-1">
<div class="text-white text-opacity-80 text-sm px-2">
Embargo against you
${translateText("player_panel.embargo")}
</div>
<div class="bg-opacity-50 bg-gray-700 rounded p-2 text-white">
${other.hasEmbargoAgainst(myPlayer) ? "Yes" : "No"}
${other.hasEmbargoAgainst(myPlayer)
? translateText("player_panel.yes")
: translateText("player_panel.no")}
</div>
</div>
<!-- Stats -->
<div class="flex flex-col gap-1">
<div class="text-white text-opacity-80 text-sm px-2">
Nukes sent by them to you
${translateText("player_panel.nuke")}
</div>
<div class="bg-opacity-50 bg-gray-700 rounded p-2 text-white">
${this.getTotalNukesSent(other.id())}
@@ -390,7 +399,7 @@ export class PlayerPanel extends LitElement implements Layer {
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
text-white rounded-lg transition-colors"
>
Stop trading
${translateText("player_panel.stop_trade")}
</button>`
: ""}
${!canEmbargo && other != myPlayer
@@ -401,7 +410,7 @@ export class PlayerPanel extends LitElement implements Layer {
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
text-white rounded-lg transition-colors"
>
Start trading
${translateText("player_panel.start_trade")}
</button>`
: ""}
</div>
+7 -2
View File
@@ -1,5 +1,6 @@
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { GameView } from "../../../core/game/GameView";
import { renderNumber, renderTroops } from "../../Utils";
import { Layer } from "./Layer";
@@ -56,7 +57,9 @@ export class TopBar extends LitElement implements Layer {
<div
class="sm:col-span-1 flex items-center space-x-1 overflow-x-auto whitespace-nowrap"
>
<span class="font-bold shrink-0">Pop:</span>
<span class="font-bold shrink-0"
>${translateText("control_panel.pop")}:</span
>
<span translate="no"
>${renderTroops(myPlayer.population())} /
${renderTroops(maxPop)}</span
@@ -73,7 +76,9 @@ export class TopBar extends LitElement implements Layer {
<div
class="flex items-center space-x-2 overflow-x-auto whitespace-nowrap"
>
<span class="font-bold shrink-0">Gold:</span>
<span class="font-bold shrink-0"
>${translateText("control_panel.gold")}:</span
>
<span translate="no"
>${renderNumber(myPlayer.gold())}
(+${renderNumber(goldPerSecond)})</span
+16 -7
View File
@@ -1,5 +1,6 @@
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { translateText } from "../../../client/Utils";
import { EventBus } from "../../../core/EventBus";
import { Team } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates";
@@ -136,8 +137,12 @@ export class WinModal extends LitElement implements Layer {
<h2>${this._title || ""}</h2>
${this.innerHtml()}
<div class="button-container">
<button @click=${this._handleExit}>Exit Game</button>
<button @click=${this.hide}>Keep Playing</button>
<button @click=${this._handleExit}>
${translateText("win_modal.exit")}
</button>
<button @click=${this.hide}>
${translateText("win_modal.keep")}
</button>
</div>
</div>
`;
@@ -174,7 +179,7 @@ export class WinModal extends LitElement implements Layer {
myPlayer.hasSpawned()
) {
this.hasShownDeathModal = true;
this._title = "You died";
this._title = translateText("win_modal.died");
this.show();
}
this.game.updatesSinceLastTick()[GameUpdateType.Win].forEach((wu) => {
@@ -183,9 +188,11 @@ export class WinModal extends LitElement implements Layer {
new SendWinnerEvent(wu.winner as Team, wu.allPlayersStats, "team"),
);
if (wu.winner == this.game.myPlayer()?.team()) {
this._title = "Your team won!";
this._title = translateText("win_modal.your_team");
} else {
this._title = `${wu.winner} team has won!`;
this._title = translateText("win_modal.other_team", {
team: wu.winner,
});
}
this.show();
} else {
@@ -196,9 +203,11 @@ export class WinModal extends LitElement implements Layer {
new SendWinnerEvent(winner.clientID(), wu.allPlayersStats, "player"),
);
if (winner == this.game.myPlayer()) {
this._title = "You Won!";
this._title = translateText("win_modal.you_won");
} else {
this._title = `${winner.name()} has won!`;
this._title = translateText("win_modal.you_won", {
player: winner.name(),
});
}
this.show();
}