agentleFS
bbvch-aibbvch-ai/aihub-core/.claude/skills/i18n/SKILL.md

i18n

Skill16 starsChanged 3 days agoApache-2.0View on GitHub

Comprehensive i18n reference and validation. Covers the dual i18n architecture (frontend Nuxt i18n + backend python-i18n), LocaleHandler/LocaleString class hierarchies, translation file structure and naming, scope inheritance, and when to create vs extend locale files. Also validates key consistency across locales and scans for hardcoded strings. Use when user says 'how does i18n work', 'add translations', 'create locale file', 'check translations', 'find missing i18n keys', 'validate locales', 'i18n coverage', or 'any hardcoded strings'. Do not use for frontend-only i18n config questions — see packages/web/CLAUDE.md instead.

We split this skill into the rules it gives an agent

8 rules. 0 are common, 5 are distinctive, and 3 are very specific to this project.

  • 0

    Common

    Most of the 36 edited versions we read keep these.

  • 5

    Distinctive

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    All user-facing strings must be translated into 4 locales: de (German), en (English), fr (French), it (Italian).

    line 9
  2. Very specific:

    Navigation: ALL routes MUST use localePath() — URLs include locale prefix (/en/service/agents)

    line 21
  3. Distinctive:

    Every translation file MUST have exactly 4 variants — one per locale:

    line 53
  4. Distinctive:

    Create a new file: when:

    line 75
  5. Distinctive:

    Extend an existing file: when:

    line 81
  6. Very specific:

    Key insight: All sub-package handlers call super().get_locale_paths(), which includes packages/core translations.

    line 145
  7. Distinctive:

    All 4 locales MUST be provided.

    line 222
  8. Distinctive:

    Run the deterministic validation script to compare YAML keys across locales:

    line 234

“Common” is measured: kept by at least 6 in 10 of the 36 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Internationalization (i18n)

All user-facing strings must be translated into 4 locales: de (German), en (English), fr (French), it (Italian). English (en) is the reference/default locale. Fallback chain: requested locale -> en -> first available.

Architecture: Two Separate Systems

Frontend and backend use completely independent i18n systems. They share no files, no configuration, and no runtime.

Frontend (Nuxt @nuxtjs/i18n)

  • Config: packages/web/nuxt.config.ts (i18n section, strategy: 'prefix', lazy: true)
  • Files: packages/web/i18n/locales/{locale}.yaml — one mega YAML per locale (.yaml extension)
  • Usage: $t('key.path') in templates, t('key.path') in <script setup> via useI18n()
  • Navigation: ALL routes MUST use localePath() — URLs include locale prefix (/en/service/agents)
  • Key structure: Nested YAML, accessed via dot-notation: common.actions.save -> { common: { actions: { save: "Save" } } }

Backend (python-i18n library)

  • Files: {scope}/{name}.{locale}.yml across multiple packages (.yml extension)
  • Usage: locale_handler("scope.name.key.path") at runtime, ScopeLocaleString.from_i18n_path("scope.name.key") at definition time
  • Key structure: First two dot segments are {scope}.{filename}, the rest navigate YAML nesting

Backend Translation Files

File Locations

PackageTranslation DirectoryScope Prefix
packages/corepackages/core/swiss_ai_hub/core/i18n/translations/lib/lib
packages/corepackages/core/swiss_ai_hub/core/i18n/translations/bot/bot
packages/apipackages/api/swiss_ai_hub/api/i18n/translations/api/api
packages/agentpackages/agent/swiss_ai_hub/agent/i18n/translations/agent/agent
packages/processpackages/process/swiss_ai_hub/process/i18n/translations/process/process

Bot exception: Bot translations live under packages/core (scope bot/), NOT in packages/bot. Bots use the base LocaleHandler, not a bot-specific subclass.

Naming Convention

{scope}/{descriptive_name}.{locale}.yml

Every translation file MUST have exactly 4 variants — one per locale:

agent/rag_agent.de.yml
agent/rag_agent.en.yml
agent/rag_agent.fr.yml
agent/rag_agent.it.yml

Path Resolution

The dot-notation path agent.rag_agent.metadata.name resolves as:

  1. agent -> scope directory name
  2. rag_agent -> YAML filename (without locale/extension)
  3. metadata.name -> nested keys inside the YAML file

The system searches all registered load_path directories for agent/rag_agent.{locale}.yml, then navigates data["metadata"]["name"].

When to Create a New File vs. Extend an Existing One

Create a new file when:

  • Adding a new agent -> agent/{agent_name}.{locale}.yml for metadata/steps/config labels
  • Adding a new process -> process/{process_name}.{locale}.yml
  • Adding a new logical domain that doesn't fit existing groupings (e.g., a new guard type, a new controller)

Extend an existing file when:

  • Adding keys to an existing agent/process (e.g., new step in agent/rag_agent.en.yml)
  • Adding common/shared translations -> lib/common.{locale}.yml or api/common.{locale}.yml
  • Adding event translations -> lib/events.{locale}.yml
  • Adding guard translations -> lib/guards.{locale}.yml

Naming guidance: Group by the thing being described, not by where it's used. Agent metadata, step names, config labels, and error messages for the same agent all go in one file (e.g., agent/rag_agent.{locale}.yml). If a file grows too large (>200 keys), split by sub-domain (e.g., agent/rag_agent_config.{locale}.yml).

Example File Structure

agent/rag_agent.en.yml:

metadata:
  name: "RAG Agent"
  description: "Retrieves relevant documents and generates answers."
steps:
  retrieve_nodes:
    name: "Retrieve Documents"
    description: "Searches knowledge bases for relevant content."
config:
  context_prompt:
    label: "Context Prompt"
    help: "System prompt used when generating answers."

Accessed as: agent.rag_agent.metadata.name, agent.rag_agent.steps.retrieve_nodes.name, etc.

LocaleHandler Hierarchy

LocaleHandler is the base class for runtime translation lookup. Each sub-package extends it to register its own translation directory while inheriting lib-level translations.

Base: LocaleHandler (packages/core/swiss_ai_hub/core/i18n/locale_handler.py)

  • DEFAULT_LOCALE = "en", LOCALE_WHITE_LIST = ["de", "en", "fr", "it"]
  • get_locale_paths() returns [packages/core/swiss_ai_hub/core/i18n/translations/]
  • __call__(key, locale) -> i18n.t(key, locale=locale) — translates a key
  • t_object(key, locale) -> returns raw YAML data (dict/list) instead of string
  • extract(locale_data, locale) -> extracts from dict[str, Any] or LocaleString objects
  • in_locale(locale) -> returns new handler instance for a different locale

Sub-package Handlers

Each extends LocaleHandler and overrides get_locale_paths() to add its own translation directory:

ApiLocaleHandler (packages/api/swiss_ai_hub/api/i18n/api_locale_handler.py):

def get_locale_paths(self) -> list[str]:
    return [*super().get_locale_paths(), self_translations_path]

Load paths: [lib/translations/, api/translations/] -> can resolve both lib.* and api.* keys.

AgentLocaleHandler (packages/agent/swiss_ai_hub/agent/i18n/agent_locale_handler.py): Same pattern. Load paths: [lib/translations/, agent/translations/].

ProcessLocaleHandler (packages/process/swiss_ai_hub/process/i18n/process_locale_handler.py): Same pattern. Load paths: [lib/translations/, process/translations/].

Key insight: All sub-package handlers call super().get_locale_paths(), which includes packages/core translations. This means agent code can reference lib.events.start_event.name or lib.guards.context_guard.reason — lib-level translations are always available in sub-packages.

Runtime Usage

In agent/process @step methods, the dispatcher injects a locale-aware handler:

async def my_step(self, event: UserMessageEvent, t: AgentLocaleHandler) -> StopEvent:
    error_msg = t("agent.my_agent.errors.not_found")
    lib_msg = t("lib.common.test")  # lib keys work too
    return StopEvent(output=error_msg)

LocaleString Hierarchy

LocaleString holds pre-resolved translations for all 4 locales in a single Pydantic model. Used at definition time (class attributes, config labels, event metadata).

Base: LocaleString (packages/core/swiss_ai_hub/core/i18n/locale_string.py)

Fields: de: str | None, en: str | None, fr: str | None, it: str | None

Key methods:

  • in_locale(locale) -> returns string for that locale
  • from_i18n_path(path) -> creates instance by calling LocaleHandler(locale)(path) for each locale
  • as_form(label, ...) -> creates a LocaleInput form element for multi-language editing

Sub-package LocaleStrings

Each sub-package has its own LocaleString subclass that uses the package-specific LocaleHandler:

AgentLocaleString (packages/agent/swiss_ai_hub/agent/i18n/agent_locale_string.py):

@classmethod
def from_i18n_path(cls, path: str) -> Self:
    return cls(
        de=AgentLocaleHandler("de")(path),
        en=AgentLocaleHandler("en")(path),
        fr=AgentLocaleHandler("fr")(path),
        it=AgentLocaleHandler("it")(path),
    )

ApiLocaleString (packages/api/swiss_ai_hub/api/i18n/api_locale_string.py): Uses ApiLocaleHandler.

ProcessLocaleString (packages/process/swiss_ai_hub/process/i18n/process_locale_string.py): Uses ProcessLocaleHandler.

Definition-Time Usage

Agent/process metadata and config labels use from_i18n_path():

class RetrievalAgent(BaseAgent):
    name: ClassVar[AgentLocaleString] = AgentLocaleString.from_i18n_path(
        "agent.retrieval_agent.metadata.name"
    )

    @step(
        name=AgentLocaleString.from_i18n_path("agent.retrieval_agent.steps.retrieve.name"),
        description=AgentLocaleString.from_i18n_path("agent.retrieval_agent.steps.retrieve.description"),
    )
    async def retrieve(self, ...) -> ...:

Direct Construction

For inline values that don't come from YAML files:

LocaleString(de="Hallo", en="Hello", fr="Bonjour", it="Ciao")

All 4 locales MUST be provided. This is common in event classes and form elements.

Persistence: LocaleStringEntity

LocaleStringEntity (packages/core/swiss_ai_hub/core/persistence/i18n/locale_string_entity.py) is a MongoEngine EmbeddedDocument with the same 4 fields. Convert between them:

  • LocaleStringEntity.from_locale_string(locale_string) -> for storage
  • entity.to_locale_string() -> for runtime

Validation: Run the Script

Run the deterministic validation script to compare YAML keys across locales:

uv run python .claude/skills/i18n/scripts/validate-i18n.py $ARGUMENTS

$ARGUMENTS: frontend, backend, or all (default).

The script flattens nested YAML keys to dot-notation, uses English as reference, and reports missing/extra/empty keys with coverage percentages. Present the script output to the user as-is.

Validation: LocaleString Completeness

The script does not cover inline LocaleString(...) instances. Check manually:

grep -rn 'LocaleString(' packages/agent/ packages/process/ --include='*.py'

Each LocaleString(...) must have all 4 keyword arguments: de=, en=, fr=, it=.

Validation: Hardcoded Strings in New Code

Review git diff main -- '*.py' '*.vue' '*.ts' for hardcoded user-facing strings.

Frontend: Flag literal text in HTML elements, hardcoded label=/placeholder=/title= props, toast messages without $t() or t().

Backend: Flag LocaleString() with fewer than 4 locales, hardcoded strings in display_chunk()/display_thought(), literal form element label=/help= without LocaleString.

Do NOT flag: Log messages, internal exceptions, test files, comments, NATS subjects, config keys, docstrings.

Validation: Frontend Template Key Usage

Extract i18n keys from Vue templates and verify they exist in en.yaml:

grep -rhoP "\$t\(['\"]([^'\"]+)['\"]\)" packages/web/ --include='*.vue' | sort -u

Ignore dynamic keys like $t(variable).

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.