How user-visible strings are stored, generated, and used across the project.
Roject uses a file-based locale system. Every user-visible string lives as a
plain file under locales/en/. On server start the generator walks
that tree and produces typed TypeScript classes into
src/locales/generated/. In code, strings are fetched at runtime
through LocaleManager using the generated path constants — for
example LocaleManager.$.get( Locales.FileTree.renameButton_txt ).
Rule: whenever you add a new user-visible string, add a locale file first, regenerate, then reference the constant — never hardcode the string in TypeScript directly.
All locale source files live under locales/en/. The en
directory is the English source of truth. One file = one string. The directory
hierarchy is free-form — organise by feature area.
.txt — raw UTF-8 text, used for plain labels and messages.
.html — HTML markup, used when the string contains tags.
.json — flexible format; must contain at least { "value": "…" }.
Extra fields are ignored by the loader but can carry metadata for tools.
.md)
Any locale file may have a sibling whose name is the full filename plus
.md. For example rename-button.txt may have a companion
rename-button.txt.md. These files are human-readable notes for
translators and contributors. The generator and the app ignore them completely —
they are never served or compiled.
The generator (server/localeGenerator.ts) runs automatically every
time the server starts via npm start. After adding, renaming, or
removing any locale file, restart the server to regenerate
src/locales/generated/. The generated files are checked into the
repo so the frontend TypeScript compiler can see them without a running server.
The generator mirrors the locales/en/ directory tree into
src/locales/generated/. Each directory produces exactly one
.ts file. The root directory produces
src/locales/generated/Locales.ts. Never edit generated files
manually — changes will be overwritten on the next server start.
locales/en/ → src/locales/generated/
greeting.txt → Locales.ts (member: greeting_txt)
commands/ → commands/
file-tree-commands/ → Commands.ts (member: FileTreeCommands)
add-file.txt → file-tree-commands/
→ FileTreeCommands.ts (member: addFile_txt)
Directory names are converted to PascalCase: dashes and underscores split words, each word is capitalised, the first letter is always uppercase regardless of the original name.
commands → Commands
file-tree-commands → FileTreeCommands
my_panel → MyPanel
File names keep the case of their first character. Dashes convert subsequent words to camelCase. The file extension is appended after an underscore, with dots in the extension also replaced by underscores.
add-file.txt → addFile_txt
greeting.txt → greeting_txt
MyFile.html → MyFile_html
config.min.json → config_min_json
Each generated class has static readonly members. File members hold
the locale path string (relative to locales/en/, extension included).
Subdirectory members import and re-expose the child class so the entire tree is
reachable from Locales.
// src/locales/generated/commands/file-tree-commands/FileTreeCommands.ts
// Auto-generated — do not edit manually.
export class FileTreeCommands
{
static readonly addFile_txt = 'commands/file-tree-commands/add-file.txt';
}
// src/locales/generated/commands/Commands.ts
// Auto-generated — do not edit manually.
import { FileTreeCommands } from './file-tree-commands/FileTreeCommands.js';
export class Commands
{
static readonly FileTreeCommands = FileTreeCommands;
}
// src/locales/generated/Locales.ts
// Auto-generated — do not edit manually.
import { Commands } from './commands/Commands.js';
export class Locales
{
static readonly Commands = Commands;
}
LocaleManager.$ is a static getter that returns the single instance.
No parentheses needed. Import from
src/locales/LocaleManager.ts (compiled to
public/locales/LocaleManager.js).
LocaleManager.$.get( path ) is async and returns
Promise<string>. Pass a generated path constant — never a raw
string literal. The result is cached by key after the first fetch; subsequent
calls return immediately from cache. If the server returns an error the method
falls back to returning the path string itself so the UI never breaks silently.
const label = await LocaleManager.$.get( Locales.Commands.FileTreeCommands.addFile_txt );
// → "Add file"
The manager detects the format from the file extension in the path.
.json files are parsed and data.value is returned.
All other extensions are returned as raw text.
The current locale defaults to 'en'. Switch with
LocaleManager.$.setLocale( 'de' ). After switching, call
LocaleManager.$.invalidateCache() to clear cached strings so
the next get call fetches from the new locale.
Choose a location under locales/en/ that matches the feature.
Create a .txt file whose name describes the string.
locales/en/file-tree/rename-button.txt
content: Rename
Optionally add a companion context file:
locales/en/file-tree/rename-button.txt.md
content: Label on the Rename entry in the file tree right-click context menu.
The generator runs on every npm start and creates or updates the
generated TypeScript files. After this step the following files exist:
npm start
src/locales/generated/file-tree/FileTree.ts ← created
src/locales/generated/Locales.ts ← updated (FileTree member added)
Compile the generated TypeScript so the browser can import the new constants.
npm run build
Import LocaleManager and Locales, then await the string.
Always import Locales (not individual child classes) so the full
path is visible at the call site and easy to trace back to the source file.
import { LocaleManager } from '../../locales/LocaleManager.js';
import { Locales } from '../../locales/generated/Locales.js';
const label = await LocaleManager.$.get( Locales.FileTree.renameButton_txt );
button.textContent = label;