Skip to content
cccskills
FreePublished on GitHub

i18n

Localize every user-visible string as you write it — the portal ships English + German, so a hard-coded UI string is a bug that renders English for every viewer. Use whenever you write or review text a human reads on screen: buttons, labels, tooltips, aria-labels, placeholders, page titles, empty states, validation messages, toasts, dialog copy, menu entries, settings tabs, notifications, errors. Covers the two shapes (a Translation attribute on a declaration, a strings.en/de key everywhere else), the second key home in the plugins repo whose drift guard compares VALUES and merges core-first, the CurrentUICulture ban (it covers date and number FORMATTING too), and the list of things deliberately NOT translated.

The English summary is being prepared. Showing the original description.

Systemorph/MeshWeaver12 stars(GitHub 12 + cccskills 0)

Run score Not tested

See how to install

Files included (1)

  • SKILL.md11.8 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

/i18n — how does this render for a German viewer?

Before you write ANY text a user will read, stop and ask that question. This is not a final-polish step or a follow-up ticket — it is part of writing the feature, the same way a null check is. The portal ships English + German; a hard-coded UI string is a bug, because it renders English for every viewer regardless of their language.

This applies to every surface, not just obvious ones: buttons, labels, tooltips, aria-labels, placeholders, page titles, empty states, validation messages, toasts, dialog copy, menu entries, settings tabs, notification text, and error strings. If a human reads it on screen, it needs a key or a [Translation].

Canonical reference: Localization.md

Prefer text that doesn't need translating at all

A language-neutral glyph beats a translated word: the AI menu's "new thread" entry uses ➕, and the node menu uses ✏️ 🔖 ➡️ 📋 🗑️. An icon plus a translated tooltip is usually better than a translated label — it shrinks the translation surface and reads identically in every locale. (The tooltip still needs a key.)

🚨 Inside authored content that preference is BINDING — clause 2

The rule was settled by #3203 and lives in ChromeAndContentLanguage.md:

Clause 1 — ownership decides the language. Platform-/module-owned text (a catalog key, a module text table, a [Translation]) follows the viewer. Authored content renders as authored. A bare literal compiled into a view is unowned — that is a bug, not a third category.

Clause 2 — in-flow chrome minimises words. A platform/module control rendered inside the author's flow carries no translated visible label where a glyph, a number or a symbol conveys the same thing. The localized text moves to the tooltip and the accessible name.

The symptom this exists for: a German-profile reader of an English lesson met Ausführen on the code cell's Run button, mid-English-paragraph. The button already had a ▶ glyph and an already localized Title; the word added nothing.

Two things this does NOT license:

  • 🚨 Dropping a visible label without an accessible name. A tooltip is the control's accessible name — a glyph-only control keeps a localized Title / aria-label, or you have traded a language bug for an accessibility one.
  • 🚨 Making in-content chrome follow the CONTENT's language. That is #3203's option (a), and it is declined: the Edu pack shipped it, measured it and reverted it (EduTexts.ResolveChrome's comment: "do not restore the old order"), because its worst reader is an English speaker facing a German course whose every control is in a language they do not have.

Clause 2 reaches an enumerated list of in-flow surfaces — the code-cell toolbars, the fenced block's copy affordance, the kernel placeholders inside the cell frame, Edu/** — not the shell. A German node menu around an English document is not an inconsistency and keeps its words.

Two shapes — pick by whether the text hangs off a declaration

  • On a declaration (property label, node-type name, enum member) → add [Translation("de", "…")] next to the existing [Description]. Nothing else to wire; the translation rides beside the English so they cannot drift.
  • Everywhere else (Blazor markup, inline Controls.*, toasts) → add a key to both src/MeshWeaver.Messaging.Hub/Localization/strings.{en,de}.json and read it via Access.Localize("key") (Blazor, @inject AccessService Access) or host.Localize("key") (layout areas).
// Server-side layout area
Controls.Button(host.Localize("ui.createRelease"))
stack.WithView(Controls.Body(host.Localize("ui.noCodeDefined")))

// Blazor view
@inject AccessService Access
<button title="@Access.Localize("common.close")">…</button>

// On a declaration
[Description("Sync direction")]
[Translation("de", "Synchronisierungsrichtung")]
public SyncDirection Direction { get; init; }

LocalizationTest fails if a language is missing any English key, so a half-translated string cannot merge — and it is now the ONLY localization guard in this repo.

🚨 A new key has a SECOND home — in another repo

The web clients' catalog lives at MeshWeaver.Plugins/clients/react/src/i18n/. 044a85618 ("The GUI leaves the platform", 2026-08-25) moved the web clients out; clients/ in core now holds only python/ (voice-gateway/ followed to MeshWeaver.Plugins clients/ in the core slimming), and the six guards the old text and the Clients workflow named — i18n/localize.test.ts, render/parity.test.ts, ciTrigger.test.ts, grpc-web/src/{wire,rest}Contract.test.ts, portal-next/e2e — no longer exist in this repo. Do not recreate them here; the mirror belongs where the client lives.

🚨 The REQUIREMENT survived the move. The guidance briefly read "That is gone … do not go looking for the mirror", which is true of the directory and false of the rule. On 2026-08-27 that cost two keys: they were added server-side, the mirror was looked for in core, its absence was read as deletion, and "there is no mirror" went into a merged PR body. The mirror was three keys behind by then. Deleted and relocated look identical from one repo — assume relocated until you have looked in the other one.

✅ The drift guard DOES run — this paragraph used to say it did not, and that was the dangerous half. It read "nothing currently runs those specs … the drift guard has been reporting to no one" (from MeshWeaver.Plugins#771). That lane now exists: the plugins repo's RN app + web clients (typecheck + test) job runs src/i18n/localize.test.ts, and on 2026-08-28 it caught a real drift within minutes of the push — a composition.column.provision value changed in the client catalog but not in core's:

FAIL src/i18n/localize.test.ts > catalog drift guard > strings.en.json is identical to the server catalog
AssertionError: value drift for "composition.column.provision": expected 'Install as' to be 'Provision as'

Core is the source of truth, so the order is fixed: the core catalog change merges FIRST, and only then can the mirror point at it. Do not "fix" a mirror red by reverting the mirror — it is the guard doing its job across a repo boundary.

🚨 But adding a key in core does NOT turn the plugins repo red, and expecting it to is how a mirror goes months un-synced. The guard compares the client catalog against a PINNED core commit recorded in clients/react/src/i18n/catalog-source.json — {repository, catalogDir, ref, keys} — not against core's main. That pin is deliberate (MeshWeaver.Plugins#893): core merges faster than a Plugins CI cycle, so an unpinned guard could not converge and reddened every unrelated PR on a subsystem its diff could not reach. The consequence to hold onto:

A core catalog change makes the mirror STALE, silently. Nothing anywhere goes red.

Measured 2026-09-04: the pin was e7f1d69 (2026-09-03) at 1104 keys while core's main carried 1174 — 70 keys behind, every gate green in both repos. So a core PR that adds keys must say so and hand over the sync; the mirror moves by a deliberate

cd clients/react && npm run sync:i18n -- --ref <the merged core sha>

which rewrites both strings.{en,de}.json, the ref, and the keys counts together (a separate assertion pins the counts, so a truncated sync shows up in the diff).

🚨 The guard compares VALUES, not just keys — so it also catches the case LocalizationTest cannot see: a key present in both catalogs whose text has diverged.

To run it by hand from the plugins checkout (it fails loudly, naming the paths it probed, when it cannot find a core checkout — so a green there is real and a skip is impossible):

cd clients/react && MESHWEAVER_CORE=/path/to/MeshWeaver npx vitest run src/i18n/localize.test.ts

🚨 Never resolve from CultureInfo.CurrentUICulture / CurrentCulture

This covers formatting (dates, numbers, calendars), not just translated strings. Two independent reasons:

  1. A layout-area render hops the hub scheduler and an ambient AsyncLocal culture does not survive it, so one user's UI would pick up another user's language.
  2. On Blazor Server the ambient culture is the server process's — i.e. the container the portal runs in — identical for every simultaneous viewer and unrelated to any of them.

DateTimeView defaulted its calendar culture that way until 2026-08-17 and rendered English month names for German users regardless of their choice. Resolution is always explicit off AccessContext.Locale (AccessService.ViewerLocale() / host.ViewerLocale()); derive a CultureInfo from that when you need one.

The language policy

Take the language of the USER's computer, and put it on the user. The onboarding form's first field is a language picker pre-selected from the visitor's own Accept-Language; it writes User.Locale at user creation. It is then editable in two places sharing ONE control (MeshNodeContentEditorControl over User.Locale): the profile editor and User → Settings → Preferences. A silent auto-detect of an unshipped language stores nothing (so a later translation applies automatically); an explicit pick is always honoured — Locales.TryMatch vs Locales.Resolve is that distinction in code.

🚨 Do NOT translate

  • LLM tool-parameter [Description]s — model-facing; translating degrades tool-calling.
  • Wire identifiers — nodeType:Thread in help text, RequestAction("New"), Fluent icon names.
  • The glossary terms kept English on purpose: Thread, Mesh, Node, Agent, Skill, Harness, Provider, Namespace, Partition, Store.

Adding a language is cheap by design

A tag in Locales.Supported, a strings.{tag}.json, and the matching [Translation] attributes. Keep it that way: never scatter a second resolution mechanism, and never let a string bypass the catalog "just this once".

Checklist

  • Every new user-visible string is a [Translation] on its declaration or a key in both strings.en.json and strings.de.json.
  • Every string has an OWNER — platform catalog, module table, or the author. No bare literal compiled into a .razor/.cs view.
  • A control rendered INSIDE authored content carries a glyph, not a translated word — and keeps a localized Title/aria-label as its accessible name.
  • Nothing resolves culture from CurrentUICulture/CurrentCulture — including date/number formatting.
  • If the string also appears in a web client, the mirror in MeshWeaver.Plugins/clients/react/src/i18n/ was updated — and the core change merges first.
  • No model-facing description, wire identifier, or glossary term was translated.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

async

Free

Forwarding AccessContext through async/reactive boundaries, and the no-async rule on the actor-model mesh. Use when writing or reviewing any hub-reachable / Blazor-view / agent-round / IIoPool code that creates, reads, or updates a mesh node, OR when a write "silently does nothing" / "Access denied" appears after a .Subscribe / IIoPool / Observable.Create hop. The async/await/ToTask/.Wait/.Result/.GetAwaiter/FirstAsync family and lost AccessContext are the two ways async boundaries cause wedges and storms. Grounded in AsynchronousCalls.md, SyncedMeshNodeQueries.md, AccessContextPropagation.md.

The English summary is being prepared. Showing the original description.

Systemorph/MeshWeaver12Updated Oct 29, 2026

ci

Free

What CI proves, what it cannot prove, and how to write a gate that cannot pass on no evidence. Use when authoring or changing a GitHub Actions workflow, adding a required check, wiring a satellite repo CI, or reasoning about whether a green run actually covers your change. Covers the absolute no-skip-trapdoor rule (a gate must never carry continue-on-error on its input step nor an if that asks whether a secret is set — GitHub paints a skipped job the same colour as a passed one), the reusable workflow_call jobs satellite repos must call instead of hand-rolling, why runs on main are deliberately never cancelled, and the large body of in-mesh C# that no dotnet build or test ever type-checks.

The English summary is being prepared. Showing the original description.

Systemorph/MeshWeaver12Updated Oct 29, 2026

debug

Free

Debug a deadlock / hang / timeout in the mesh — a test that times out, a hub handler whose response never arrives, a GetRemoteStream / GetMeshNodeStream / GetQuery that never emits, an agent round or per-instance-hub activation that parks forever. Use whenever something times out and you need to find WHERE the message flow breaks down. The timeout is the symptom, never the bug — raising it just hangs longer. Grounded in DebuggingMessageFlow.md.

The English summary is being prepared. Showing the original description.

Systemorph/MeshWeaver12Updated Oct 29, 2026

Remove / reset a user (and their partition) on a running MeshWeaver portal (AKS). Use when you need to kick out a user — e.g. a throwaway test account — so they re-onboard from scratch. The normal delete API REFUSES a user's home ("user partition root … cannot be deleted"), so deletion is a Postgres-schema drop, not an MCP call. Covers where a user's data actually lives (per-user PG schemas named by BOTH id AND email, e.g. `rbuergi` + `rbuergi@systemorph.com`), how to reach PG (the connection string is inline in the portal's `ConnectionStrings__memex` env — NOT the `POSTGRES_PASSWORD` secret), the substring-match footgun (`buergi` also matches admin `rbuergi`), the in-memory resurrection gotcha (a recycle re-saves the node — a portal restart is what actually clears it), and the half-onboarded case where the user has NO schema at all (in-memory only → restart is the whole fix).

The English summary is being prepared. Showing the original description.

Systemorph/MeshWeaver12Updated Oct 29, 2026

deploy

Free

Deploy MeshWeaver — the two routes (shared AKS cluster, and Azure Container Apps via the Aspire test/prod modes) and the traps that make a deploy look done when it is not. Use when rolling a code change onto a portal, standing up or patching an environment, or verifying that what is running is what you built. FIRST RULE (maintainer, 2026-09-08): on AKS an operation is a Hosting/InstanceAction on the control instance (Roll, Restart, Suspend, Audit, Reconcile, …) executed by the in-cluster operator — az aks command invoke / kubectl are break-glass, never the procedure. Also covers the bootstrap build-image / set-image / restart sequence the operator runs for you, why an env deploy.sh is first-time setup and not a code-update path, why the database migration is a run-once Job that a helm upgrade runs — never a Deployment to roll — and the DbVersionGate that makes a portal refuse to serve ahead of its schema.

The English summary is being prepared. Showing the original description.

Systemorph/MeshWeaver12Updated Oct 29, 2026

gui

Free

Build or review ANY MeshWeaver UI — layout areas, editable views, forms, tables, content editing, side-panel composers — by wiring the framework's EXISTING data-binding pieces, NEVER hand-rolling. The one rule is ALWAYS DATA-BIND, especially editable views: the backend layout area declares WHAT to render (a UiControl tree + node PATHS), and every value read / write-back happens on the GUI side through the per-node IMeshNodeStreamCache. Hand-woven binding — replicating a node into a /data/{id} copy + a save subscription, emitting HTML strings, hand-built selects/checkboxes, .Take(1) on a live stream, async/await/QueryAsync in a layout area — is the bug class behind "awaiting first data" / "renders empty" / a stuck editable view. Use when writing or reviewing a layout area, control, editor, form, table, or any data-bound view. Grounded in src/MeshWeaver.Layout/DataBinding/, Doc/GUI/DataBinding.md + Editor.md, the Edit extension (EditorExtensions.cs), and the node-bound editor controls.

The English summary is being prepared. Showing the original description.

Systemorph/MeshWeaver12Updated Oct 29, 2026

See all skills by Systemorph

Report a problem with this skill