---
name: transloco-i18n
description: >
  Transloco internationalization patterns for Aurora Catalyst. Covers file loading strategy (eager vs
  lazy), scope configuration, translation in wrapper vs leaf components, breadcrumb/sidebar patterns,
  and common pitfalls. Trigger: When adding translations, creating i18n files, using transloco in
  components, or debugging missing translation errors.
license: MIT
metadata:
  author: aurora
  version: '2.0'
---

## When to Use

- Adding or modifying translations (i18n JSON files)
- Using transloco in a new component
- Debugging "Missing translation" warnings
- Adding a new transloco scope
- Translating sidebar navigation, breadcrumbs, or layout elements
- Changing language switching behavior

---

## Translation File Loading

### How it works

`TranslocoHttpLoader` fetches JSON files from `/i18n/{lang}.json` at runtime via HTTP.

- **Config**: `transloco.config.ts` defines available langs (`['en', 'es']`) and root path
  (`public/i18n/`)
- **Loader**: `src/app/transloco-loader.ts` — `TranslocoHttpLoader` implements `TranslocoLoader`
- **Provider**: `app.config.ts` — `provideTransloco()` with `reRenderOnLangChange: true`
- **Default language**: `'es'` (Spanish)

### File structure

```
public/i18n/
├── en.json, es.json              <- Global scope (eager) — Cancel, Delete, Create, etc.
├── navigation/                   <- Navigation scope (eager) — sidebar items, breadcrumbs
│   ├── en.json, es.json
├── iam/                          <- Bounded context scope (lazy) — entity-specific labels
│   ├── en.json, es.json
```

### Eager vs Lazy loading

| Strategy | When loaded | Where configured | Use case |
|---|---|---|---|
| **Eager** | App startup (blocks rendering) | `provideAppInitializer` in `app.config.ts` | Global UI + navigation |
| **Lazy** | When component renders | `provideTranslocoScope('scope')` in component | Bounded context entities |

### Eager scopes (loaded at app startup)

```typescript
// app.config.ts
provideAppInitializer(() => {
  const transloco = inject(TranslocoService);
  const lang = transloco.getDefaultLang();
  return lastValueFrom(forkJoin([
    transloco.load(lang),                    // global scope
    transloco.load(`navigation/${lang}`),     // navigation scope
  ]));
})
```

**CRITICAL**: `transloco.load()` returns an Observable. Must use `forkJoin` + `lastValueFrom` for
the initializer.

### Lazy scopes (loaded on demand)

Each bounded context loads its scope via `provideTranslocoScope` in the component. The `*transloco`
directive waits for the scope to load before rendering:

```typescript
@Component({
  providers: [provideTranslocoScope('iam')],
  template: `
    <div *transloco="let t; scope: 'iam'">
      {{ t('iam.tag.Title') }}
    </div>
  `,
})
```

The HTTP loader resolves scope paths automatically: scope `'iam'` loads `/i18n/iam/{lang}.json`.

### Adding a new scope

1. Create `public/i18n/{scope}/en.json` and `es.json`
2. If needed at app startup → add to `provideAppInitializer` in `app.config.ts`
3. If needed only in specific views → use `provideTranslocoScope('{scope}')` in the component

---

## Component Patterns

### RULE 1: Never use `*transloco` on elements with `<ng-content>`

The `*transloco` structural directive delays rendering until translations load. If placed on a
wrapper element, it blocks ALL child content from rendering.

**BAD** — blocks entire sidebar including `<ng-content>`:

```html
<div *transloco="let t" hlmSidebarWrapper>
  ...
  <ng-content />
</div>
```

**GOOD** — use `inject(TranslocoService)` + `translate()` in the class:

```typescript
export class AppSidebar {
  private readonly transloco = inject(TranslocoService);
  readonly label = signal(this.transloco.translate('navigation.Platform'));
}
```

### RULE 2: Never use `TranslocoPipe` without explicit scope

The pipe infers scope from dot-separated keys, causing 404 errors:

```html
<!-- BAD — tries to load /i18n/error/es.json -->
[label]="'Platform' | transloco"
```

### RULE 3: Use `selectTranslation()` to react to language changes in wrapper components

`langChanges$` emits BEFORE new language JSONs are loaded. `selectTranslation('scope')` emits AFTER
translations are available:

```typescript
// BAD — translates before new JSONs loaded
this.transloco.langChanges$.subscribe(() => {
  this.label.set(this.transloco.translate('navigation.Platform'));
});

// GOOD — waits for translations to be ready
this.transloco.selectTranslation('navigation').subscribe(() => {
  this.label.set(this.transloco.translate('navigation.Platform'));
});
```

### RULE 4: Leaf components use `*transloco` directive normally

Components that don't wrap `<ng-content>` can use the directive safely:

```html
<ng-container *transloco="let t; scope: 'iam'">
  <h1>{{ t('iam.tag.Title') }}</h1>
</ng-container>
```

### RULE 5: Breadcrumb pattern

`useBreadcrumbs` stores translation keys — the site-header translates them:

```typescript
// Component — just stores keys
private readonly breadcrumb = useBreadcrumbs([
  { translation: 'navigation.Tags', link: '/iam/tag' },
  { translation: 'Create' },
]);
```

```html
<!-- Site-header — translates with *transloco on the <nav> only -->
<nav *transloco="let t" hlmBreadcrumb>
  @for (crumb of breadcrumbs(); track $index) {
    <a>{{ t(crumb.translation) }}</a>
  }
</nav>
```

The site-header has `provideTranslocoScope('navigation')` so `t()` can resolve both global keys
(`Create`) and navigation keys (`navigation.Tags`).

---

## Sidebar Navigation Translation

The sidebar data stores `translation` keys:

```typescript
{
  title: 'Dashboard',
  translation: 'navigation.Dashboard',
  url: '/dashboard',
  icon: 'lucideLayoutDashboard',
}
```

`app-sidebar` translates them:

- **Initial render**: `translate()` synchronous (scopes loaded eagerly)
- **Language change**: `selectTranslation('navigation')` re-evaluates

---

## Gender-aware Translations

Spanish requires gender agreement. Use nested keys:

```json
{
  "Created": { "F": "Creada", "M": "Creado" },
  "New": { "F": "Nueva", "M": "Nuevo" }
}
```

Usage with composed translations:

```typescript
toast.success(`${t('Created.F')} ${t('Successfully')}`);
// -> "Creada correctamente"
```

---

## Data Table Column Translations

For reactive translations in data-table components, see the **catalyst-data-table-management** skill
which covers:

- `label` as `string | (() => string)` in `GridColumnDef`
- Lazy functions in template for reactive language switching
- Entity data via `input()` vs component UI via `*transloco`

---

## Common Errors and Fixes

| Error | Cause | Fix |
|---|---|---|
| `Missing translation for 'X'` | Scope not loaded when `translate()` called | Use `*transloco` directive or `label: () => translate(...)` pattern |
| `GET /i18n/error/es.json 404` | `TranslocoPipe` inferring scope from key | Don't use `TranslocoPipe` — use directive or `translate()` |
| Sidebar/layout not rendering | `*transloco` on element with `<ng-content>` | Use `translate()` in class instead |
| Translations not updating on lang change | Using `langChanges$` | Use `selectTranslation('scope')` instead |
| Grid columns not updating on lang change | Static column labels resolved at init | Use `label: () => translator?.translate(...) ?? fallback` |
