This commit is contained in:
2026-09-09 17:35:23 +08:00
parent 2a98ae7556
commit 161af68fe3
1271 changed files with 58251 additions and 98639 deletions
@@ -0,0 +1,422 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Internationalization | HiAPI-C# 2025 </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Internationalization | HiAPI-C# 2025 ">
<link rel="icon" href="../../img/HiAPI.favicon.ico">
<link rel="stylesheet" href="../../public/docfx.min.css">
<link rel="stylesheet" href="../../public/main.css">
<meta name="docfx:navrel" content="../../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../../">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../../index.html">
<img id="logo" class="svg" src="../../img/HiAPI.logo.png" alt="">
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="Anatomy-I18n">
<h1 id="internationalization">Internationalization</h1>
<p>The web client ships three locales — <code>en</code>, <code>zh-Hant</code> and <code>zh-Hans</code> — over one message shape, and it
owns no route: the mechanism is a vue-i18n instance created in the <code>i18n</code> module, a boot file that
picks the locale before the application mounts, a single switch function every locale change goes
through, and two Node scripts that gate the production build. English is not one locale among three;
it is the schema the other two are written against and the text every unlocalized path falls back to.
The user-facing gesture that triggers a switch is documented under
<a class="xref" href="../shell/language-selection-submenu.html">Language Selection SubMenu</a>, and the vocabulary the
translations are held to under <a class="xref" href="../conventions/translation-remarks.html">Translation Remarks</a>.</p>
<h2 id="the-three-bundles">The Three Bundles</h2>
<p>Each locale is a folder under the i18n directory holding one file per namespace plus an index file
that re-exports them as a single object. The three folders carry identical file lists — the same
twenty-three namespaces, from <code>apiErrors</code> and <code>auth</code> through <code>tree</code> and <code>widgets</code> — because a
namespace's keys are added to all three locales in the same change. Keys are named
<code>namespace.area.element</code> in lowerCamelCase and are split by the UI region they serve rather than by
the source file that reads them.</p>
<p>English is the schema, mechanically and not by convention. <code>wwwroot-src/src/i18n/schema.ts</code> exports
<code>MessageSchema = typeof en</code>, and each Chinese locale's index file annotates its own message object
with that type, so a key missing from a Chinese namespace is a type error at the annotation rather
than a silent English fallback at runtime. <code>schema.d.ts</code> augments vue-i18n's <code>DefineLocaleMessage</code> with the
same shape, which registers it as the global message schema for call sites. Type checking is not part
of the production build, however: <code>wwwroot-src/package.json</code> runs <code>vue-tsc</code> under a separate <code>lint</code>
script, and <code>build</code> runs the two i18n scripts described below instead.</p>
<p>The instance is created with <code>legacy: false</code>, an initial <code>locale</code> of <code>en</code> and <code>fallbackLocale: 'en'</code>.
The resolution chain is therefore current locale → English → the key string itself, so a key missing
from a Chinese bundle renders English with no runtime signal, and a key missing everywhere renders as
its own dotted name.</p>
<p>Quasar's own component texts are a parallel bundle with different code names, mapped in the same
module: <code>en</code> to Quasar's <code>en-US</code> pack, <code>zh-Hant</code> to <code>zh-TW</code>, <code>zh-Hans</code> to <code>zh-CN</code>. All three packs are
static imports rather than a dynamic import built from the locale code, because a template-literal
import on a bare package specifier is not statically analysable by the bundler.</p>
<h2 id="applying-a-locale">Applying a Locale</h2>
<p><code>applyLocale(code)</code> in the i18n module is the only place a locale change happens. It first maps its
argument through <code>normalizeLocale</code>, falling back to <code>en</code>, and then does six things in one call:</p>
<ul>
<li>sets the vue-i18n instance's <code>locale</code> ref;</li>
<li>calls Quasar's <code>Lang.set()</code> with the mapped language pack;</li>
<li>writes <code>document.documentElement.lang</code>, which is what selects the CJK glyph variant;</li>
<li>rebuilds the exported <code>collator</code>, an <code>Intl.Collator</code> for the new locale with base sensitivity and
numeric ordering;</li>
<li>writes the locale into <code>localStorage</code> under <code>hinc.lang</code>, inside a <code>try</code>/<code>catch</code> because storage can
be unavailable;</li>
<li>calls the re-title hook the router registered, so a browser tab parked on a page without navigating
still gets its title in the new language.</li>
</ul>
<p><code>normalizeLocale</code> maps any language tag — the server's, the browser's, or a stored one — onto a
supported locale or <code>null</code>. An exact match wins; otherwise a tag beginning <code>en</code> becomes <code>en</code>, and a
tag beginning <code>zh</code> becomes <code>zh-Hant</code> when it carries the <code>hant</code> script subtag or a <code>TW</code>, <code>HK</code> or <code>MO</code>
region, and <code>zh-Hans</code> otherwise. Anything else yields <code>null</code>. <code>applyLocale</code> and the boot file's
browser-language fallback each turn that <code>null</code> into <code>en</code>; the boot file's read of the stored value
instead treats it as a cache miss and asks the server, so an unsupported stored code takes the
cold-start path rather than pinning English.</p>
<p>Only two modules call <code>applyLocale</code>: the boot file, and the app-state store's language action.
The store updates its own <code>languageCode</code> optimistically, POSTs the new value, adopts the <code>current</code> and
<code>available</code> the server echoes back, and only then applies the locale — so the UI text flips after the
server has accepted the value, and a failed POST rolls the store reference back without any visible
language change.</p>
<h2 id="boot">Boot</h2>
<p><code>wwwroot-src/quasar.config.ts</code> lists three boot files in the order <code>auth</code>, <code>i18n</code>, <code>routine-toast</code>.
The i18n file is placed after <code>auth</code> because <code>auth</code> patches the global <code>fetch</code> for the 401 login gate,
and the language request the i18n file makes must go through that patch on builds where login is
enabled.</p>
<p>The boot file installs the plugin and then reads the <code>hinc.lang</code> value:</p>
<ul>
<li><strong>Cache hit.</strong> The cached locale is applied immediately, so the first painted frame is already
translated, and a reconcile against the server is fired without being awaited. The server value is
the source of truth, so a disagreement — the language was changed from another browser — repaints
shortly after mount.</li>
<li><strong>Cache miss.</strong> The file returns a promise racing the language request against a four-second
timeout, and mount waits on it. A dead backend rejects quickly; a hung one is capped by the timeout
rather than blanking the application. Either failure falls back to the normalized browser language,
and then to English.</li>
</ul>
<p>Every locale step is individually guarded, because a boot file that throws does not degrade to
English — the client entry logs the error and never mounts. The one unguarded step is installing the
plugin itself, without which nothing in the application can render a translated string at all. The
worst case the guards preserve is an application that mounts in English.</p>
<p>The shipped <code>wwwroot-src/index.html</code> is static and carries <code>lang=&quot;en&quot;</code>, so the very first painted
frame always reports English on the root element; <code>applyLocale</code> corrects it in the same tick as mount.</p>
<h2 id="what-re-renders">What Re-Renders</h2>
<p>A locale change is a write to one reactive ref, so everything that reads a translation inside a
render or a computed re-evaluates on its own. Five things follow it in practice:</p>
<ul>
<li><strong>Component text.</strong> Every <code>t()</code> call in a template or computed.</li>
<li><strong>Quasar's built-in texts</strong>, through the language pack the same call sets.</li>
<li><strong>The tab title.</strong> Route records carry an i18n <em>key</em> in <code>meta.title</code>; the router resolves it against
the active locale and composes the title from it. It re-resolves both after a navigation and from
the hook <code>applyLocale</code> calls.</li>
<li><strong>Control-Tree node labels</strong>, described below.</li>
<li><strong>Engine session messages</strong>, which are re-resolved because the localizing helper reads the locale
ref, so rows built inside a computed rebuild on a switch.</li>
</ul>
<p>File Explorer sorting is the one thing that does <em>not</em> follow on its own. The shared <code>collator</code> is a
live <code>let</code> binding rather than a reactive one: <code>applyLocale</code> rebuilds it for the new locale, but no
render tracks it and <code>FileExplorer.vue</code> watches no locale, so a listing already on screen keeps its
pre-switch order until the next folder load or sort-spec change re-runs the comparison. Because the
binding is live, a caller must invoke <code>collator.compare(...)</code> in place; capturing the instance or
extracting its <code>compare</code> would pin the pre-switch locale for good. <code>explorerSort.ts</code> is its only
consumer, and no bare <code>localeCompare</code> — whose <code>undefined</code> locale argument means the <em>browser</em> locale
— is called anywhere in the client.</p>
<p>Numbers deliberately do not follow the locale. <code>Intl.NumberFormat</code> is used nowhere in the client, and
the only <code>toLocale*</code> calls are two triangle counts and two timestamps, each passed the app locale
explicitly rather than defaulting to the browser's. NC and machine-tool quantities go through no
locale-aware number formatting at all, so a coordinate never picks up a comma decimal separator from
a locale that uses one.</p>
<h2 id="control-tree-node-labels">Control-Tree Node Labels</h2>
<p>A Control-Tree node carries both a <code>label</code> and an optional <code>labelKey</code>, and <code>nodeDisplayLabel(n)</code> in
<code>itemTypes.ts</code> returns the translation of <code>labelKey</code> — with the node's <code>labelParams</code> interpolated —
whenever one is set, and the verbatim <code>label</code> otherwise. <code>nodeDisplayInfo</code> applies the same rule to a
group stem's intro text through <code>infoKey</code> over <code>info</code>. The translator function is read inside the call
rather than captured at build time, so the computed that maps nodes onto the rendered tree tracks the
locale ref and the whole tree's role labels change on a switch.</p>
<p>The two fields overlap on purpose. <code>label</code> is required on the node type and <code>labelKey</code> is optional,
so a fixed role label is written both ways: the key that actually renders, and the verbatim English
sitting beside it as the fallback <code>nodeDisplayLabel</code> returns when no key is set. <code>label</code> is also the
only storage for text that can never be a key at all — server-composed mission titles, file-backed
geometry paths, and engine type names. It is the first of those two jobs, not the second, that makes
the verbatim English on node labels the largest single class in the census allowlist: fixed role
labels the census can see but the screen never shows, classified rather than removed because the
node type requires the field.</p>
<p>Mission command nodes are the one family that never takes the key path. A node's label is the title
the server composes for the command — the kind name in the request language, with the command's own
text in brackets when there is one — and <code>Missions/MissionController.cs</code> fills that title in for every
entry: from the command's own composition where it implements the engine's title contract — whose rule
keeps the kind name at the front and never drops it — and from the kind's display name, resolved in the
request culture, where it does not. Because a title is therefore always present, the node's
<code>labelKey</code> is always dropped and the client's kind-name fallback never fires. Since the composition
happens on the server per request, the tree host watches the locale and re-reads the command entries,
rewriting those labels in place; it deliberately does not rebuild the branch, which would remount an
open command editor mid-edit. The kind keys are still built as <code>tree.mission.kind.${commandType}</code>, and
they do render — <code>AddCommandDialog.vue</code> prefers one over the server's label for every kind that has
one — while the key lint resolves the template shape by wildcard rather than reporting an orphan.</p>
<h2 id="the-server-half">The Server Half</h2>
<p>The language preference is server-held. <code>GET /api/preference/language</code> answers
<code>{ success, current, available }</code>, where <code>current</code> is the persisted <code>UserConfig.LanguageCode</code> —
default <code>en</code> — and <code>available</code> is the server's own supported list, <code>en</code>, <code>zh-Hans</code>, <code>zh-Hant</code>. <code>POST</code>
rejects an unlisted code with 400 and otherwise writes the value and saves the user configuration,
which round-trips through the XML user config file. The <code>localStorage</code> key holds only a paint-time
hint and loses every disagreement with this value.</p>
<p>Some response text is composed on the server and never passes through a bundle.
<code>PresentCatalogService</code> resolves the effective language per request in this order: an explicit <code>?lang=</code>
query, then the request's <code>Accept-Language</code> header, then the persisted preference, then English.
Because the header sits <em>above</em> the saved preference, a browser set to a different language than the
application would win by default — so the client sends <code>?lang=</code> explicitly, from a <code>currentLang()</code>
helper returning the app locale, on the endpoints whose text is localized server-side: the
selected-step info, the execution strip chart, its step-property picker and its colour guide, the
mission command entries, catalog and field descriptors, and the step-present key list. The mission
controller resolves the same
chain into a per-request UI culture and injects it into the command text source, so the culture rides
on the request rather than on the thread.</p>
<p>Two further server-originated string classes are localized on the client instead:</p>
<ul>
<li><strong>Coded API errors.</strong> Ten error codes are mapped onto <code>apiErrors.*</code> keys; a failed response carrying
one of them renders the bundle value with the server's interpolation arguments. English is passed
straight through to the server's own message on purpose, because that message <em>is</em> the English
rendering and keeps per-site nuance the generic bundle value flattens.</li>
<li><strong>Engine notifications.</strong> Session messages arrive with a structured id and English text. The client
swaps in an <code>engineMessages.&lt;id&gt;</code> bundle entry, but only when the wire text agrees with the English
bundle entry for that id: a templated message must match the canonical English template exactly
before its arguments are re-interpolated, and an untemplated one is swapped only when the bundle
entry has no interpolation holes. Any mismatch — an id emitted with different templates at different
sites, or an engine built against a different bundle — falls back to the wire English rather than
rendering wrong text. The English <code>engineMessages</code> values are therefore load-bearing code, not a
description of the Chinese ones.</li>
</ul>
<p>The step-present labels have a third home again: they ship beside the executable as
<code>catalog.{lang}.json</code> files that overlay translations on top of the live English attribute data.
English never comes from those files, and a missing file, a missing key or a parse failure simply
means English.</p>
<h2 id="the-glossary-and-the-build-gate">The Glossary and the Build Gate</h2>
<p>Two Node scripts stand between the bundles and a shipped release. <code>wwwroot-src/package.json</code> defines
<code>lint:i18n</code> as <code>census.mjs</code> followed by <code>lint-glossary.mjs</code>, and <code>build</code> as <code>lint:i18n</code> followed by
<code>quasar build</code> — so either script failing stops the production build. The development server runs
neither.</p>
<p><code>lint-glossary.mjs</code> bundles each locale's index through esbuild, flattens the three message trees into
key/value maps, and applies four checks, every one of them a hard failure:</p>
<ol>
<li><strong>Key isomorphism.</strong> The three flattened key sets must be identical, compared in both directions,
so an extra key in a Chinese bundle fails as loudly as a missing one.</li>
<li><strong>Bidirectional key integrity.</strong> Every bundle-shaped string literal under the source tree —
excluding the bundles themselves — must resolve to an English key, and every English key must be
referenced from somewhere. A literal naming a namespace prefix counts as a reference to everything
beneath it; a template literal with an interpolation hole is expanded into a wildcard, which is how
<code>tree.mission.kind.${kind}</code> is matched; and <code>@:linked.key</code> references inside bundle values count as
well. A double-quoted literal in a Vue template is treated as a key only when its attribute is one
of the four the check knows to be key-valued, so an expression attribute whose local variable
happens to share a namespace name is not mistaken for one. The orphan half is what makes the check
bidirectional, and its allow-list is currently empty.</li>
<li><strong>Shape preservation.</strong> Never-translate tokens present in an English value must survive verbatim in
both Chinese values — product and brand names, file formats and unit symbols by literal match, and
NC codes, controller parameter names and function-key shortcuts by pattern. Placeholder sets must
match in both directions, so a Chinese value can neither lose a placeholder nor invent one.</li>
<li><strong>Per-locale vocabulary.</strong> An adjudicated forbidden-term table per locale, each entry naming its
replacement and its reason, plus a check for a Chinese value identical to its English source that
contains no CJK at all. Both carry narrow exemptions: named keys where a term is used in a
different sense, and a small identity set for values that legitimately are the identifier.</li>
</ol>
<p>The tables the third and fourth checks read are written at the top of the script itself.
<code>glossary.yaml</code>, the large harvested term list that sits beside the bundles, is generated data that
the application never imports and the lint never reads: it is where a reading is settled before it is
written into a bundle, not an input to the gate.</p>
<p><code>census.mjs</code> guards the opposite direction — text that never became a key. It scans the same tree for
user-visible English string literals reached through display-named template attributes, display-named
object properties, notification and dialog shorthands, and prose text nodes; subtracts a classified
allowlist of concrete text-and-file pairs; and compares what is left, per file, against a baseline.
The gate fails only when a file's residual count <em>rises</em>, so cleanup can land incrementally — but the
baseline is currently empty, which allows zero residual strings in any file and makes the effective
rule that a newly hard-coded display string fails the build unless it is added to the allowlist with
a reason.
Allowlist entries whose file field is a glob or a brace list are classification notes rather than
matchers; only concrete paths participate.</p>
<h2 id="strings-outside-the-bundles">Strings Outside the Bundles</h2>
<p>Three classes of user-visible English render in every locale — two because an interpolation hole
hides them from the census, which discards any literal containing one, and one because the allowlist
deliberately keeps it:</p>
<ul>
<li><strong>The shared numeric field's validation text.</strong> <code>NumericInput.vue</code> imports nothing from vue-i18n and
builds three messages as template literals directly: an invalid-number message quoting what was
typed, a &ldquo;must be ≥&rdquo; message naming the field's minimum, and a &ldquo;must be ≤&rdquo; message naming its
maximum. All three are produced on blur — the field commits on blur or on Enter — and every panel
that embeds the widget shows them in English. A <code>widgets</code> namespace exists for exactly this kind of
shared control text; this component does not use it.</li>
<li><strong>The geometry and topology editors' error prefixes.</strong> Those editors emit their failure context as a
template literal that leads with the type name and the operation, so the leading phrase is English
whatever the locale. The allowlist records the family as a ledger note rather than as matched
entries.</li>
<li><strong>The API-error default.</strong> The generic fallback message in the HTTP helper is allowlisted
deliberately, on the grounds that it sits in the same stream as the server's own English messages.</li>
</ul>
<p>A fourth class is untranslated by design rather than by escape: the dynamic <code>label</code> text on a
Control-Tree node — a file-backed path, an engine type name, and the user's own words inside a
server-composed mission title — and the identity strings the allowlist classifies alongside it, which
are backend type names rendered inside a translated sentence, axis and unit column headers, and brand
names. The role-label half of that same allowlist class is a different case again: stored English that
never reaches the screen at all, because a <code>labelKey</code> always resolves ahead of it.</p>
<h2 id="source-code-path">Source Code Path</h2>
<p>See <a class="xref" href="../index.html">HiNC App Anatomy</a> for git repository links.</p>
<h3 id="web-application">Web Application</h3>
<p>HiNC-2025-webservice (Quasar CLI SPA):</p>
<ul>
<li><code>wwwroot-src/src/i18n/index.ts</code> — the vue-i18n instance and its fallback chain, <code>SUPPORTED_LOCALES</code>,
<code>normalizeLocale</code>, <code>applyLocale</code>, the Quasar language-pack map, the shared collator, the storage key
and the re-title registration.</li>
<li><code>wwwroot-src/src/i18n/schema.ts</code> — the English bundle as <code>MessageSchema</code>.</li>
<li><code>wwwroot-src/src/i18n/schema.d.ts</code> — the ambient augmentation that registers that shape as
vue-i18n's global message schema.</li>
<li><code>wwwroot-src/src/i18n/en/index.ts</code> — the English bundle: the twenty-three namespaces assembled into
one object.</li>
<li><code>wwwroot-src/src/i18n/zh-Hant/index.ts</code> and <code>wwwroot-src/src/i18n/zh-Hans/index.ts</code> — the same
assembly, each annotated with <code>MessageSchema</code>.</li>
<li><code>wwwroot-src/src/i18n/en/engineMessages.ts</code> — the engine-notification renderings keyed by structured
id; its English values are the eligibility gate for every swap.</li>
<li><code>wwwroot-src/src/i18n/en/routes.ts</code> — the strings the routes' <code>meta.title</code> keys resolve to.</li>
<li><code>wwwroot-src/src/i18n/en/widgets.ts</code> — the shared-widget namespace.</li>
<li><code>wwwroot-src/src/i18n/README.md</code> — the invariants written beside the bundles: the fallback rule, the
never-translate list, the number and sorting rules and the vocabulary table.</li>
<li><code>wwwroot-src/src/i18n/glossary.yaml</code> — generated term data, imported by nothing.</li>
<li><code>wwwroot-src/src/boot/i18n.ts</code> — the plugin installation, the cached-locale fast path with its
unawaited reconcile, the awaited race on a cache miss, and the guards around every locale step.</li>
<li><code>wwwroot-src/quasar.config.ts</code> — the boot list whose order places i18n after the authentication
fetch patch.</li>
<li><code>wwwroot-src/index.html</code> — the static shell whose <code>lang</code> attribute is corrected at mount.</li>
<li><code>wwwroot-src/package.json</code> — <code>build</code> as the i18n scripts followed by <code>quasar build</code>, and <code>vue-tsc</code>
as a separate script.</li>
<li><code>scripts/lint-glossary.mjs</code> — the four bundle checks and the adjudication tables they read.</li>
<li><code>scripts/census.mjs</code> — the raw display-string extraction, the allowlist subtraction and the per-file
ratchet.</li>
<li><code>scripts/i18n-allowlist.json</code> — the classified exemptions, each with its reason.</li>
<li><code>scripts/i18n-baseline.json</code> — the per-file residual ceiling the census compares against.</li>
<li><code>wwwroot-src/src/stores/appState.ts</code> — the language reference, the available-code list, and the
action that POSTs before applying the locale and rolls back on failure.</li>
<li><code>wwwroot-src/src/api/preference.ts</code> — the typed wrappers over the language endpoints and the
<code>?lang=</code>-bearing step-present key request.</li>
<li><code>wwwroot-src/src/api/http.ts</code> — <code>currentLang()</code>, the coded-error map and the app-locale rendering
that skips English.</li>
<li><code>wwwroot-src/src/api/sessionMessages.ts</code> — the engine-notification localizer, its template-equality
gate and the repeat-fold wrapper.</li>
<li><code>wwwroot-src/src/components/controlTree/itemTypes.ts</code> — the node's <code>label</code>, <code>labelKey</code> and
<code>labelParams</code> fields and the <code>nodeDisplayLabel</code> and <code>nodeDisplayInfo</code> resolvers.</li>
<li><code>wwwroot-src/src/components/controlTree/useControlTreeHost.ts</code> — the rendered-tree computed that
calls the resolver, and the locale watcher that re-pulls the server-composed mission command titles
without rebuilding the branch.</li>
<li><code>wwwroot-src/src/components/controlTree/missionItemTypes.ts</code> — the kind display names and their
<code>tree.mission.kind.*</code> key twin, and the rule that drops the key whenever the entry carries a title.</li>
<li><code>wwwroot-src/src/components/widgets/NumericInput.vue</code> — the shared numeric field: blur-and-Enter
commit, and the three hard-coded validation messages.</li>
<li><code>wwwroot-src/src/components/AppMenuBar.vue</code> — the language submenu, its hard-coded self-name map and
the action it calls.</li>
<li><code>wwwroot-src/src/components/explorerSort.ts</code> — the only consumer of the shared collator, calling it
in place so a switch is picked up.</li>
<li><code>wwwroot-src/src/router/index.ts</code> — the key-valued <code>meta.title</code> resolution and the hook the locale
switch calls to re-title a parked tab.</li>
<li><code>Environments/PreferenceController.cs</code> — the language endpoints, the supported-code list and the
400 on an unlisted code.</li>
<li><code>Environments/UserConfig.cs</code> — the persisted language code, its English default and its XML
round-trip.</li>
<li><code>Environments/PresentCatalogService.cs</code> — the per-request language resolution chain, the tag
normalizer and the shipped catalog overlay.</li>
<li><code>Missions/MissionController.cs</code> — the per-request UI culture taken from the same chain and injected
into command title composition.</li>
</ul>
<h2 id="see-also">See Also</h2>
<ul>
<li><a class="xref" href="../shell/language-selection-submenu.html">Language Selection SubMenu</a> — the menu gesture that drives this mechanism, and the only
place a user changes the locale</li>
<li><a class="xref" href="../conventions/translation-remarks.html">Translation Remarks</a> — the terminology contract the bundles are written to, and the
readings the vocabulary lint enforces</li>
</ul>
</article>
<div class="contribution d-print-none">
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<span> Copyright © 2025 <a href='https://superhightech.com.tw'>Tech Coordinate</a>. All rights reserved. <a href='https://superhightech.com.tw'>超級高科技股份有限公司</a> © 2025 版權所有 </span>
</div>
</div>
</footer>
</body>
</html>