rojects/workspace/guides/locales/index.html

273 lines
10 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Locales — Roject</title>
<link rel="stylesheet" href="../../_assets_/styles.css">
<link rel="stylesheet" href="../../_assets_/nav.css">
</head>
<body>
<div class="page">
<header>
<h1>Locales</h1>
<p class="subtitle">How user-visible strings are stored, generated, and used across the project.</p>
</header>
<section>
<h2>Summary</h2>
<div class="card">
<p>
Roject uses a file-based locale system. Every user-visible string lives as a
plain file under <code>locales/en/</code>. On server start the generator walks
that tree and produces typed TypeScript classes into
<code>src/locales/generated/</code>. In code, strings are fetched at runtime
through <code>LocaleManager</code> using the generated path constants — for
example <code>LocaleManager.$.get( Locales.FileTree.renameButton_txt )</code>.
</p>
<p style="margin-top:0.75rem">
<strong>Rule:</strong> 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.
</p>
</div>
</section>
<section>
<h2>Locale Source Files</h2>
<div class="decision">
<strong>Location</strong>
<p>
All locale source files live under <code>locales/en/</code>. The <code>en</code>
directory is the English source of truth. One file = one string. The directory
hierarchy is free-form — organise by feature area.
</p>
</div>
<div class="decision">
<strong>Supported formats</strong>
<p>
<code>.txt</code> — raw UTF-8 text, used for plain labels and messages.<br>
<code>.html</code> — HTML markup, used when the string contains tags.<br>
<code>.json</code> — flexible format; must contain at least <code>{ "value": "…" }</code>.
Extra fields are ignored by the loader but can carry metadata for tools.
</p>
</div>
<div class="decision">
<strong>Companion context files (<code>.md</code>)</strong>
<p>
Any locale file may have a sibling whose name is the full filename plus
<code>.md</code>. For example <code>rename-button.txt</code> may have a companion
<code>rename-button.txt.md</code>. These files are human-readable notes for
translators and contributors. The generator and the app ignore them completely —
they are never served or compiled.
</p>
</div>
</section>
<section>
<h2>Code Generation</h2>
<div class="decision">
<strong>When it runs</strong>
<p>
The generator (<code>server/localeGenerator.ts</code>) runs automatically every
time the server starts via <code>npm start</code>. After adding, renaming, or
removing any locale file, restart the server to regenerate
<code>src/locales/generated/</code>. The generated files are checked into the
repo so the frontend TypeScript compiler can see them without a running server.
</p>
</div>
<div class="decision">
<strong>Output structure</strong>
<p>
The generator mirrors the <code>locales/en/</code> directory tree into
<code>src/locales/generated/</code>. Each directory produces exactly one
<code>.ts</code> file. The root directory produces
<code>src/locales/generated/Locales.ts</code>. Never edit generated files
manually — changes will be overwritten on the next server start.
</p>
<pre><code>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)</code></pre>
</div>
<div class="decision">
<strong>Naming — class names (directories)</strong>
<p>
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.
</p>
<pre><code>commands → Commands
file-tree-commands → FileTreeCommands
my_panel → MyPanel</code></pre>
</div>
<div class="decision">
<strong>Naming — member names (files)</strong>
<p>
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.
</p>
<pre><code>add-file.txt → addFile_txt
greeting.txt → greeting_txt
MyFile.html → MyFile_html
config.min.json → config_min_json</code></pre>
</div>
<div class="decision">
<strong>Generated class shape</strong>
<p>
Each generated class has <code>static readonly</code> members. File members hold
the locale path string (relative to <code>locales/en/</code>, extension included).
Subdirectory members import and re-expose the child class so the entire tree is
reachable from <code>Locales</code>.
</p>
<pre><code>// 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;
}</code></pre>
</div>
</section>
<section>
<h2>LocaleManager API</h2>
<div class="decision">
<strong>Singleton access</strong>
<p>
<code>LocaleManager.$</code> is a static getter that returns the single instance.
No parentheses needed. Import from
<code>src/locales/LocaleManager.ts</code> (compiled to
<code>public/locales/LocaleManager.js</code>).
</p>
</div>
<div class="decision">
<strong>Fetching a string</strong>
<p>
<code>LocaleManager.$.get( path )</code> is async and returns
<code>Promise&lt;string&gt;</code>. 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.
</p>
<pre><code>const label = await LocaleManager.$.get( Locales.Commands.FileTreeCommands.addFile_txt );
// → "Add file"</code></pre>
</div>
<div class="decision">
<strong>Format handling</strong>
<p>
The manager detects the format from the file extension in the path.
<code>.json</code> files are parsed and <code>data.value</code> is returned.
All other extensions are returned as raw text.
</p>
</div>
<div class="decision">
<strong>Changing the active locale</strong>
<p>
The current locale defaults to <code>'en'</code>. Switch with
<code>LocaleManager.$.setLocale( 'de' )</code>. After switching, call
<code>LocaleManager.$.invalidateCache()</code> to clear cached strings so
the next <code>get</code> call fetches from the new locale.
</p>
</div>
</section>
<section>
<h2>How To — Add and Use a Locale String</h2>
<div class="card">
<h3>Step 1 — Create the locale file</h3>
<p>
Choose a location under <code>locales/en/</code> that matches the feature.
Create a <code>.txt</code> file whose name describes the string.
</p>
<pre><code>locales/en/file-tree/rename-button.txt
content: Rename</code></pre>
<p style="margin-top:0.75rem">
Optionally add a companion context file:
</p>
<pre><code>locales/en/file-tree/rename-button.txt.md
content: Label on the Rename entry in the file tree right-click context menu.</code></pre>
</div>
<div class="card">
<h3>Step 2 — Restart the server to regenerate</h3>
<p>
The generator runs on every <code>npm start</code> and creates or updates the
generated TypeScript files. After this step the following files exist:
</p>
<pre><code>npm start
src/locales/generated/file-tree/FileTree.ts ← created
src/locales/generated/Locales.ts ← updated (FileTree member added)</code></pre>
</div>
<div class="card">
<h3>Step 3 — Build</h3>
<p>
Compile the generated TypeScript so the browser can import the new constants.
</p>
<pre><code>npm run build</code></pre>
</div>
<div class="card">
<h3>Step 4 — Use in TypeScript</h3>
<p>
Import <code>LocaleManager</code> and <code>Locales</code>, then await the string.
Always import <code>Locales</code> (not individual child classes) so the full
path is visible at the call site and easy to trace back to the source file.
</p>
<pre><code>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;</code></pre>
</div>
</section>
<footer>
Roject &mdash; locales
</footer>
</div>
<script>var NAV_ROOT = '../../';</script>
<script src="../../_assets_/nav-data.js"></script>
<script src="../../_assets_/nav.js"></script>
</body>
</html>