Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
18c3bfd
Make shared-webapp build plugins ESM-safe
raix Jul 2, 2026
06a52c7
Convert i18n.config.json to i18n.config.ts
raix Jul 2, 2026
19553d5
Build shared-webapp libraries with rslib and consume them from dist
raix Jul 2, 2026
76e0c50
Share @lingui/core and @lingui/react as module-federation singletons
raix Jul 2, 2026
7b7e184
Decouple federated translations via self-registration
raix Jul 2, 2026
b4882a8
Extract MobileMenuDialogs into its own file
raix Jul 2, 2026
6a2eda9
Make @repo/ui translations self-contained via a RepoUiTranslation pro…
raix Jul 2, 2026
6b420a4
Adopt Lingui context for translation disambiguation
raix Jul 2, 2026
53176f4
Document the frontend build system and deferred TypeScript 7 and Reac…
raix Jul 2, 2026
6bdbd70
Route HMR WebSocket through the AppGateway
raix Jul 3, 2026
687f420
Add Lingui i18n, SWC plugin compatibility, and message context skills
raix Jul 3, 2026
66399ed
Ignore local aspire.config.json
raix Jul 3, 2026
6534ceb
Centralize preferred-locale persistence in setLocale
raix Jul 4, 2026
11f5f8f
Clear the stored locale preference when it is the default
raix Jul 4, 2026
e44bc06
Apply the stored locale before first render for anonymous users
raix Jul 4, 2026
18bcf9c
Always persist the explicit locale choice
raix Jul 4, 2026
8e5b294
Harden translation catalog loading against switch races, load failure…
raix Jul 4, 2026
ebe3222
Do not gate host content on the account remote's translations
raix Jul 4, 2026
178497e
Validate and de-duplicate anonymous locale re-application
raix Jul 4, 2026
478080e
Derive the active locale from the i18n singleton in the account langu…
raix Jul 4, 2026
7029917
Remove dead federated translation exposes
raix Jul 4, 2026
43360ce
Derive the active locale from the i18n singleton in user preferences
raix Jul 4, 2026
921385c
Remove dead translation-expose branch from federation type generation
raix Jul 4, 2026
ff22078
Restore environment type-checking and clean up rslib library configs
tjementum Jul 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .claude/rules/frontend/build-system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
paths: application/shared-webapp/*/rslib.config.ts,application/*/WebApp/rsbuild.config.ts,application/*/BackOffice/rsbuild.config.ts,application/shared-webapp/build/plugin/*.ts,application/package.json
description: How the frontend build system is wired (rslib libraries, rsbuild apps, module federation) and which build-tool upgrades are intentionally deferred
---

# Frontend Build System

The shared libraries under `application/shared-webapp/` (`@repo/ui`, `@repo/infrastructure`, `@repo/utils`,
`@repo/build`) are built with **rslib** (ESM, bundleless) and consumed from their `dist` output. The three
SPAs (`main/WebApp`, `account/WebApp`, `account/BackOffice`) are built with **rsbuild**, and **module
federation lives only in the WebApp rsbuild builds** — rslib builds plain libraries, it does not produce
federation remotes. Understand these constraints before changing any `rslib.config.ts`, an app's
`rsbuild.config.ts`, or the toolchain versions in `application/package.json`.

## Implementation

1. **Build shared libraries with rslib, consume from `dist`.** Each library has an `rslib.config.ts`
(`bundle: false`, `format: "esm"`, `dts: true`). Their `package.json` `exports` map subpaths into
`dist`; there is no `source` export condition and the WebApps do not use `pluginSourceBuild`. Turbo's
`^build` ordering builds libraries before the apps.

2. **Run the Lingui macro transform inside any rslib library that uses macros.** Because a library is
pre-built (not transformed by the consuming app), its `rslib.config.ts` must include `LinguiPlugin()`
(which injects `@lingui/swc-plugin`) in `plugins`, or `<Trans>` / `` t` ` `` macros ship untransformed
and throw `Trans is not defined` / `t is not defined` at runtime. Today only `@repo/ui` uses macros, so
only its config needs it — add `LinguiPlugin()` to any other library that introduces Lingui macros.
`@lingui/swc-plugin` runs against rspack's embedded SWC (now in both the WebApp rsbuild builds and this
rslib build), and the SWC plugin API is not semver-stable — it's pinned to an exact version, so re-check
compatibility (plugins.swc.rs) whenever `@rspack/binding` or `@lingui/swc-plugin` is bumped, or the build
fails with `LayoutError` / "failed to run Wasm plugin transform".

3. **Give rslib the asset extension for images imported by library components.** Raster/vector assets a
library component imports (e.g. `@repo/ui` `Logo.tsx` importing `../images/*.png`) must have their
extension in the library's rslib `source.entry.index` glob (`./**/*.png`, `./**/*.webp`, `./**/*.svg`)
so rslib emits an asset module and rewrites the import correctly. Package-path imports from apps
(`@repo/ui/images/x.webp`) resolve through the `./*.webp` / `./*.png` `exports`; rslib emits the raw
assets there unhashed via `output.distPath.image: "images"` + `output.filename.image: "[name][ext]"` (no
hand-rolled copy step). Node-side build code must be ESM-safe (`import.meta.dirname`,
not `__dirname`; read JSON with `fs`, not `require()`).

4. **Keep `react`, `react-dom`, `@lingui/core`, `@lingui/react` as module-federation `shared` singletons**
(`application/shared-webapp/build/plugin/ModuleFederationPlugin.ts`). The translation system depends on a
single shared Lingui `i18n` across remotes — see [translations](/.claude/rules/frontend/translations.md).

5. **TypeScript 7 is intentionally deferred — stay on the 6.x line.** TS 7's native compiler removed the
classic programmatic API (its `package.json` exposes no main export, only `./unstable/*`), so
`openapi-typescript` (the `swagger` codegen) and rslib's `dts` generator both fail to load it, and it
cannot be the root `typescript`. Only the `tsc` CLI works. Revisit when those tools support the native
compiler (≈ TS 7.1). If a TS 7 typecheck-only gate is wanted before then, run it in parallel via `tsgo`
from `@typescript/native-preview` (a separate binary, no `typescript` peer conflict) — do **not** bump
the root `typescript` to 7.

6. **The Rust React Compiler is deferred to a follow-up PR.** It is the official React Compiler ported to
Rust and integrated via SWC, shipped in **Rspack/Rsbuild 2.1**; this repo is on 2.0.x. To adopt: bump
`@rsbuild/core` and `@rspack/binding` to 2.1, enable it on `@rsbuild/plugin-react` (or via
`builtin:swc-loader` `jsc.transform.reactCompiler`), and validate the component set. The federation
singletons in step 4 are already the prerequisite.

7. **After changing rules under `.claude/`, run `dotnet run --project developer-cli -- sync-ai-rules --quiet`.**

## Examples

### Example 1 - Lingui macros in an rslib library config

```ts
// ✅ DO: a library that uses <Trans>/t must run the Lingui SWC plugin during its own build
import { LinguiPlugin } from "@repo/build/plugin/LinguiPlugin";
import { pluginReact } from "@rsbuild/plugin-react";
import { defineConfig } from "@rslib/core";

export default defineConfig({
source: { entry: { index: ["./**/*.tsx", "./**/*.ts", "./**/*.svg", "./**/*.css", "./**/*.png", "!rslib.config.ts", "!node_modules/**", "!dist/**"] } },
lib: [{ bundle: false, dts: true, format: "esm" }],
output: { target: "web" },
plugins: [pluginReact(), LinguiPlugin()]
});

// ❌ DON'T: omit LinguiPlugin() — macros ship untransformed → "Trans is not defined" at runtime
```

### Example 2 - Enabling the React Compiler later (Rsbuild/Rspack 2.1)

```ts
// After bumping @rsbuild/core and @rspack/binding to 2.1:
pluginReact({ reactCompiler: true });
```
5 changes: 3 additions & 2 deletions .claude/rules/frontend/translations.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@ Guidelines for implementing translations and internationalization in the fronten
6. **Catalogs are split between the shared component library and each self-contained system**:
- Markers in `application/shared-webapp/ui/**` extract to `application/shared-webapp/ui/translations/locale/{locale}.po` — one shared catalog used by every app
- Markers in `application/<system>/WebApp/**` extract to that system's `shared/translations/locale/{locale}.po`
- At runtime, `createFederatedTranslation` merges them: shared underneath, system on top, federated remote (e.g. account loaded into main) on top of that
- At runtime each system self-registers its catalog into one shared Lingui `i18n` (`Translation.create` for a host SPA; `withSystemTranslations` for a federated module) and they merge: shared UI underneath, host system on top, federated remote (e.g. account loaded into main) on top of that. `@lingui/core`/`@lingui/react` are module-federation singletons so every remote reads the same merged dictionary
- Translate each shared string **once** in the shared catalog, not in every system's catalog
- Prefer Lingui macros inside `shared-webapp/ui` components over threading translatable text through props -- only accept a text prop when a consumer genuinely needs to override the default (e.g., context-specific wording)
- **Disambiguate colliding strings with `context`**: the merged dictionary keys by message ID (a hash of source text **+ `context`**), so identical bare source strings share one translation. When the same word must translate differently by area (e.g. "Account" in the auth flow vs. in the product), add `context` -- either per marker (`<Trans context="auth">Account</Trans>`, `` t({ message: "Account", context: "auth" }) ``) or once for a block/file with a Lingui **Context Directive** (`// lingui-set context="auth"` … `// lingui-reset`; supported by `@lingui/swc-plugin`). `context` feeds the ID hash so both translations coexist in the one dictionary. Don't blanket-namespace a whole system with `context` -- that defeats sharing of genuinely-common strings; use it only where meaning actually diverges
7. Translation workflow:
- After adding/changing `<Trans>` or t\`\` markers, the `*.po` files are auto-regenerated on build
- Don't manually add or remove `msgid` entries -- only translate `msgstr` values
- After auto-generation, translate all new/updated entries for all supported languages
8. **Use correct language-specific characters** in translations -- e.g., Danish requires æøå/ÆØÅ, not ae/oe/aa substitutes. Never approximate with ASCII equivalents
9. Don't translate fully dynamic content such as variable values or dynamic text
9. **Domain terminology consistency**:
10. **Domain terminology consistency**:
- Use consistent terminology throughout the application
- Before translating, check existing `*.po` files for established domain terms
- If "Tenant" is used, always use "Tenant" (not "Customer", "Client", "Organization", etc.)
Expand Down
229 changes: 229 additions & 0 deletions .claude/skills/enhanced-message-context/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
---
name: enhanced-message-context
description: Provide additional context for messages based on the codebase and the context of the message to improve the quality of the translations.
---

# Enhanced Message Context

When implementing Lingui i18n, add translator comments to messages so translators have context to provide the best translation. Even when the message text is self-explanatory, it is important to know where and how it appears in the UI to choose the correct tone, length, and wording.

## When to Add Comments

Add a `comment` field when the message:

- **Is ambiguous**: Short words/phrases that can be different parts of speech
- "Back" (noun or verb?), "Delete" (button or confirmation?), "Close" (verb or adjective?)
- **Lacks UI context**: Labels isolated from their surroundings
- Table column headers, tooltip content, standalone button labels, menu items
- **Has domain-specific meaning**: Terms with different meanings across contexts
- "Post" (verb or noun?), "Tag" (noun or verb?), "Follow" (social media or instruction?)
- **Depends on grammatical gender**: The translation depends on what the message refers to
- "Selected" (masculine/feminine/neutral depends on what is selected)
- **Uses unclear variables**: Placeholder names don't reveal what they contain
- `{count}` (count of what?), `{name}` (user name, file name, project name?)
- **Could benefit from UI context even if clear**: Where the text appears (button, dialog, banner, form field) affects tone and length - add a brief location or purpose comment when it helps.

## Writing Effective Comments

A good translator comment includes:

1. **Location**: Where in the UI the message appears
- "Button in the top navigation bar"
- "Tooltip for the save icon"
- "Column header in the users table"

2. **Action/Purpose**: What happens or what it means
- "Navigates back to the previous page"
- "Deletes the selected item permanently"
- "Shows the number of unread notifications"

3. **Disambiguation**: Clarify part of speech or meaning
- "Used as a verb, not a noun"
- "Refers to email addresses, not postal addresses"
- "Singular form, user will see 'item' or 'items' based on count"

## API Reference

Lingui provides three ways to add translator comments:

### 1. JS Macro (`t`)

For JavaScript code outside JSX:

```js
import { t } from "@lingui/core/macro";

// With comment
const backLabel = t({
comment: "Button in the navigation bar that returns to the previous page",
message: "Back",
});

// With comment and variable
const uploadSuccess = t({
comment: "Success message showing the name of the file that was uploaded",
message: `File ${fileName} uploaded successfully`,
});
```

### 2. React Macro (`Trans`)

For JSX elements:

```jsx
import { Trans } from "@lingui/react/macro";

// With comment
<Trans comment="Button that deletes the selected email message">Delete</Trans>

// With comment in a component
<button>
<Trans comment="Label for button that saves changes to user profile">
Save
</Trans>
</button>
```

### 3. Deferred/Lazy Messages (`defineMessage` / `msg`)

For messages defined separately from their usage:

```js
import { defineMessage } from "@lingui/core/macro";

const messages = {
deleteButton: defineMessage({
comment: "Button that permanently removes the item from the database",
message: "Delete",
}),

statusLabel: defineMessage({
comment: "Shows whether the service is currently operational. Values: 'Active', 'Inactive', 'Pending'",
message: "Status: {status}",
}),
};
```

## Examples

### Example 1: Ambiguous Short Word

**Before** (no context):

```jsx
<button onClick={goBack}>
<Trans>Back</Trans>
</button>
```

**After** (with context):

```jsx
<button onClick={goBack}>
<Trans comment="Button in the toolbar that navigates to the previous page">
Back
</Trans>
</button>
```

### Example 2: UI Label Without Context

**Before** (no context):

```jsx
const columns = [
{ key: "name", label: t`Name` },
{ key: "status", label: t`Status` },
];
```

**After** (with context):

```jsx
const columns = [
{
key: "name",
label: t({
comment: "Column header in the projects table showing project name",
message: "Name"
})
},
{
key: "status",
label: t({
comment: "Column header showing project status: Active, Inactive, or Archived",
message: "Status"
})
},
{
key: "created",
label: t({
comment: "Column header showing the date when the project was created",
message: "Created"
})
},
];
```

### Example 3: Domain-Specific Term

**Before** (ambiguous):

```jsx
<button onClick={handlePost}>
<Trans>Post</Trans>
</button>
```

**After** (clarified as verb):

```jsx
<button onClick={handlePost}>
<Trans comment="Button that publishes the content. Used as a verb (to post), not a noun (a post)">
Post
</Trans>
</button>
```

### Example 4: Variable Without Clear Meaning

**Before** (unclear what count represents):

```js
const message = t`${count} items selected`;
```

**After** (clarified):

```js
const message = t({
comment: "Shows the number of email messages currently selected in the inbox",
message: `${count} items selected`,
});
```

### Example 5: Self-Explanatory Message (Comment Still Optional but Helpful)

```jsx
// Message is clear on its own; adding a comment with location still helps translators
<Trans comment="Validation hint shown below the password field on the sign-up form">
Your password must contain at least 8 characters, including one uppercase letter and one number.
</Trans>
```

## Workflow

When implementing or reviewing Lingui messages:

1. **Read the message**: Look at the string itself
2. **Check context**: Consider where and how it's used in the code
3. **Ask**: "Could a translator misinterpret this without seeing the UI?"
4. **If yes**: Add a `comment` field with location, purpose, and any disambiguation
5. **If no**: Skip the comment and keep the code clean

## Notes

- Comments are extracted into message catalogs for translators
- Comments are stripped from production builds (zero runtime cost)
- Comments appear in translation management systems (TMS)
- Use consistent terminology across all comments in your project
Loading
Loading