Skip to content

Fix(loc): Register MANY/MAKEPLURAL for the Es-ES Culture#4181

Closed
TheLacrox wants to merge 24 commits into
Monolith-Station:mainfrom
TheLacrox:capibara/fix-es-fluent-functions
Closed

Fix(loc): Register MANY/MAKEPLURAL for the Es-ES Culture#4181
TheLacrox wants to merge 24 commits into
Monolith-Station:mainfrom
TheLacrox:capibara/fix-es-fluent-functions

Conversation

@TheLacrox

Copy link
Copy Markdown

Problem

Server logs were flooded with:

[ERRO] loc: "es-ES"/"reagent-effect-guidebook-status-effect": "Unknown function: MANY()"

for reagent-effect-guidebook-status-effect, -movespeed-modifier, -electrocute and -reduce-rotting.

Root cause

ContentLocalizationManager.cs registers the language-specific Fluent functions (MANY, MAKEPLURAL) only on the en-US fallback culture (upstream comment even says other languages should add their own). The es-ES tree calls MANY() 8× and MAKEPLURAL() 11×, so the active es-ES bundle hit an unknown function every time those messages rendered.

Changes

  • ContentLocalizationManager.cs (the one approved Capibara divergence): register MANY/MAKEPLURAL on the es-ES culture with Spanish pluralization rules (vowel → +s, í/ú → +es, z → -z+ces, s/x invariable, consonant → +es). Bracketed with // Capibara ESP markers.
  • es-ES/guidebook/chemistry/effects.ftl: translate the literal argument MANY("second", …)MANY("segundo", …) (8×) so times render as "3 segundos" instead of "3 seconds".
  • es-ES/_EinsteinEngines/power/batteryDrinker.ftl: fix CAPATALIZECAPITALIZE typo (copied verbatim from upstream en-US, which still has the bug — en-US left untouched per fork policy).
  • CapibaraCultureTest: regression assertions that MANY() resolves with Spanish singular/plural inside the es-ES bundle, via new seed key capibara-loc-many.

Verification

  • pwsh _Capibara/validate-locale.ps1 → exit 0, "All es-ES files structurally valid."
  • dotnet test Content.IntegrationTests --filter CapibaraCultureTest → 1 passed, 0 failed.

Note: untranslated "Inner Ring" (out of scope)

The location splash texts ("Inner Ring", "Civilised space…") come from Resources/Prototypes/_Crescent/Biomes/space_biomes.yml (ambientSpaceBiome prototypes). SpaceTextDisplaySystem draws biome.Name/biome.Description verbatim — there is no Loc call, so they cannot be translated additively. Fixing them requires either a new (tiny) C# divergence in Content.Client/_Crescent/SpaceBiomes/SpaceBiomeTextDisplaySystem.cs (Loc.TryGetString($"space-biome-{id}-name")-style lookup) or translating the YAML in place like the ServerInfo exception. Left out of this PR pending a policy decision.

🤖 Generated with Claude Code

TheLacrox and others added 24 commits July 1, 2026 12:35
Design for translating Monolith-Capibara-ESP to Spanish (es-ES) via a
Fluent es-ES locale tree with en-US fallback, a Claude-subagent MT pipeline,
and _Capibara/ maintenance tooling. Keeps upstream merges conflict-free by
making all Spanish additive and confining code changes to a single
documented divergence in ContentLocalizationManager.cs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task-by-task plan: upstream remote, es-ES culture switch + fallback with
integration test, glossary, structural validator, upstream sync tool +
manifest, the Claude-subagent bulk translation workflow, and root CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Canonical es-ES term map and Fluent-preservation rules injected into every
translation agent for consistency across the locale tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Documents the Spanish localization architecture, the single ContentLocalizationManager
divergence and its keep-ours merge rule, the never-edit-upstream discipline, the
Fluent-preservation rules, and the upstream sync loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds validate-locale.ps1, a PowerShell 7 script that checks es-ES
Fluent files against their en-US counterparts for:
  - Dropped or renamed variables (missing/extra $placeholders)
  - Hallucinated or renamed message IDs not present in en-US
  - Unbalanced braces within a message
  - Orphan translation files with no en-US counterpart

Also adds test fixtures under _Capibara/tests/validate/:
  - en-US/sample.ftl  — reference English file
  - es-good/sample.ftl — correct Spanish translation (validator exits 0)
  - es-bad/sample.ftl  — three deliberate errors (validator exits 1)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The line-based parser reset the current message on any column-0 line
that was not a new message. Real Fluent messages contain col-0 content
mid-message (selector-closing `}`, `}{$var}`, arm labels), which caused
two bugs:

  C1 (false negative): variables in the second half of multi-selector
    messages (e.g. $tool in comp-repairable-repair) were never captured,
    so a dropped/renamed variable there passed silently.
  C2 (false positive): 27 en-US files with `}` at column 0 produced
    spurious "unbalanced braces" failures.

Get-Messages now tracks brace depth and treats a blank line as the
message terminator, so col-0 content stays attached to its message.
The per-message brace-balance check is unchanged but now meaningful.

Also hardens paths: resolve EnRoot as well as EsRoot, use the resolved
en root in Join-Path/Test-Path, add -LiteralPath to the en Test-Path,
and read files as UTF-8.

Adds fixtures exercising the fixes:
  - broken-brace (en-US + es-bad) trips the brace-balance check
  - es-good/_Capibara/extra.ftl verifies the _Capibara/ orphan exemption

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds _Capibara/sync-locale.ps1 that compares en-US source against
es-ES translations using a SHA1 hash manifest. After an upstream
merge it reports NEW keys (need translation), CHANGED keys (en-US
source changed, needs retranslation), and REMOVED keys (stale,
prune from es-ES).

Fixed a PowerShell variable-collision bug present in the original
spec: the local dict variable was named $manifest, which clashes
with the typed [string]$Manifest parameter (PS variables are
case-insensitive). The type constraint silently coerces @{} to the
string "System.Collections.Hashtable", breaking .ContainsKey().
Renamed the local to $stored to avoid the collision.

Includes fixture files under _Capibara/tests/sync/ (en-US, es-ES,
and a pre-seeded manifest.json) verified to produce:
  fixture run  -> NEW=1 CHANGED=1 REMOVED=1
  real en-US round-trip -> NEW=0 CHANGED=0 REMOVED=0 (23 509 keys)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sets the build-time culture to Spanish (Spain) and registers en-US as the
fallback culture so untranslated keys render English. Adds the es-ES seed
file and an integration test asserting the switch + fallback.

This is the single intentional divergence from upstream. On a merge conflict
in ContentLocalizationManager.cs, keep the Capibara block.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the Spanish (es-ES) translation of the entire en-US Fluent tree (1437
files, ~23.5k message keys), produced by a 185-agent Claude workflow ordered
core-facing dirs first. Includes the generated batch list, the source-hash
manifest, the workflow runbook, and the progress log.

All files are additive under Resources/Locale/es-ES; upstream never touches
them. Untranslated keys fall back to en-US at runtime.

Verified: validate-locale.ps1 exit 0; sync NEW=0 CHANGED=0; CapibaraCultureTest
boots the server with the full es-ES tree (no Fluent parse errors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a multi-stage Dockerfile (.NET 10 SDK -> runtime) that inits submodules,
packages a linux-x64 hybrid-ACZ server, and runs it non-root as the ss14 user
on 1212/udp+tcp. Baked prod config in Docker/server_config.prod.toml with
runtime env overrides via entrypoint.sh, plus a single-service docker-compose.
No TTS/redis (this fork has no tts.* cvars). Documents deploy in CLAUDE.md.

Verified: `docker build` succeeds on net10 and the container boots the server
(loads the es-ES config, SQLite prefs on the /data volume).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SS14 stores item/mob/structure names+descriptions in YAML prototypes, not .ftl,
so the earlier .ftl pass did not cover them (they rendered English in-game).
These are now localized via additive ent-<PrototypeId> Fluent overrides, which
the engine applies over the YAML name without touching any prototype file.

- CapibaraEntityDumpTest: engine-based dumper resolving each entity's English
  name/desc through inheritance (accurate, skips abstract).
- 16,843 entities / ~18k unique strings translated by a 206-agent workflow.
- generate-entity-ftl.ps1: merges the translations and emits valid Fluent
  (brace-escaping, multiline descs) to es-ES/_Capibara/entities/*.ftl (16,824 keys).
- sync-locale.ps1: exempt ent-*/capibara-* (fork-owned es-ES-only keys) from REMOVED.

Verified: CapibaraCultureTest boots the server with all entity keys loaded
(Fluent parse-clean). 20 strings fell back to English (logged in progress.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ts tooltip

Fourteen entity strings failed to key-match in the bulk run (curly quotes,
em-dashes, trademark signs in the source text); translated directly and
regenerated the entity .ftl files — entity coverage now 100% (0 fallbacks).

Adds hud-chatbox-highlights-tooltip to the fork seed file: upstream references
the key in ChannelFilterPopup.xaml but defines it in no locale, so it rendered
as a raw key id. Sync tool exempts this fork-owned key from REMOVED.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Translates all 307 ServerInfo XML docs (~832k chars) and the 4 player-facing
.txt files in place: the full in-game guidebook, the join-screen server rules
(_Mono/MonolithRuleset.xml), Space Law, and the intro/gameplay/sandbox texts.
A follow-up pass translated the 434 [textlink=...] link labels the first pass
preserved as markup.

This is an approved exception to the never-edit-upstream rule: the engine has
no per-locale mechanism for ServerInfo docs. Merge rule (documented in
CLAUDE.md): on conflict take upstream's English version and retranslate it;
_Capibara/guidebook-manifest.json records the source hashes to detect which
docs upstream changed.

Verified: validate-guidebook.ps1 — every <...> tag byte-identical to the
English baseline across all 307 files; upstream GuideEntryPrototypeTests +
DocumentParsingTest pass (every doc parses); English-remnant sweep clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ity keys

Two issues found verifying in-client:

1. TextLinkTag renders each [textlink] label as a single unbreakable Label
   control; 27 Spanish labels exceeded the rules-window width after
   translation expansion, crashing the client in WordWrap.FinalizeText on
   the join-rules popup. Shortened all labels to <=72 chars (full text lives
   in the linked rule pages).

2. Upstream defines ent- overrides for 2 entities in regular .ftl files
   (translated in the mirrored tree), which the generated entity files
   redefined - a duplicate-message Fluent error at load. The generator now
   excludes ids already defined in the mirrored es-ES tree.

Verified: validate-guidebook + validate-locale clean, no labels >75 chars,
0 duplicate ent- ids, guidebook parse tests pass (2/2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Guide entry titles come from guideEntry name: fields resolved via
Loc.GetString(name). Titles using guide-entry-*/species-name-* loc ids were
already translated; 15 _Mono entries use raw strings and rendered English.

- 8 raw names that are valid Fluent ids (Economy, Factions, TSFMC, Smuggling,
  Defusal, Minor_Factions, USSP, Lore) are localized additively in
  es-ES/_Capibara/guide-entries.ftl - the raw name doubles as the loc key.
- 6 names that cannot be Fluent ids (spaces/dots: Economy Recipes, Arc Furnace
  Recipes, S.O.P, Union of Soviet Socialist Planets, The Viper Group, Quick
  Timeline) carry the Spanish title directly in the _Mono YAML, marked with
  Capibara ESP comments (tiny extension of the in-place exception, documented
  in CLAUDE.md).
- sync-locale.ps1: REMOVED exemption is now folder-based - any key defined
  under es-ES/_Capibara/ is fork-owned by design.

Verified: validate-locale clean, sync NEW=0 CHANGED=0 REMOVED=0, guidebook
prototype+parse tests pass (2/2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
~35 user-visible strings were hardcoded English in Content.Client C#/XAML,
bypassing localization entirely (language menu "Choose", trait picker
"Expand/Collapse All", ammo counter "No Magazine!", surgery window title,
atmos device titles, crystallizer labels, drone console, ghost-role admin
window, "Unknown station" fallbacks, GPS "Error", late-join "Join Game").

Each is now routed through Loc with a capibara-ui-* key defined in fork-owned
Resources/Locale/es-ES/_Capibara/ui/*.ftl; source edits are marked with
Capibara ESP comments. Deliberately skipped: dev/mapping/debug tools,
runtime-overridden labels, and brand names (Robust#OS).

Also translates Minor_Factions.xml, which the guidebook pass missed due to a
write race (committed English), unified on "Grupo Viper" naming.

Verified: client compiles (0 errors), validate-locale + validate-guidebook clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upstream ships this file twice in the git index with different casing
(Minor_Factions.xml / minor_factions.xml) pointing to one physical file on
Windows. Earlier commits only updated the lowercase entry, leaving the other
English - and at package-extraction time either could win. Both entries now
carry the Spanish content (same blob).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Raises update.restart_delay from the 20s default to 600s so a pending
uptime/update restart only fires after the server has been empty for 10
minutes; any player connecting in that window aborts it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Spanish (es-ES) localization + Dokploy dockerization
The Dokploy host already runs another SS14 server on 1212, so the bind
failed. The host-side port is now ${SS14_PORT:-1212} (container still
listens on 1212 internally), and the advertised launcher connect address
(status.connectaddress) uses the external port instead of hardcoding 1212.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(deploy): configurable host port (SS14_PORT)
The language-specific Fluent functions were only registered on the
en-US fallback culture, so every es-ES message calling MANY() logged
"Unknown function: MANY()" at runtime and rendered broken guidebook
text. Register Spanish-grammar versions of MANY/MAKEPLURAL on the
active es-ES culture, translate the literal "second" arguments in the
chemistry guidebook to "segundo", and fix a CAPATALIZE typo that was
copied verbatim from upstream en-US. Adds a regression assertion to
CapibaraCultureTest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@TheLacrox

Copy link
Copy Markdown
Author

Opened against the wrong repo (this is a downstream translation fork change) — sorry for the noise.

@TheLacrox TheLacrox closed this Jul 2, 2026
@monolith8319 monolith8319 changed the title fix(loc): register MANY/MAKEPLURAL for the es-ES culture Fix(loc): Register MANY/MAKEPLURAL for the Es-ES Culture Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant