Table of Contents

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 locale ref;
  • 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 localStorage under hinc.lang, inside a try/catch because 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 hook applyLocale calls.
  • 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) includes foldForSearch(query) — both sides folded. The fold decomposes (NFKD), drops the combining marks except the kana voicing marks, composes again, turns katakana into hiragana, maps đ to d and 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) find STL; 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 keyup Enter. 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 through isCommitEnter, 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's isComposing, 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 through asciiNumberText before Number(): an IME in full-width mode types 0.5 and -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 sends 0.5 and -3. Nothing else is folded — not the whole text through NFKC, which would read 10² as 102 — 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 English engineMessages values 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:

  1. Locales and key isomorphism. Every listed locale needs its folder and index file and, English aside, a literal import() of that folder in LOCALE_LOADERS, and a folder holding a bundle must be listed: quasar build notices neither omission, and the locale then never shows. One that LOCALE_LOADERS leaves 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 language normalizeLocale has no rule for, has not even the warning — normalizeLocale gives its code null, and applyLocale applies 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.
  2. 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.key 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.
  3. 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.
  4. 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.
  5. 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 |.
  6. Step-field catalogs. Every name and short name in the catalog.{lang}.json files of the HiNc-Resource package's StepPresentCatalog folder, for each locale but English, must be NFC and clear of that locale's forbidden-term table. The folder is looked for the way lint-engine-messages.mjs looks 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, the vi and ja packs' replaced texts), loadLocale and its four-second cap, applyLocale with 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 as MessageSchema.
  • 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.ts and wwwroot-src/src/i18n/ja/index.ts — the same assembly, each annotated with MessageSchema and 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.title keys 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 whose lang attribute is corrected at mount.
  • wwwroot-src/package.json — build as the i18n scripts followed by quasar build, and vue-tsc as a separate script.
  • scripts/lint-glossary.mjs — the locale list read from SUPPORTED_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 under HINC_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's label, labelKey and labelParams fields and the nodeDisplayLabel and nodeDisplayInfo resolvers.
  • 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 their tree.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 the widgets.numericInput keys.
  • 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.vue and the contour prompt in wwwroot-src/src/composables/useSpindleCapability.ts — the native number fields that take a decimal, each carrying lang="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, reading collator.value at 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 — the toFixed defaults 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 — isImeComposing and isCommitEnter, 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 before Number().
  • wwwroot-src/src/router/index.ts — the key-valued meta.title resolution 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