Localization System
このコンテンツはまだ日本語訳がありません。
TomoriBot localizes user-facing text through locale files in src/locales/.
Supported Locales
Section titled “Supported Locales”Currently loaded from source:
en-US(default fallback)ja
Runtime Architecture
Section titled “Runtime Architecture”- Loader:
src/utils/text/localizer.ts - Locale structure:
src/locales/{locale}/directories, one.tsfile per category - Categories:
general,commands,providers,tools,bridges - At boot,
initializeLocalizer()scans each locale directory, imports all category slices, and merges them into a single tree viaObject.assign - Locale values are nested objects accessed through dot-path keys.
general.defaults.base_trigger_wordsis a string array used for locale-specific persona defaults. tools.intent_packsholds keyword lists that let non-English messages reach detectors whose built-in patterns are English:deliberate.<target>for each Deliberate Tool Mode target andexplicit_memoryfor explicit memory requests.src/utils/text/localeIntentPacks.tsunions every authored locale’s list instead of following the user’s preference, because members of a bilingual server type in either language. Entries are literal text with an optional trailing*word stem, no regex syntax, and at least two characters when written in Han, kana, or Hangul; those scripts match as substrings.getLocaleStringList()reads one locale’s list with no English fallback. English deliberate packs stay empty because the built-in patterns already cover English.- Directory names must be Discord locale codes. Unsupported names and alias-key directories are logged and skipped, so they cannot break command registration.
Example lookup:
localizer(locale, "commands.config.setup.description")Important Behaviors
Section titled “Important Behaviors”initializeLocalizer()must run during startup before lookups.- Locale lookup tries an exact authored code, then an alias, then an unambiguous base-language match, then
en-US. For example,es-ESuses the authoredes-419tree once that tree exists; unsupported codes use English. - Missing key falls back to
en-USfor that key alone (see below). - Panel route IDs accept Discord locale codes even when no translation is loaded. This keeps
controls usable for users whose Discord language is not yet authored; their text falls back to
en-US. - Multi-line strings are dedented automatically on load.
- Values interpolated into user-facing strings resolve through the same authored-locale chain, so their language matches the sentence around them: dates through
formatTimeWithOffset(date, offset, options, locale), durations throughformatLocalizedDuration(), and integer grouping throughformatLocaleInteger(). Durations useIntlunit formatting rather than locale keys because some languages have several plural forms. Model-facing text keeps English values:formatTimeRemaining(), tool results, and context builders that omit the locale. - Rendered strings used as embed protocol data are registered in
src/utils/discord/embedProtocol.ts. Startup builds one reverse lookup from the authored locale trees and fails on a title collision between distinct protocol keys. Template keys must retain the same placeholder names and counts across authored locales; target-title templates also need literal text around a placeholder. Reply-context field templates are matched only against their own embed fields, not against titles. Each released locale’s protocol-key values are immutable because Discord already stores unmarked historical embeds using those values.
New bot-produced protocol embeds carry a [tomori:v1:<kind>] footer token. The classifier reads the
token first, then uses the precomputed localized title lookup for older messages. Reset markers drop
the marker message from history; compact-refresh markers keep their summary message as the new
conversation opener. The Matrix relay serializes footer text along with title and description, so
the token remains in its plain-text copy. A Matrix /refresh also writes the token on its Discord
embed. Components V2 notices have no embeds and continue to use their reconstructed title text.
Key Resolution and the en-US Fallback
Section titled “Key Resolution and the en-US Fallback”localizer() resolves in three steps, and the middle one is what keeps an incomplete locale
usable:
| Case | Result |
|---|---|
| Key present in the requested locale | That locale’s string, with {placeholder} variables interpolated |
Key missing from the requested locale but present in en-US | The en-US string, with the same interpolation applied, plus one warn log |
| Key missing from both | The key path itself, returned verbatim |
The per-key retry means a locale that is 99% translated renders English for the remaining 1%
instead of showing users a raw commands.foo.bar_description path. The warn fires once per
locale:key per process, so a gap stays visible in development without flooding a hot path.
check-locales treats missing translations as an advisory exit 2 while the Japanese catch-up is
pending. A source key missing from every locale remains a blocking error.
bun run list-unused-locales reports keys without a known runtime consumer. Its scanner includes
TypeScript and TSX literals, template-built key namespaces, command metadata generated from the live
command tree, help guide key construction, runtime string lists, and the embed protocol registry.
Review each reported key against routes, tests, documentation generation, and persisted protocol
titles before deleting it. A key hidden from this report because of a dynamic namespace is protected
from automatic pruning; the namespace is not proof that every child is rendered.
getSupportedLocales() returns authored locale directories only. getRegisterableLocales() adds
aliases whose source tree is loaded, so command descriptions, option descriptions, and choice names
register under both es-419 and es-ES once Spanish content ships. The personal language control
lists authored locales, not aliases.
Command registration emits a locale-specific description or choice only when that authored locale defines the key; an English runtime fallback is not advertised as a translation. Panel route tokens accept any known Discord locale from a stored preference, including locales that currently render through English fallback.
Two consequences to keep in mind when writing new code:
- Do not infer key existence from the returned string. A miss now yields English, which is
indistinguishable from a real translation. Use
hasLocaleKey(locale, key)instead: it walks only the requested locale’s tree and never falls back.embedClassifier.tsdepends on this to decide which dynamically discovered reward and punish titles a locale actually defines. - The miss-everywhere case is unchanged, so callers that detect an unknown key by comparing
the result against the key still work.
openrouterStreamAdapter.tsandopenaiCompatibleErrorFormatter.tsuse that comparison, andst-preset/node/toggle.tsrelies on the verbatim echo to pass a dynamic label throughlocalizer().
getLocaleSubKeys(locale, path) deliberately enumerates only what the requested locale defines
and performs no fallback of any kind, so callers can pair it with hasLocaleKey to build a set
of keys that genuinely exist in one locale.
Locale File Shape
Section titled “Locale File Shape”Each locale exports a nested object (not a flat key-value map).
general.language_name is the locale’s endonym shown in /personal config > Profile > General.
general.defaults.bot_name and general.defaults.base_trigger_words own localized defaults.
Trigger-word reservation reads getAllBaseTriggerWords(), the union across authored locales, so an
alter can never claim the bot’s name in any shipped language.
BASE_TRIGGER_WORDS remains a global chat-trigger detection setting and does not supply the
locale-specific persona default list. The language modal uses a String Select, which holds at most
25 options; beyond that, the picker needs pagination or another selection flow.
export default { commands: { config: { setup: { description: "...", }, }, },};Slash Command Metadata Localization
Section titled “Slash Command Metadata Localization”commandLoader.ts auto-generates description/choice localizations from locale keys.
Recommended pattern in command files:
- set base metadata with
localizer("en-US", key) - do not hardcode localized
setDescriptionLocalizations(...)per command
Key conventions:
- Subcommand description:
commands.{category}.{path}.description
- Option description:
commands.{category}.{path}.{option_name}_description
- Choice label:
commands.{category}.{path}.{option_name}_choice_{choice_value}
Tip-item keys (genai.tips.*)
Section titled “Tip-item keys (genai.tips.*)”User-facing hints (“Tips”) are stored as atomic, single-sentence keys under genai.tips.*
(defined in providers.ts, which exports the genai tree). Each key is one self-contained bullet;
never a multi-hint paragraph:
genai: { tips: { title: "💡 Tip", wait_and_retry: "Please wait a few minutes before trying again.", openrouter_models: "Browse the [OpenRouter model list](https://openrouter.ai/models) and switch models with `/providers`.", // ...one key per bullet },}Callers compose the bullet list by passing an ordered tipKeys array to createTipText() (see
Tip modals), including or omitting keys per branch. This is why tips are
atomic: a conditional hint (e.g. an OpenRouter-only tip) is added by including its key in one branch,
not by duplicating a whole paragraph string. Descriptions render markdown and hyperlinks.
genai.tips.support_server is the one reserved key: createTipText() appends it as the closing
bullet of every rendered tip modal so the Official Support Server link is always offered. Do not put
it in a caller’s tipKeys array.
When adding a tip:
- Add the atomic key under
genai.tipsin bothsrc/locales/en-US/providers.tsandsrc/locales/ja/providers.ts. - Reference it by dot-path from the calling
tipKeysarray; do not inline hint text in code. - Run
bun run check-localesfor parity.
User Language Preference
Section titled “User Language Preference”- User preference is stored in
users.language_pref. - Registration writes the observed Discord locale to
language_prefandregistration_localefor a new user. A guild join uses the guild’s preferred locale; a slash-command registration uses the interaction locale. Chat registration uses the invoker locale when available, otherwise the guild locale. Existing preferences are not reset on registration.registration_localeis analytics data and never controls routing. - Unsupported stored preferences remain intact. The localizer resolves them at read time, so a matching authored locale begins serving those users when its content ships without a database rewrite.
/personal config> Profile > General lets a user change the preference explicitly, and/personal languageopens that same picker on its own. - Interaction replies receive
locale/userData.language_prefand should use that for response text./setupused to override it withinteraction.guildLocaleso the wizard read in the server’s language; it no longer does, because an admin reading the wizard in a language they did not choose is the more common case than a server whose admin does not speak its locale.
Adding or Changing Locale Keys
Section titled “Adding or Changing Locale Keys”- Update the appropriate category file under
src/locales/en-US/(e.g.commands.ts,general.ts). - Mirror the same key structure in the corresponding
src/locales/ja/file. - Run:
bun run check-localesThis validates cross-locale key parity and catches missing keys. Follow with the placeholder, marker, and link validation gates:
bun run check-locale-placeholders --locale=<target> # placeholder parity against en-USbun run check-locale-lengths # Discord 45/100 code-point capsbun run check-locale-markers # embed protocol key uniqueness and templatesbun run check-locale-links --locale=<target> # project doc routes and heading fragmentsUse --locale=<code> to validate a specific translation target. check-locale-links resolves a
project-owned route in the linking file’s own locale tree first and then in the default locale, so a
link to a page whose translation has not landed yet passes, and only a route that exists in neither
tree fails. Running it across the entire repository surfaces pre-existing Japanese catch-up debt (two
heading fragments whose Japanese pages use different headings), which is reconciled during the
Japanese catch-up phase.
Docs Destinations and the Second Locale Table
Section titled “Docs Destinations and the Second Locale Table”src/constants/docsLocales.ts lists the locales the docs site serves, separately from the authored
locale trees. The two are related but not the same: a locale can have runtime strings before its
documentation is translated, and the docs prefix has to exist as a route before the bot may link to
it.
src/utils/discord/docsLinks.tsandsrc/utils/misc/docsUrl.tsboth route throughbuildLocalizedDocsPath(), which prefixes an authored docs locale and otherwise returns the default locale.DOCS_PATHStherefore holds locale-less routes only.src/constants/locales.tsowns the Discord locale keys and thees-EStoes-419alias. The docs table inverts that same alias map, so one alias decision covers runtime strings and docs URLs.- Locale strings keep absolute docs URLs, including the prefix, because static text cannot call a
builder.
tests/unit/docs/docsRouteRegistry.test.tsresolves each of them againstdocs/.
Adding a docs locale, including the sidebar, hreflang, and README surfaces, is covered in Docs Site Localization.
Discord Length Limits
Section titled “Discord Length Limits”Discord silently truncates several text slots past their cap, so a separate strict gate
(bun run check-locale-lengths, also run as a fatal step in bun run vl) source-traces each
locale key to the Discord component it feeds and flags any value that overruns:
- Command descriptions and choice names: ≤100 chars
- Modal titles / input labels: ≤45 chars
- Modal placeholders / Label descriptions: ≤100 chars
- Select / checkbox option
labelanddescription: ≤100 chars (traced from{ value, label: localizer(...), description: localizer(...) }option literals)
Both the en-US and ja values must fit. Shorten the reported string rather than relying on
Discord’s truncation: the cap is counted in characters (code points), matching Discord backend
measurements, so compact Japanese text usually fits where English does not.
Best Practices
Section titled “Best Practices”- Localize all user-facing command/embed strings.
- Keep command metadata keys aligned with command path conventions.
- Avoid hardcoded strings in command implementations.