Internationalization
The web client ships five locales — en, zh-Hant, zh-Hans, vi and ja — over one message
shape, and it owns no route: the mechanism is a vue-i18n instance created in the i18n module, a
loader that fetches a locale's bundle the first time the locale is applied, a boot file that picks
the locale before the application mounts, a single switch function every locale change goes through,
and three Node scripts that lint the bundles — a gate for npm run build, a notice in the release
builds. English is not one locale among five; it is the schema the other four are written against,
the only one of the app's own bundles loaded from the start, and the text every unlocalized path
falls back to.
The user-facing gesture that triggers a switch is documented under
Language Selection SubMenu, and the vocabulary the
translations are held to under Translation Remarks.
The Locale Bundles
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 five folders carry identical file lists — the same
namespaces, from apiErrors and auth through tree and widgets — because a
namespace's keys are added to every locale in the same change. Keys are named
namespace.area.element in lowerCamelCase and are split by the UI region they serve rather than by
the source file that reads them. A locale's own name stays out of the bundles: the language menu,
and the toasts that report a pick, name each locale in its own language whatever the current one —
English, 简体中文, 繁體中文, Tiếng Việt, 日本語 — from a map in AppMenuBar.vue.
English is the schema, mechanically and not by convention. wwwroot-src/src/i18n/schema.ts exports
MessageSchema = typeof en, and every other locale's index file annotates its own message object
with that type, so a key missing from one of its namespaces is a type error at the annotation rather
than a silent English fallback at runtime. schema.d.ts augments vue-i18n's DefineLocaleMessage with the
same shape, which registers it as the global message schema for call sites. Type checking is not part
of npm run build, however: wwwroot-src/package.json runs vue-tsc under a separate lint
script, and build runs the three i18n scripts described below instead. Of the release builds, only
the installed HiNC's runs the type check, and a key missing from a locale stops it there.
The instance is created with legacy: false, an initial locale of en, fallbackLocale: 'en'
and the English messages alone; every other locale's messages are merged in when that locale first
loads. The resolution chain is therefore current locale → English → the key string itself, so a key
missing from another locale's bundle renders English with no runtime signal, and a key missing
everywhere renders as its own dotted name.
Quasar's own component texts are a parallel bundle with different code names, paired in the same
module: en with Quasar's en-US pack, zh-Hant with zh-TW, zh-Hans with zh-CN, vi with
vi and ja with ja. The vi pack is used with three texts replaced where its own wording reads
poorly: QTree's two empty states, tree.noNodes and tree.noResults, read Không có mục nào and
Không tìm thấy mục phù hợp — mục, the bundles' word for a row of an interface tree, since in
the same panels a nút is a button — and the label-less form of label.collapse reads Thu gọn;
the rest is the pack's own. The ja pack is used with four replaced where its own wording reads
wrongly: the label-less label.expand and label.collapse are 拡大 (enlarge) and 崩壊 (a
building's collapse) in the pack and read 展開 and 折りたたむ, their labelled forms read
「…」を展開 and 「…」を折りたたむ alike (the pack ends only the first with です/ます), and
QTree's two empty states read 項目がありません and 該当する項目がありません — 項目, the
bundles' word for a row of an interface tree, where the pack says ノード, which the bundles keep
for a node of an engine data structure.
Only the English pair — the en bundle and the en-US pack — is a static import, so the entry
closure every route preloads carries the app's own strings in one locale (a product module's boot
file, described below, brings that module's strings in every locale). Every other locale's bundle
and pack load on demand through its LOCALE_LOADERS entry, a pair of import() calls whose literal
specifiers give each locale a chunk of its own: a template-literal import on a bare package
specifier is not statically analysable by the bundler. Bundled eagerly, the five locales would push
the entry's i18n chunk past Vite's 500 kB chunk-size warning, which stays a tripwire: the build does
not raise chunkSizeWarningLimit.
Applying a Locale
applyLocale(code) in the i18n module is the only place a locale change happens, and it is
asynchronous. It first maps its argument through normalizeLocale, falling back to en, waits for
loadLocale to bring that locale in, and then does five things in one synchronous step:
- sets the vue-i18n instance's
localeref; - calls Quasar's
Lang.set()with that locale's language pack; - writes
document.documentElement.lang, which is what selects the CJK glyph variant; - writes the locale into
localStorageunderhinc.lang, inside atry/catchbecause storage can be unavailable — the one step a fallback skips (below); - 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.
The exported collator needs no step of its own: it is a computed over the locale ref, an
Intl.Collator for the current locale with base sensitivity and numeric ordering.
loadLocale resolves at once for English and for a locale already loaded. Otherwise it runs the
locale's loader, which merges the messages with mergeLocaleMessage — never setLocaleMessage,
which would wipe the namespaces the product module's boot file merges into every locale before most
of them load — and records the Quasar pack. Every call made while a run is in flight shares that
run, each waiting under a cap of its own: a chunk that fails to fetch, or takes longer than four
seconds (LOCALE_LOAD_TIMEOUT_MS), rejects the load, and a retry after a failed fetch may meet the
same failure until the page reloads. applyLocale turns the rejection into a console warning and
keeps the locale already shown — English while the application boots, the worst case the boot
file's guards keep to as well — and leaves the hinc.lang hint as it was, so a failed load never
changes the language the next cold load paints its first frame in. The loader runs on past the
cap, though: when a chunk that was only slow lands, its locale is applied then, hint included,
unless a newer call has begun meanwhile, while a chunk that failed to fetch applies nothing
later. Calls overlap when a pick is made while another chunk is in flight, or when the boot
file's reconcile meets a pick; a sequence number makes the newest win, and a call whose wait ends
after a newer call began applies nothing.
normalizeLocale maps any language tag — the server's, the browser's, or a stored one — onto a
supported locale or null. An exact match wins; otherwise, in any letter case, a tag beginning en
becomes en; a tag beginning zh becomes zh-Hant when it carries the hant script subtag or a
TW, HK or MO region, and zh-Hans otherwise; and a tag whose primary subtag is vi or ja —
vi-VN or ja-JP, or written with an underscore — becomes vi or ja, while a three-letter code
that merely begins with those letters, such as vic, vin or jam, names another language.
Anything else yields null. applyLocale
and the boot file's browser-language fallback each turn that null into en; 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.
Only two modules call applyLocale: the boot file, and the app-state store's language action.
The store updates its own languageCode optimistically, POSTs the new value, adopts the current and
available the server echoes back, and only then applies the locale, awaiting its load — 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. The menu's confirmation toast waits for that load too, so it
reads in the new language. When the locale in force afterwards is not the one picked, its chunk
having failed to load or missed the cap, the toast says so instead
(menu.preference.languageLoadFailed): the choice is saved but its texts did not load in time, and
the page is to be reloaded if the interface does not switch — which it does by itself once a chunk
that was only slow lands.
menu.preference.languageSaveFailed is left to a POST that fails.
Boot
wwwroot-src/quasar.config.ts lists four boot files in the order auth, i18n, routine-toast,
'~product/boot'. The i18n file is placed after auth because auth patches the global fetch for
the 401 login gate, and the language request the i18n file makes must go through that patch on builds
where login is enabled. The last is the product module's own boot file, where a module built in
through the product alias merges its strings into every locale before the application mounts,
whether or not that locale's own bundle has loaded; the web service's own module leaves it empty —
see Hosting from another project.
The boot file installs the plugin and then reads the hinc.lang value:
- Cache hit. The file returns a promise that applies the cached locale, so mount waits for that locale's chunk — English has none to wait for — and never for the language API, and the first frame the application paints is already translated. A reconcile against the server is then 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.
- Cache miss. The file returns a promise racing the language request against a four-second
timeout and then applying the locale the server names, and mount waits on it. A dead backend
rejects quickly; a hung one is capped by the timeout rather than blanking the application, and the
locale's chunk is capped the same way by
loadLocale. A failed or timed-out request falls back to the normalized browser language, and then to English.
On either path a chunk that fails to load or misses its cap lets the application mount in English,
and the late apply in applyLocale switches it once a chunk that was only slow lands.
Every locale step is individually guarded, because a boot file that throws, or returns a promise that rejects, 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.
The shipped wwwroot-src/index.html is static and carries lang="en", so the very first painted
frame always reports English on the root element; applyLocale corrects it in the same tick as mount.
What Re-Renders
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:
- Component text. Every
t()call in a template or computed. - Quasar's built-in texts, through the language pack the same call sets.
- The tab title. Route records carry an i18n key in
meta.title; the router resolves it against the active locale and composes the title from it. It re-resolves both after a navigation and from the hookapplyLocalecalls. - Control-Tree node labels, described below.
- Engine session messages, which are re-resolved because the localizing helper reads the locale ref, so rows built inside a computed rebuild on a switch.
File Explorer sorting follows too, by a different route. The shared collator is a computed derived
from the locale ref, so a Vue computed that sorts through explorerSort.ts re-runs on a switch by
itself; the explorer's tree is sorted imperatively (each folder is sorted as it loads), so
FileExplorer.vue watches the collator alongside its sort spec and re-sorts every loaded folder in
place — the listing already on screen takes the new order without a reload. A caller must read
collator.value.compare(...) at compare time; capturing the instance or extracting its compare
would pin the pre-switch locale for good. explorerSort.ts is the only code that compares through
it (FileExplorer.vue imports it only as a watch source), and no bare localeCompare — whose
undefined locale argument means the browser locale — is called anywhere in the client.
Numbers deliberately do not follow the locale. Intl.NumberFormat is called nowhere in the client's
own code, and the only toLocale* calls format counts — of triangles, lines, rows and pending
changes — 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 — they
are written with toFixed, toPrecision, toExponential or a plain string conversion, none of
which reads a locale — so a coordinate the client writes never picks up a comma decimal separator
from a locale that uses one. Vietnamese is such a locale, and it groups digits with the .: a
Vietnamese reader takes 1.500 for fifteen hundred, where in a machine quantity that . is the
decimal point. The rule therefore holds inside translations too — a vi value that carries a
number keeps the English . decimal (0.016 mm, never 0,016 mm) — and in the charts: uPlot's
default formats ticks and legend values in the browser's locale, so UplotChart.vue gives every
axis and series without a caller's formatter a toFixed default, ticks to the decimals their
increment needs and legend values to at most three decimals, trailing zeros dropped and no digits
grouped.
The browser's own number field is the one place a quantity can still show a locale's decimal comma.
The shared NumericInput widget is a text field and always writes the ., but a native
<q-input type="number"> shows its value the way the browser formats numbers: Firefox follows the
field's lang, which it otherwise takes from the root element — vi in Vietnamese — so the native
number fields that take a decimal carry lang="en" and show the .; Chromium ignores lang and
follows its own interface language, so in a Chromium whose interface is Vietnamese those fields
still show 0,05. Only the display differs: the value the field holds, and the one the application
reads and sends, is 0.05.
Typing and Searching
Text typed into the client reaches it in more forms than the bundles write, and three helpers
under wwwroot-src/src/utils/ bring it to one, in every locale.
- Type-to-filter boxes. The Add Command dialog's filter, the Session Messages filter, the
Color Index chart's step-property picker, the program node's invocation picker and the Step
Colors branch's key choosers — the Add terms… picker and a term's or a leaf's key select —
match a row
when
foldForSearch(text)includesfoldForSearch(query)— both sides folded. The fold decomposes (NFKD), drops the combining marks except the kana voicing marks, composes again, turns katakana into hiragana, mapsđtodand lowers the case. So a Vietnamese word is found with its tone marks, with them typed as combining marks, or with none; full-width letters and digits an IME leaves (STL) findSTL; half-width katakana and hiragana (ツール,つーる) findツール; andガstill does not findカ. - Enter. A Japanese, Chinese or Korean IME confirms a conversion with Enter, and Chromium reports
that key as a keydown
Process(key code 229) followed by a plain keyupEnter. A field that committed on keyup Enter therefore committed a name typed in two conversions after the first, so every Enter commit of the client's own fields — a name, a note, a step-variable key, an M code, a path, a number — runs on keydown throughisCommitEnter, which passes an Enter that is neither part of a composition nor an auto-repeat. The key handlers that drive a list — the Add Command dialog's arrows and Enter, the program viewer's cell editor and its go-to-line box's Esc — leave a key that belongs to a composition (isImeComposing) to the IME. Quasar's prompt dialogs are the exception: the File Explorer's New Folder, New File and Rename, the coolant condition's Save As and the spindle contour's duration commit on a keyup Enter behind Quasar's own test of the last keydown'sisComposing, which holds in Chromium and Firefox but not in Safari, whose composition-ending keydown no longer carries it; the CL Runner's excluded words are added by QSelect on a keydown Enter. - Numbers.
NumericInput,Vec3Input,Mat4Input, the cylindroid editor and the program viewer's go-to-line box pass their text throughasciiNumberTextbeforeNumber(): an IME in full-width mode types0.5and-3, in kana mode its minus, period and comma keys give the long-vowel markー,。and、, and pasted text brings the minus sign−or a dash; each becomes ASCII, and the field shows and sends0.5and-3. Nothing else is folded — not the whole text through NFKC, which would read10²as102— so a superscript or a circled digit is still refused.
Control-Tree Node Labels
A Control-Tree node carries both a label and an optional labelKey, and nodeDisplayLabel(n) in
itemTypes.ts returns the translation of labelKey — with the node's labelParams interpolated —
whenever one is set, and the verbatim label otherwise. nodeDisplayInfo applies the same rule to a
group stem's intro text through infoKey over info. 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.
The two fields overlap on purpose. label is required on the node type and labelKey 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 nodeDisplayLabel returns when no key is set. label 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.
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 Missions/MissionController.cs 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
labelKey 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 tree.mission.kind.${commandType}, and
they do render — AddCommandDialog.vue 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.
The Server Half
The language preference is server-held. GET /api/preference/language answers
{ success, current, available }, where current is the persisted UserConfig.LanguageCode —
default en — and available is the server's own supported list in the order the language menu
draws it: en, zh-Hans, zh-Hant, vi, ja. POST rejects a code that is not on the list —
matched exactly, letter case included — with 400 and otherwise writes the value and saves the user
configuration, which round-trips through the XML user config file. The localStorage key holds only
a paint-time hint and loses every disagreement with this value.
Some response text is composed on the server and never passes through a bundle.
PresentCatalogService resolves the effective language per request in this order: an explicit ?lang=
query, then the request's Accept-Language header, then the persisted preference, then English.
Because the header sits above the saved preference, a browser set to a different language than the
application would win by default — so the client sends ?lang= explicitly, from a currentLang()
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. Every tag the chain reads goes through
PresentCatalogService.Normalize onto a closed set — a zh tag to one of the two Chinese scripts,
a primary subtag of vi or ja (split on - or _, as on the client) to vi or ja, anything
else to English —
because the result names the catalog.{lang}.json file read and is handed to
CultureInfo.GetCultureInfo.
Two further server-originated string classes are localized on the client instead:
- Coded API errors. The HTTP helper maps the error codes it knows onto
apiErrors.*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 is the English rendering and keeps per-site nuance the generic bundle value flattens. - Engine notifications. Session messages arrive with a structured id and English text. The client
swaps in an
engineMessages.<id>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 EnglishengineMessagesvalues are therefore load-bearing code, not a description of the translated ones.
The step-present labels have a third home again: they ship beside the executable as
catalog.{lang}.json 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. The translations in them are generated from glossary.yaml, described below, so a
step-field name is changed there and not in a catalog file.
The Glossary and the Build Gate
Three Node scripts lint the bundles. wwwroot-src/package.json defines lint:i18n as
census.mjs, then lint-glossary.mjs, then lint-engine-messages.mjs, and build as lint:i18n
followed by quasar build — so any of the three failing stops npm run build. The release builds —
the web service's own front end and the installed HiNC's — do not run npm run build. They run
lint:i18n as a step of its own and only report its failure, so the bundle ships all the same, with
a missing translation in English and a wrong value as it is. The installed HiNC's build then runs
the type check, which does stop it on a key missing from a locale. The development server runs none
of them.
lint-glossary.mjs takes its locale list from SUPPORTED_LOCALES in the i18n module, bundles each
locale's index through esbuild, flattens the message trees into key/value maps, and applies five
checks to them and a sixth to the server's step-field catalogs, any one of which fails the lint:
- Locales and key isomorphism. Every listed locale needs its folder and index file and, English
aside, a literal
import()of that folder inLOCALE_LOADERS, and a folder holding a bundle must be listed:quasar buildnotices neither omission, and the locale then never shows. One thatLOCALE_LOADERSleaves out has no more to show for it than a console warning and, when it is picked from the menu, the toast saying its texts did not load; a bundle the list leaves out, in a languagenormalizeLocalehas no rule for, has not even the warning —normalizeLocalegives its codenull, andapplyLocaleapplies English in its place — only the toast when it is picked. A bundle that does not build is a finding too. Every other locale's flattened key set must then equal English's, compared in both directions, so an extra key fails as loudly as a missing one. - Bidirectional key integrity. 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
tree.mission.kind.${kind}is matched; and@:linked.keyreferences 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. - Shape preservation. Never-translate tokens present in an English value must survive verbatim in every other locale's value — 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 translated value can neither lose a placeholder nor invent one.
- Per-locale vocabulary. An adjudicated forbidden-term table per locale, each entry naming its reason and, where there is a single one, its replacement, plus a check for a value identical to its English source that still holds something to translate — for a Chinese value, any two Latin letters in a row. Both carry narrow exemptions: named keys where a term is used in a different sense, and an identity set per locale for values that legitimately are the identifier.
- Message form. Every non-English value must be Unicode NFC; every value vue-i18n renders — all
but the English engine messages, whose wire English renders in their place — must compile under
its message compiler, so a bare
@or an unpaired brace fails the lint (a literal@is written{'@'}); and a Vietnamese or Japanese value must keep its English value's number of plural forms, separated by|. - Step-field catalogs. Every name and short name in the
catalog.{lang}.jsonfiles of the HiNc-Resource package'sStepPresentCatalogfolder, for each locale but English, must be NFC and clear of that locale's forbidden-term table. The folder is looked for the waylint-engine-messages.mjslooks for its catalog. No folder, or no file for a locale, is no finding — a checkout of the web service alone has none to check, and the server serves a locale without its file in English — while a file that does not parse is one.
Vietnamese, the one translated locale in Latin letters, is read in its own way. A never-translate
token or code counts in a Chinese or Japanese value wherever it occurs, but in a Vietnamese one only
as a word of its own, with Unicode letters in its boundaries, so the capital N of Nước does not
stand in for the newton symbol N. The vi forbidden-term table, its patterns bounded the same way since \b
knows only ASCII letters, bans Han characters and CJK or fullwidth punctuation; a tone mark in the
newer place on an open oa, oe or uy syllable (hoá, tuỳ, khoẻ), where the bundles keep the
traditional placement on the first vowel that Microsoft's and Quasar's Vietnamese UI use (hóa,
tùy, khỏe); and three readings the vocabulary table rules out — chương trình điều khiển for the
Control Tree (cây điều khiển), thành phần lỗi or số hạng lỗi for an error term, a deviation
(thành phần sai số) rather than a mistake, and bầu kẹp for the holder (đầu kẹp). A Vietnamese
value identical to its English is a finding unless nothing is left to translate once tokens, codes,
placeholders, numbers and units are set aside (RTCP, {n} mm), or its key is in the vi identity
set — a set of its own, since a locale in Latin letters keeps far more English than a Chinese one
and its exemptions must not pass a Chinese value. NFC means precomposed letters: a value holding a
letter stored as a base letter followed by combining marks is a finding. Vietnamese does not tell
one from many, so its plural forms usually repeat one text, a zero form aside, but their number
stays the English value's.
Japanese is read as the Chinese locales are: a token or code counts wherever it occurs (G54の値),
and a value identical to its English is a finding when it holds two Latin letters in a row, unless
its key is in the ja identity set. Its forbidden-term table starts with characters. A Han
character outside JIS X 0208 — the set read once from Node's Shift_JIS decoder without the NEC and
IBM extension rows of Windows-31J, whose kanji are mostly Traditional forms (增, 寬, 橫, 黑); on a
Node built with small ICU the lint fails rather than skip the check — is a Chinese one, Simplified
or a Traditional form Japanese never adopted; a 旧字体 form from a table of pairs (轉 → 転, 數 → 数,
沒 → 没 …) is a Traditional or pre-war spelling; a few JIS characters Chinese writes where Japanese
does not (后, 无, 气, 决 …), half-width katakana, full-width letters or digits, and the full-width
comma and full stop , . are findings too. Then words: Chinese words that read like Japanese
(程式, 檔案, 預設, 載入, 刀具, 進給, 工件 …), each with the Japanese to write where there is one;
function words in kanji that Microsoft's Japanese style guide writes in kana (下さい, 出来, 既に, 全て,
又は …), each pattern stopping short of a correct word with the same characters (変更に, 一致し,
及びます, 出来高); loanwords written with the final long-vowel mark that JIS Z 8301, the spelling of
the machine-tool makers' manuals, drops, from a table of spellings (パラメーター for パラメータ,
ホルダー for ホルダ, メモリー for メモリ); and six readings the vocabulary table rules out — 制御プログラム or
制御ツリー for the Control Tree, エラー項 for an error term, ツールホルダ for the holder, 取付け点
for a mount, 衝突 for a collision and プレイヤ for the player. Japanese has no plural either, so its
plural forms repeat one text, a zero form aside, and their number stays the English value's.
The tables the third and fourth checks read are written at the top of the script itself.
glossary.yaml, the large term list that sits beside the bundles, is no input to the lint: the
lint never reads it and the application never imports it. Its Chinese values were harvested from
the legacy localization corpus by .ai/harvest-i18n.py, whose adjudication tables, not the file,
hold its rulings on a harvested term; that full harvest is retired, and running it again needs the
legacy repositories cloned back beside the web service first. The step-field entries (context
MachiningStep), whose legacy source is gone, and the Vietnamese and Japanese values, which never
had one, are kept in the file by hand, and a full run carries them over. What the file does feed is
the server: harvest-i18n.py --present-catalog, which needs no legacy source, generates the
step-field catalogs StepPresentCatalog/catalog.{zh-Hant,zh-Hans,vi,ja}.json from it, so a
step-field name — a finding of the sixth check included — is changed in glossary.yaml, never in a
catalog, which the next run would overwrite.
census.mjs 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 rises, 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 lint 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.
lint-engine-messages.mjs holds the English engineMessages entries to the engine that emits them.
It reads the catalog of emit sites the engine ships in the HiNc-Resource package — each templated
site's id and template — and fails on a templated English entry no site carries, on one that matches
none of its id's templates byte for byte, and on a listed id the engine emits with two different
templates: in each case the engine-notification swap above falls back to the wire English, in every
locale, for some or all of that id's messages. No catalog found is a failure, not a skip.
A build with HINC_PRODUCT_SPA set compiles another product module in place of the web service's
own (Hosting from another project).
With the variable set, lint-glossary.mjs also loads that folder's bundles, one per locale, which its
boot file merges into the app's, checks them as part of the app's and scans the folder's files with
the source tree, and adds two checks. A module with strings of its own must carry a bundle for every
listed locale and merge each in its boot file, because a locale left out breaks no build and only
shows that module's strings in English; and no top-level namespace of the module's may repeat one of
the app's, because the merge is deep and whichever side merges last wins: the module's strings
overwrite the app's in English and in the locale applied at boot, and the app's own bundle
overwrites the module's in a locale loaded later — either way a string changes with no sign.
census.mjs scans the folder too, against an allowlist and a baseline the folder keeps for itself,
so the module's exemptions never land in the web service's own lists.
Strings Outside the Bundles
Two classes of user-visible English render in every locale — one because an interpolation hole hides it from the census, which discards any literal containing one, and one because the allowlist deliberately keeps it:
- The geometry and topology editors' error prefixes. 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.
- The API-error default. 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.
A third class is untranslated by design rather than by escape: the dynamic label 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 labelKey always resolves ahead of it.
Source Code Path
See HiNC App Anatomy for git repository links.
Web Application
HiNC-2025-webservice (Quasar CLI SPA):
wwwroot-src/src/i18n/index.ts— the vue-i18n instance and its fallback chain,SUPPORTED_LOCALES,normalizeLocale,LOCALE_LOADERS(each locale's bundle and Quasar pack, theviandjapacks' replaced texts),loadLocaleand its four-second cap,applyLocalewith its sequence number, its fallback to the locale shown and its late apply of a slow chunk, the shared collator, the storage key and the re-title registration.wwwroot-src/src/i18n/schema.ts— the English bundle asMessageSchema.wwwroot-src/src/i18n/schema.d.ts— the ambient augmentation that registers that shape as vue-i18n's global message schema.wwwroot-src/src/i18n/en/index.ts— the English bundle: the namespaces assembled into one object.wwwroot-src/src/i18n/zh-Hant/index.ts,wwwroot-src/src/i18n/zh-Hans/index.ts,wwwroot-src/src/i18n/vi/index.tsandwwwroot-src/src/i18n/ja/index.ts— the same assembly, each annotated withMessageSchemaand each loaded on demand as a chunk of its own.wwwroot-src/src/i18n/en/engineMessages.ts— the engine-notification renderings keyed by structured id; its English values are the eligibility gate for every swap.wwwroot-src/src/i18n/en/routes.ts— the strings the routes'meta.titlekeys resolve to.wwwroot-src/src/i18n/en/widgets.ts— the shared-widget namespace.wwwroot-src/src/i18n/README.md— the invariants written beside the bundles: the fallback rule, the on-demand loading, the never-translate list, the number and sorting rules, keyboard input through an IME, the vocabulary table and the Vietnamese and Japanese rules.wwwroot-src/src/i18n/glossary.yaml— the term list: Chinese values harvested, the step-field entries and the Vietnamese and Japanese values kept by hand; imported by nothing, and the source of the step-field catalogs..ai/harvest-i18n.py— the retired harvest of the Chinese values and its adjudication tables, and--present-catalog, which writes the step-field catalogs from the glossary.wwwroot-src/src/boot/i18n.ts— the plugin installation, the cached-locale path that holds mount for that locale's chunk and then fires an unawaited reconcile, the awaited race on a cache miss, and the guards around every locale step.wwwroot-src/quasar.config.ts— the boot list whose order places i18n after the authentication fetch patch and the product module's boot file last.wwwroot-src/src/product/boot.ts— the web service's own product boot file, which adds nothing.wwwroot-src/index.html— the static shell whoselangattribute is corrected at mount.wwwroot-src/package.json—buildas the i18n scripts followed byquasar build, andvue-tscas a separate script.scripts/lint-glossary.mjs— the locale list read fromSUPPORTED_LOCALES, the five bundle checks and the adjudication tables they read, the step-field catalog check, and the product module's bundles, locale wiring and namespace check underHINC_PRODUCT_SPA.scripts/census.mjs— the raw display-string extraction, the allowlist subtraction and the per-file ratchet.scripts/lint-engine-messages.mjs— the English engine-message entries checked against the emit-site catalog the engine ships.scripts/i18n-allowlist.json— the classified exemptions, each with its reason.scripts/i18n-baseline.json— the per-file residual ceiling the census compares against.wwwroot-src/src/stores/appState.ts— the language reference, the available-code list, and the action that POSTs before applying the locale, awaits its load, and rolls back on failure.wwwroot-src/src/api/preference.ts— the typed wrappers over the language endpoints and the?lang=-bearing step-present key request.wwwroot-src/src/api/http.ts—currentLang(), the coded-error map and the app-locale rendering that skips English.wwwroot-src/src/api/sessionMessages.ts— the engine-notification localizer, its template-equality gate and the repeat-fold wrapper.wwwroot-src/src/components/controlTree/itemTypes.ts— the node'slabel,labelKeyandlabelParamsfields and thenodeDisplayLabelandnodeDisplayInforesolvers.wwwroot-src/src/components/controlTree/useControlTreeHost.ts— 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.wwwroot-src/src/components/controlTree/missionItemTypes.ts— the kind display names and theirtree.mission.kind.*key twin, and the rule that drops the key whenever the entry carries a title.wwwroot-src/src/components/widgets/NumericInput.vue— the shared numeric field: blur-and-Enter commit, and the four validation messages it reads from thewidgets.numericInputkeys.wwwroot-src/src/components/execution/charts/BaseStripChart.vue,wwwroot-src/src/components/execution/charts/BaseCycleLineChart.vue,wwwroot-src/src/components/CsvStripPane.vue,wwwroot-src/src/components/controlTree/toolhouse/HolderSectionPanel.vueand the contour prompt inwwwroot-src/src/composables/useSpindleCapability.ts— the native number fields that take a decimal, each carryinglang="en".wwwroot-src/src/components/AppMenuBar.vue— the language submenu, its hard-coded self-name map, the action it calls and the toasts that report whether the pick was saved and its texts loaded.wwwroot-src/src/components/explorerSort.ts— the only code that compares through the shared collator, readingcollator.valueat compare time so a switch is picked up.wwwroot-src/src/components/FileExplorer.vue— watches the collator and re-sorts the loaded tree in place on a switch.wwwroot-src/src/components/execution/charts/UplotChart.vue— thetoFixeddefaults for every axis and series without a caller's formatter.wwwroot-src/src/utils/searchFold.ts—foldForSearch, the key both sides of a type-to-filter match are folded to.wwwroot-src/src/utils/imeKey.ts—isImeComposingandisCommitEnter, the keydown tests the client's own Enter commits and list-driving key handlers go through (Quasar's prompt dialogs and QSelect's new values keep Quasar's own).wwwroot-src/src/utils/numberText.ts—asciiNumberText, a number field's text in ASCII beforeNumber().wwwroot-src/src/router/index.ts— the key-valuedmeta.titleresolution and the hook the locale switch calls to re-title a parked tab.Environments/PreferenceController.cs— the language endpoints, the supported-code list in menu order and the 400 on an unlisted code.Environments/UserConfig.cs— the persisted language code, its English default and its XML round-trip.Environments/PresentCatalogService.cs— the per-request language resolution chain, the tag normalizer onto its closed set and the shipped catalog overlay.Missions/MissionController.cs— the per-request UI culture taken from the same chain and injected into command title composition.
See Also
- Language Selection SubMenu — the menu gesture that drives this mechanism, and the only place a user changes the locale
- Translation Remarks — the terminology contract the bundles are written to, and the readings the vocabulary lint enforces