Skip to content

Fix/translate incoming unit notifications#3856

Open
mike-s-zaugg wants to merge 2 commits intoopenfrontio:mainfrom
mike-s-zaugg:fix/translate-incoming-unit-notifications
Open

Fix/translate incoming unit notifications#3856
mike-s-zaugg wants to merge 2 commits intoopenfrontio:mainfrom
mike-s-zaugg:fix/translate-incoming-unit-notifications

Conversation

@mike-s-zaugg
Copy link
Copy Markdown
Contributor

Description:

Incoming unit alert notifications (atom bomb, hydrogen bomb, MIRV, and naval invasion inbound) were hardcoded English strings, bypassing the translation system entirely. This fix wires them through the existing i18n pipeline.

Changes:

  • Add params? field to UnitIncomingUpdate so substitution values (player name, troop count) travel alongside the translation key
  • Apply the same startsWith("events_display.") translation guard in onUnitIncomingEvent that already exists in onDisplayMessageEvent
  • Add four new keys to en.json: atom_bomb_inbound, hydrogen_bomb_inbound, naval_invasion_inbound, mirv_inbound
  • Replace hardcoded template literals in NukeExecution, TransportShipExecution, and MIRVExecution with key + params

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

sxndrexe

mike-s-zaugg and others added 2 commits May 5, 2026 11:24
Show player info (resources, alliances, stats) when clicking on a player
during replay playback. Action buttons are hidden as they are not applicable
in replay mode.
Atom bomb, hydrogen bomb, MIRV, and naval invasion inbound alerts
were hardcoded English strings. Wire them through the existing
translation system so they respect the player's language setting.

Changes:
- Add params field to UnitIncomingUpdate so substitution values
  (player name, troop count) can be passed alongside the key
- Translate in onUnitIncomingEvent using the same
  startsWith(events_display.) guard already used for DisplayMessageUpdate
- Add four new keys to en.json: atom_bomb_inbound,
  hydrogen_bomb_inbound, naval_invasion_inbound, mirv_inbound
- Replace hardcoded template literals in NukeExecution,
  TransportShipExecution, and MIRVExecution with key + params

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

Walkthrough

This PR introduces localized incoming unit notifications with parameterized messages and adds replay-mode support to the PlayerPanel. Execution classes now call a revised displayIncomingUnit API that accepts translation keys and payload objects instead of hardcoded strings. PlayerPanel is enhanced to render read-only UI in replay mode even when the local player is unavailable.

Changes

Localized Incoming Unit Messages with Parameters

Layer / File(s) Summary
Data Shape
src/core/game/GameUpdates.ts
UnitIncomingUpdate interface gains optional `params?: Record<string, string
API Contract
src/core/game/Game.ts
displayIncomingUnit method signature extended with optional params parameter of type Record<string, string | number>.
Core Implementation
src/core/game/GameImpl.ts
displayIncomingUnit accepts and threads the optional params parameter into the UnitIncoming update payload.
Execution Layer
src/core/execution/MIRVExecution.ts, src/core/execution/NukeExecution.ts, src/core/execution/TransportShipExecution.ts
Callers switch from hardcoded emoji/string templates (e.g., ⚠️⚠️⚠️ ${name} - MIRV INBOUND ⚠️⚠️⚠️) to translation keys (e.g., events_display.mirv_inbound) with structured payloads like { name: this.player.displayName() }.
Display & Rendering
src/client/graphics/layers/EventsDisplay.ts
onUnitIncomingEvent now detects message keys starting with events_display. and translates them using the payload, falling back to raw message text otherwise.
Localization Data
resources/lang/en.json
Translation keys added to events_display: atom_bomb_inbound, hydrogen_bomb_inbound, naval_invasion_inbound, mirv_inbound, no_boats_available.

Replay Mode Support in PlayerPanel

Layer / File(s) Summary
Replay-Aware Rendering
src/client/graphics/layers/PlayerPanel.ts
Detects replay mode via this.g.config().isReplay() and introduces a viewer alias that uses the local player when available, otherwise defaults to the observed opponent. Render logic allowed to proceed when in replay mode even if myPlayer() is null.
Identity and Resource Display
src/client/graphics/layers/PlayerPanel.ts
Identity row and resource numbers now derive from viewer instead of strictly from my, ensuring consistent display in replay mode.
Modal Invocations
src/client/graphics/layers/PlayerPanel.ts
Modals (send-resource-modal, player-moderation-modal) now pass viewer as the myPlayer context instead of my alone.
Actions Gating
src/client/graphics/layers/PlayerPanel.ts
Actions rendering is wrapped in an isReplay check; interactive actions are rendered only outside replay mode, while other UI elements remain visible.
sequenceDiagram
    participant Exec as Execution<br/>(MIRVExecution,<br/>NukeExecution, etc.)
    participant Game as Game API<br/>(displayIncomingUnit)
    participant GameImpl as GameImpl
    participant Updates as GameUpdates<br/>(UnitIncoming)
    participant Display as EventsDisplay
    participant Lang as Language File

    Exec->>Game: displayIncomingUnit(id, "events_display.mirv_inbound",<br/>type, targetId, {name: "Alice"})
    Game->>GameImpl: (forward call)
    GameImpl->>Updates: Create UnitIncoming with<br/>translationKey + params
    Updates->>Display: Update received
    Display->>Lang: Fetch translation for<br/>"events_display.mirv_inbound"
    Lang-->>Display: Return template
    Display->>Display: Substitute {name} with<br/>payload.name
    Display->>Display: Render localized message<br/>"Alice - MIRV INBOUND"
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

The PR spans multiple files with changes to both core game logic (API threading) and execution/display layers, but the underlying pattern is consistent: replacing hardcoded strings with translation keys and payloads. The replay mode logic in PlayerPanel introduces conditional rendering that requires careful verification of all branches. No complex interdependencies within each cohort.

Poem

Messages now whisper in many tongues,
their names and numbers safe in structured song—
execution speaks, the game does hear,
and players viewing back through time still peer,
a UI alive in memory's gentle glow. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: translating hardcoded incoming unit notifications to use the i18n system.
Description check ✅ Passed The description is directly related to the changeset, explaining the motivation, listing all key changes, and matching the implemented modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core/game/GameImpl.ts (1)

945-961: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add tests for the new core update field propagation.

displayIncomingUnit now changes core update payload shape by adding params. Please add tests that verify UnitIncoming updates include and preserve params through this path (and still work when params is omitted).

As per coding guidelines: "src/core/**/*.ts: All changes to src/core/ must include tests".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/game/GameImpl.ts` around lines 945 - 961, Add unit tests for the new
params propagation in displayIncomingUnit: write tests that call
GameImpl.displayIncomingUnit (or simulate the public API that triggers it) and
assert that the resulting update emitted via addUpdate with type
GameUpdateType.UnitIncoming contains the params object exactly as passed and
that when params is omitted the update either has params undefined or omitted
per the payload contract; ensure tests cover both object-preservation (deep
equality) and absence case, and assert the playerID uses smallID as before. Use
the existing test harness for GameImpl updates to capture addUpdate outputs and
locate tests near other GameUpdateType.UnitIncoming specs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/graphics/layers/PlayerPanel.ts`:
- Around line 881-884: The replay-path sets viewer = my ?? other causing
perspective widgets to compare a player to themselves; update the calls and
helpers so they no-op when there is no real "me": change renderIdentityRow and
renderRelationPillIfNation signatures to accept my: PlayerView | null and
immediately return/do nothing if my is null, and similarly guard renderStats (or
its internal call to other.hasEmbargoAgainst(other)) so it doesn't check
embargoes when my is null; then either pass my (which may be null) into those
functions or only call them when my != null to ensure replay rendering doesn't
show self-relations or incorrect embargo/trade status.

---

Outside diff comments:
In `@src/core/game/GameImpl.ts`:
- Around line 945-961: Add unit tests for the new params propagation in
displayIncomingUnit: write tests that call GameImpl.displayIncomingUnit (or
simulate the public API that triggers it) and assert that the resulting update
emitted via addUpdate with type GameUpdateType.UnitIncoming contains the params
object exactly as passed and that when params is omitted the update either has
params undefined or omitted per the payload contract; ensure tests cover both
object-preservation (deep equality) and absence case, and assert the playerID
uses smallID as before. Use the existing test harness for GameImpl updates to
capture addUpdate outputs and locate tests near other
GameUpdateType.UnitIncoming specs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7bbdecd5-588f-4158-80d4-976120fdf00f

📥 Commits

Reviewing files that changed from the base of the PR and between b8a544a and 6900ab0.

📒 Files selected for processing (9)
  • resources/lang/en.json
  • src/client/graphics/layers/EventsDisplay.ts
  • src/client/graphics/layers/PlayerPanel.ts
  • src/core/execution/MIRVExecution.ts
  • src/core/execution/NukeExecution.ts
  • src/core/execution/TransportShipExecution.ts
  • src/core/game/Game.ts
  • src/core/game/GameImpl.ts
  • src/core/game/GameUpdates.ts

Comment on lines +881 to +884
// In replay mode myPlayer() is null; use other as stand-in so read-only rendering works
const viewer = my ?? other;
const myGoldNum = viewer.gold();
const myTroopsNum = Number(viewer.troops());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Heads-up: in replay mode viewer becomes other, so a couple of "perspective" widgets compare the player to themselves.

When my is null (replay), viewer = other. That value then flows into:

  • Line 952 → renderIdentityRow(other, viewer)renderRelationPillIfNation(other, my=other) → reads this.otherProfile.relations?.[other.smallID()], i.e. the player's relation to themselves. You'll get a "Neutral" pill on every Nation in replay.
  • Line 1003 → renderStats(other, viewer)other.hasEmbargoAgainst(other), which is essentially always falsy, so the trading row will read "Active" for every player in replay regardless of the real embargo state.

The send-resource and moderation modals are safe because Actions is hidden in replay, so viewer never leaks there in practice. Still, the two display widgets above will show a small fib in replay mode.

Easiest fix: only render those perspective-dependent bits when there is a real "me", and let the helpers no-op otherwise. Something like:

♻️ Suggested tweak
-                    <div class="mb-1">${this.renderIdentityRow(other, viewer)}</div>
+                    <div class="mb-1">${this.renderIdentityRow(other, my)}</div>
@@
-                    ${this.renderStats(other, viewer)}
+                    ${my ? this.renderStats(other, my) : ""}

…and let renderIdentityRow / renderRelationPillIfNation accept my: PlayerView | null and bail early when it is null. A typed union (PlayerView | null) keeps the signatures honest without any extra class plumbing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/graphics/layers/PlayerPanel.ts` around lines 881 - 884, The
replay-path sets viewer = my ?? other causing perspective widgets to compare a
player to themselves; update the calls and helpers so they no-op when there is
no real "me": change renderIdentityRow and renderRelationPillIfNation signatures
to accept my: PlayerView | null and immediately return/do nothing if my is null,
and similarly guard renderStats (or its internal call to
other.hasEmbargoAgainst(other)) so it doesn't check embargoes when my is null;
then either pass my (which may be null) into those functions or only call them
when my != null to ensure replay rendering doesn't show self-relations or
incorrect embargo/trade status.

@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management May 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

1 participant