Rework frontend rules for the v2 UI kit
Make contrib/claude the single source of truth for v2 frontend work on the compliance-portal app and packages/ui/src/v2, treating console and the legacy @probo/ui tree as non-compliant code to migrate rather than precedent. Rewrite ui.md around the v2 kit: flat folders, Base UI as the headless layer styled thinly (controlled open/onOpenChange, no imperative ref or cloneElement plumbing), tailwind-variants only, separate components over structure-changing variants, and bundle-safe skeletons that never drag Base UI into the loading path. Add a naming/suffix taxonomy to react-components.md, replacing the Table/Row and connection-item Card suffixes with List/ListItem, and add an error/fallback props convention. Document _lib and _locales special folders plus routes.ts placement in app-arborescence.md, with at most one _locales per routes.ts. Add error-handling.md (reusable ErrorBoundary usable at any level plus async try/catch) and i18n.md (i18next key-based catalogs). Update the relay file-organization and fragment examples, the connection-item cursor rule, and the AGENTS.md index to match. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
101
contrib/claude/i18n.md
Normal file
101
contrib/claude/i18n.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Internationalization (i18next)
|
||||
|
||||
The v2 apps (starting with `apps/compliance-portal`) localize with [i18next](https://www.i18next.com/). Translations are **key-based**: code references a stable key, and each locale supplies the human-readable string in a JSON catalog. Catalogs live in `_locales/` folders colocated with routes.
|
||||
|
||||
> This differs from the legacy `@probo/i18n` translator used by `apps/console`, where the **English source string itself is the key** (`__("Save changes")`). New code uses i18next with explicit keys; do not copy the source-string-as-key pattern into v2 apps.
|
||||
|
||||
## Related guides
|
||||
|
||||
| Topic | Guide |
|
||||
|-------|--------|
|
||||
| Where `_locales/` folders live in the tree | [`contrib/claude/app-arborescence.md`](app-arborescence.md#_locales-folder) |
|
||||
| Routes and resource folders | [`contrib/claude/app-arborescence.md`](app-arborescence.md) |
|
||||
|
||||
## Catalog files
|
||||
|
||||
- One JSON file **per locale**, named by its BCP 47 locale tag: `en-US.json`, `fr-FR.json`.
|
||||
- The **filename is the locale** — there is no locale field inside the file; the file is the namespace's catalog for that locale.
|
||||
- Keys are stable, descriptive identifiers (not the English text). Nest by feature/component to avoid collisions.
|
||||
|
||||
```jsonc
|
||||
// pages/organizations/measures/_locales/en-US.json
|
||||
{
|
||||
"measures": {
|
||||
"title": "Measures",
|
||||
"empty": "No measures yet",
|
||||
"actions": {
|
||||
"create": "New measure"
|
||||
},
|
||||
"count_one": "{{count}} measure",
|
||||
"count_other": "{{count}} measures"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```jsonc
|
||||
// pages/organizations/measures/_locales/fr-FR.json
|
||||
{
|
||||
"measures": {
|
||||
"title": "Mesures",
|
||||
"empty": "Aucune mesure pour le moment",
|
||||
"actions": {
|
||||
"create": "Nouvelle mesure"
|
||||
},
|
||||
"count_one": "{{count}} mesure",
|
||||
"count_other": "{{count}} mesures"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Where catalogs live
|
||||
|
||||
`_locales/` is colocated with a `routes.ts`, at the folder that **names a resource**. The constraint is exact:
|
||||
|
||||
- **No more `_locales/` folders than `routes.ts` files.** A folder without a `routes.ts` does not get its own `_locales/`; its strings belong to the nearest ancestor resource that has one.
|
||||
- Resource boundaries own their translations: `organizations/routes.ts` → `organizations/_locales/`; `organizations/measures/routes.ts` → `organizations/measures/_locales/`.
|
||||
|
||||
See [app-arborescence.md](app-arborescence.md#_locales-folder) for the folder-tree examples.
|
||||
|
||||
## Using translations in components
|
||||
|
||||
Read translations with the i18next hook and a key. Keep keys close to where they are defined (the feature namespace), and pass interpolation values as the second argument.
|
||||
|
||||
```tsx
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function MeasuresPage() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<section>
|
||||
<h1 className="text-6 text-sand-12">{t("measures.title")}</h1>
|
||||
<p className="text-sand-11">{t("measures.count", { count })}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Do / don't: keys, not source strings
|
||||
|
||||
```tsx
|
||||
// Bad — English source string as the key (legacy @probo/i18n pattern)
|
||||
__("No measures yet");
|
||||
|
||||
// Good — stable key resolved from the locale catalog
|
||||
t("measures.empty");
|
||||
```
|
||||
|
||||
### Do / don't: no string building
|
||||
|
||||
Never assemble translated sentences by concatenation — it breaks word order in other languages. Use interpolation and pluralization keys instead.
|
||||
|
||||
```tsx
|
||||
// Bad — concatenation
|
||||
`${count} ${t("measures.unit")}`;
|
||||
|
||||
// Good — pluralized key with interpolation
|
||||
t("measures.count", { count });
|
||||
```
|
||||
|
||||
## Loading catalogs
|
||||
|
||||
Catalogs are loaded into i18next per locale at app startup (or lazily per route). Because each `_locales/` folder maps to a resource segment, catalogs can be code-split alongside the route bundle that needs them — keep a catalog scoped to the feature it serves rather than one global megafile.
|
||||
Reference in New Issue
Block a user