Skip to content
Back to skills

I18n Component Resources

ASecurity

Add or refresh the multilanguage resources of an Angular component in gebo.ui/projects/gebo-ai-reusable-ui/src/assets/i18n/en.json (and optionally a translated <lang>.json), by reading the component's template/providers and deriving the module/entity/component/field keys the gebo.multilanguage.support system expects. Use when asked to "make a component multilanguage", "add the i18n resources for X", "translate component X", "update en.json for X", or when a new control's labels/tooltips are s...

  • 7 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 20, 2026
ai-agentspythongojavashellbashangulargitbackendperformance

Security analysis

A100/100

Scanned October 2, 2026

npx -y skills add geboai/Gebo.ai --skill i18n-component-resources --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of I18n Component Resources?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for I18n Component Resources
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/geboai-i18n-component-resources/badge)](https://www.skillsdirectory.com/skills/geboai-i18n-component-resources)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: i18n-component-resources
description: Add or refresh the multilanguage resources of an Angular component in gebo.ui/projects/gebo-ai-reusable-ui/src/assets/i18n/en.json (and optionally a translated <lang>.json), by reading the component's template/providers and deriving the module/entity/component/field keys the gebo.multilanguage.support system expects. Use when asked to "make a component multilanguage", "add the i18n resources for X", "translate component X", "update en.json for X", or when a new control's labels/tooltips are still hardcoded English on screen.
user-invocable: true
allowed-tools:
  - Read
  - Edit
  - Write
  - Glob
  - Grep
  - Bash
  - PowerShell
---

# /i18n-component-resources — Wire an Angular component into the Gebo multilanguage system

Produces the `en.json` block (and, on request, translated `<lang>.json` blocks) for one
Angular component of `gebo-ai-reusable-ui` — or of any app that consumes the library.

`en.json` is not a translation file. It is the **source-of-truth catalogue of English UI
text**: at runtime the recorder writes what it sees into Mongo, and the backend merges
that with this bundled file. Editing it by hand is how a component gets into the
catalogue without having to run the recorder.

---

## 1. The key shape — four levels, no more, no less

`gebo.architecture.parent/gebo.multilanguage.support` stores one row per text with
`moduleId / entityId / componentId / fieldId`, and
`UIExistingTextService.getCurrentLanguageResources()` nests them in exactly that order:

```json
{
  "<moduleId>": {
    "<entityId>": {
      "<componentId>": {
        "<fieldId>": "The English text"
      }
    }
  }
}
```

`extractUIExistingText()` (`field-translation-container/text-language-resources.ts`)
returns `undefined` unless **all four** resolve. A block with a wrong module or entity
name is not a partial match — it is simply never found, silently.

### Where each level comes from

| Level | Source | How to read it |
|---|---|---|
| `moduleId` | `GEBO_AI_MODULE` provider | `providers: [{ provide: GEBO_AI_MODULE, useValue: "GeboAIFooModule" }]` on the component, else on its NgModule |
| `entityId` | `GEBO_AI_FIELD_HOST` provider | `useValue: fieldHostComponentName("GeboAIFooComponent")` |
| `componentId` | the element's `id` attribute in the template | `<span id="SaveHint" …>` → `SaveHint` |
| `fieldId` | which attribute the directive read | see the table in §2 |

Both providers can sit on the component or on the NgModule; the component wins. Grep for
them before writing anything:

```bash
grep -n "GEBO_AI_MODULE\|GEBO_AI_FIELD_HOST\|fieldHostComponentName" \
  <component>.component.ts <component>.module.ts
```

**`GEBO_AI_FIELD_HOST` must use `useValue`, never `useExisting`.** `fieldHostComponentName()`
returns an *instance*, and `useExisting` treats its value as a token to alias — Angular
then throws `NullInjectorError` resolving that object, and `@Optional()` on the directive's
parameter does not cover the inner lookup. Every working control in the library uses
`useValue`; if you find `useExisting` here, that is the bug, fix it first or nothing you
add to `en.json` will ever resolve.

---

## 2. What the directives read

Everything is opt-in per element: **no `id`, no resource.** Two directives cover the
template cases.

### `[gebo-ai-text]` — element body

Reads `innerText` and replaces it after lookup. One field only:

| Template | fieldId |
|---|---|
| `<span id="Foo" gebo-ai-text>Some text</span>` | `label` |

### `[gebo-ai-label]` — element attributes

Reads up to three attributes, aliased onto three field ids:

| Attribute on the element | fieldId |
|---|---|
| `label`, `legend`, `header` | `label` |
| `title` | `help` |
| `placeholder` | `placeholder` |

`label`/`legend`/`header` collapse onto the same `label` field, first one wins. Works on a
native element *and* on a PrimeNG component — for the latter the matching
`P*LabelTarget` adapter in `primeng-components-multilanguage-adapters.directive.ts` pushes
the translation into the component's own input, so check an adapter exists for the tag you
are labelling.

### Programmatic text — not template-driven

`GeboAITranslationService` has helpers for text that never appears as an attribute. They
take module/entity explicitly, and the `componentId` is the object's `id`:

| Helper | fieldIds it produces |
|---|---|
| `translateMessage(mod, ent, id, ToastMessageOptions)` | `summary`, `detail` |
| `translateConfirmation(mod, ent, ExtendedConfirmation)` | `message`, `header`, `acceptLabel`, `rejectLabel` |
| `translateMenuItems` / `translateMegaMenuItems(mod, ent, items[])` | `label`, `help`, `tooltip` |
| `translateBackendMessage(s)(GUserMessage)` | `summary`, `detail`, always under module `BackendMSGSModule` / entity `GUserMessage` |

If the component raises toasts or confirmations through these, their ids belong in the
same `en.json` block as the template ids.

---

## 3. Procedure

1. **Read the template** and list every element carrying `gebo-ai-text` or `gebo-ai-label`,
   with its `id` and the attributes present. Also grep the `.ts` for
   `translateMessage|translateConfirmation|translateMenuItems|translateBackendMessage`.
2. **Resolve `moduleId` / `entityId`** from the providers (§1).
3. **Report anything untranslatable before editing** — a visible static string with no
   `id`/directive produces no resource. Either add `id="…" gebo-ai-text` to it (preferred,
   it is a one-line template change) or say explicitly that it stays English. Do not
   silently invent a resource for an element that cannot look it up.
4. **Append the block to `en.json`** (§4). Text values must match the template's current
   English *exactly*, including the ellipsis character (`…`, U+2026) where the template
   uses one — a mismatch does not break lookup (that is by id) but it does corrupt the
   catalogue the translators work from.
5. **Verify**, then optionally add translated languages (§5).

### Skip these

Interpolated or bound content (`{{ error() }}`, `[innerHTML]`, backend-supplied
descriptions). Those are data, not UI chrome, and have no stable id.

---

## 4. Editing `en.json` safely

Path: `gebo.ui/projects/gebo-ai-reusable-ui/src/assets/i18n/en.json`
(shipped to the backend as `/static/assets/i18n/en.json`, loaded by
`gebo.ui/src/main/java/ai/gebo/ui/ExistingBundledTextResourcesDaoImpl.java`).

Its own formatting: **2-space indent, `": "` separators, LF, one trailing newline**, top-level
keys in insertion order: `json.dumps(d, indent=2, ensure_ascii=False) + '\n'` round-trips the
committed file byte for byte. Other `<lang>.json` files use a *different* style — Jackson's
`"key" : value` with 2-space indent — because they come out of the translation tool.
**Match whatever the file you are editing already uses.**

Never hand-edit with a text splice; round-trip it so the JSON cannot end up malformed,
and keep the diff append-only:

```bash
cd gebo.ui/projects/gebo-ai-reusable-ui/src/assets/i18n && python - <<'PY'
import json, io, collections
p = 'en.json'
d = json.load(io.open(p, encoding='utf-8'), object_pairs_hook=collections.OrderedDict)
d.setdefault('GeboAIFooModule', collections.OrderedDict())['GeboAIFooComponent'] = \
    collections.OrderedDict([
        ('SaveBtn',  collections.OrderedDict([('label', 'Save'), ('help', 'Save the changes')])),
        ('SaveHint', collections.OrderedDict([('label', 'Nothing to save yet')])),
    ])
with io.open(p, 'w', encoding='utf-8', newline='\n') as f:
    json.dump(d, f, indent=2, ensure_ascii=False); f.write('\n')
PY
```

`setdefault` matters: several components share one module (all of
`GeboAIChatControlModule`'s controls live under one key), so adding an entity must not
replace a module that already exists.

**Line endings.** These files are stored LF but checked out CRLF. Writing CRLF makes git
show the *whole file* as rewritten. Always write `newline='\n'`, then confirm the diff is
only your addition:

```bash
git diff --stat -- gebo.ui/projects/gebo-ai-reusable-ui/src/assets/i18n/en.json
```

A few dozen insertions and **zero deletions** is right. Thousands of both means the line
endings flipped — normalise with `raw.replace(b'\r\n', b'\n')` and re-check.

---

## 5. Adding a translated language

Same block, same keys, translated values, in that file's own formatting. Render it with
the file's separator style rather than reformatting the whole file:

```python
json.dumps({name: body}, indent=2, separators=(',', ' : '), ensure_ascii=False)
```

Then assert key parity against `en.json` — a missing key falls back to the English source
text with no warning:

```python
def keys(d, pfx=''):
    out = set()
    for k, v in d.items():
        out |= keys(v, pfx + '/' + k) if isinstance(v, dict) else {pfx + '/' + k}
    return out
print(keys(en[mod]) - keys(it[mod]))   # must be empty
```

---

## 6. Verify

```bash
cd gebo.ui
python -c "import json,io;d=json.load(io.open('projects/gebo-ai-reusable-ui/src/assets/i18n/en.json',encoding='utf-8'));print(len(d),'modules')"
npx ng build gebo-ai-reusable-ui        # only if the template/providers changed
```

To see it actually applied, the whole chain has to be alive — the resource file is the
last link, not the first. In order:

1. `assets/i18n/<lang>.json` is fetched (check `performance.getEntriesByType('resource')`
   in the page — if nothing i18n-shaped appears, the loader is misconfigured, not your JSON);
2. `assets/languages-choice.json` is fetched, or the language dropdown is empty;
3. a language component is mounted somewhere (`<gebo-ai-main-language-choice>` in the
   desktop shell, `<gebo-ai-main-micro-language>` in the office assistant footer) — nothing
   calls `changeActualLanguage` on startup without one;
4. only then does a directive swap the text.

---

## 7. Gotchas that cost real time

- **Silent total failure is the norm here.** A wrong module name, a missing `id`, or a
  `useExisting` provider all produce "English on screen", identical to having no resource
  at all. Verify the four key parts against the providers before assuming the JSON is wrong.
- **`en.json` is merged, not replaced.** `UIExistingTextService` layers Mongo-recorded rows
  *over* the bundled file, so a stale row recorded from an older build can mask what you
  add here. If the app shows text you cannot find in `en.json`, look in the
  `UIExistingText` collection.
- **The recorder only runs when enabled.** `GeboAITranslationService.recordingOn` comes from
  `getUiTextResourcesModule()`; with it off, nothing you render is captured, which is the
  usual reason a component "should have registered itself" and did not.
- **`[class$='-flag']` styling lives in `src/styles/flags.scss`**, split out of the library
  `styles.scss` so a host can include only the flags. A host that includes neither shows no
  flag — relevant whenever a language chooser renders blank circles.
- **Hosts not served from `/`.** `GeboAITranslationService.assetsUrl` resolves against
  `document.baseURI`, and the ngx-translate loader prefix should be **relative**
  (`assets/i18n/`). A root-absolute `/assets/i18n/` 404s wherever the app is served from a
  sub-path — an office plugin under `/sdkjs-plugins/<plugin>/`, for instance.
- **`@ngx-translate/http-loader` v18 reads paths from `resources`.** A top-level
  `{prefix, suffix}` is accepted and ignored, leaving the loader with nothing to request:
  `useValue: { resources: [{ prefix: 'assets/i18n/', suffix: '.json' }] }`.

---

## Report

When this skill is used, state: which component (module/entity), which ids and fieldIds
were added, which visible strings were deliberately left untranslated and why, whether a
template or provider change was needed, and the `git diff --stat` line proving the edit was
append-only.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.