i18n
SkillCommunicationUse when localizing the Inspector frontend, extracting UI strings into Paraglide messages, adding/wiring `m.*()` calls in Svelte components, authoring en/ar message JSON, translating chrome to Arabic (MSA), or verifying i18n coverage. Trigger phrases include "internationalize", "localize", "translate to Arabic", "extract strings", "i18n", "add a message key", "wire up the locale switcher".
Use i18n in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add i18n and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use the i18n skill
Details
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; ahel provides instructions and does not run this skill.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
What this skill tells your AI
The instructions your AI receives, as published by qud-technologies/quranic-universal-audio in .claude/skills/i18n/SKILL.md and read by ahel’s review.
The canonical, self-contained playbook for localizing the Inspector SPA. Every downstream i18n agent (string-finder, translator, wiring agent, browser-tester) reads this and follows it. Read the reference subfile that matches your task — don't preload all of them.
The stack is Paraglide JS 2.x (inlang message-format). English (en) is the source locale; Arabic (ar, Modern Standard) is the first target. It is already installed and wired — you add keys + wire components, you do not re-scaffold.
Scope & order (LOCKED)
This is workflow 1: strings only. Arabic strings ship into chrome that stays LTR-positioned — the full RTL layout flip is a separate, later workflow (it updates PRODUCT.md's "don't flip the whole component" principle). Your job here is accurate strings that don't visually mangle the still-LTR layout, achieved via bidi isolation (see arabic-conventions.md §8) — not a flipped layout.
Coverage order (don't get ahead of it):
- Public surfaces first — Dashboard (
dashboard_*) + Timestamps (ts_*). These are the priority. - User approves the public pass → then Segments (
segments_*). - Admin is English-default, lowest priority — do it last, only when asked.
Golden rules
- Hand-author the message JSON. NEVER round-trip through the inlang editor / Sherlock VS Code extension — they collapse the split
pathPatternarray into the last file and silently blank the others, and it still passesbuild. Edit{en,ar}.jsonwith the Write/Edit tools only. →paraglide.md,project-conventions.md§2. - Keys are flat,
snake_case, area-prefixed (common_/errors_/dashboard_/ts_/segments_). Paraglide flattens everything into onem.*()namespace; the prefix is the only thing stopping atitlein one tab clobbering atitlein another (duplicate ids resolve last-wins, silently). →project-conventions.md§1. - en ↔ ar files stay key-for-key identical (same id set, same order). A missing key in one locale silently drops it. Author the en key, then the ar key in the same position in the sibling file.
- Translate chrome, never data. Reciter names, surah names (use API
name_ar/name_en), Quran text/tokens/shard letters are data — never keyed. → §"don't-translate" below +arabic-conventions.md§6. - A Svelte file is fully runes OR fully legacy — never mix. Wire each per its mode (below). Do not introduce a single rune into a legacy file.
- Arabic follows the LOCKED conventions — MSA formal-neutral, no tashkeel in chrome, Western digits, Arabic punctuation, ICU plurals with all 6 categories, bidi-isolate mixed runs. →
arabic-conventions.md(authoritative; apply without re-deciding). - Verify after every batch:
npm run check+npm run buildgreen, en/ar keys synced. Do NOT runnpm run lint(pre-commit hook owns it; a manual pass fails on native-binding in fresh worktrees). →project-conventions.md§9.
The loop: extract → key → translate → verify
- Extract — find the hardcoded chrome string (text node, or
aria-label/title/placeholder/alt). Start at the seams where copy already lives centralized:lib/errors/friendly.ts,lib/sign-in-messages.ts, the*_LABELSenum→display maps. →project-conventions.md§7. - Key — invent
area_component_purpose(snake_case, area-prefixed). Grep the wholemessages/tree for the id first — collisions are the silent last-wins bug. Add it to the area'sen.jsonandar.jsonin the same position. - Translate — author the
arvalue perarabic-conventions.md. Counts → ICU plural with all 6 Arabic categories. Mixed Arabic+Latin/number → bidi-isolate the foreign run. - Wire — replace the literal with
m.<key>()using the correct per-mode pattern (below). - Verify —
npm run check&&npm run buildgreen; en/ar key parity (diffof sorted keys).
Translate vs. don't-translate
| Translate (UI chrome the app authored) | Do NOT translate (data the API owns) |
|---|---|
| Button / label / heading / tab / menu text | Reciter names (catalog data) |
aria-label, title, placeholder, alt | Surah / chapter names → pick API name_ar / name_en by locale |
| Validation card titles + descriptions (at render site — see registry caveat) | Quran text, tokens, ayah glyphs, waveform shard letters |
Error copy (friendly.ts), toasts, confirmations, sign-in prompts | Enum values sent to the backend (state/role/severity codes) |
| Keyboard-shortcut labels/descriptions (not the bindings) | Slugs, file paths, job IDs, SHAs, version tags, capability keys |
| Enum-derived pills/badges (the display string) | IDs, timestamps, bitrates, surah:ayah codes, units |
| Domain labels — "Surah", "Ayah 5", "Riwayah:" (key the label) | Product name ("Quranic Universal Audio", "Inspector"), brands (Hugging Face, GitHub, OAuth) |
The nuance: ayah / surah / riwayah as a UI label is translatable chrome (key it); the entity value it points at is Arabic data (leave it). See arabic-conventions.md §5–6 for the full glossary + do-not-translate list.
Wiring patterns (Svelte5 runes vs legacy Svelte4)
Both rest on one fact: m.*() reads the ambient locale set by Paraglide's setLocale(). The per-mode question is only how the component re-renders when the locale flips. The single switch entry point is switchLocale() in src/lib/i18n/locale-store.ts (it updates the Paraglide ambient locale + the legacy store + the rune cell + <html dir/lang> together).
(a) Runes files — import * as m, call m.x() inline. Reactivity comes from reading the i18n rune (src/lib/i18n/locale.svelte.ts):
<script lang="ts">
import * as m from '$lib/paraglide/messages';
import { i18n } from '$lib/i18n/locale.svelte';
// Reading i18n.locale makes this $derived re-run on switch.
const heading = $derived((i18n.locale, m.dashboard_catalog_heading()));
</script>
<h1>{heading}</h1>
<input placeholder={m.dashboard_catalog_search_placeholder()} />
A top-level read of i18n.locale is enough to re-render the whole component; when in doubt gate a $derived on it.
(b) Legacy Svelte-4 files (e.g. App.svelte) — never add a rune. Subscribe to the localeStore mirror and reference $localeStore in a reactive block so Svelte-4 reactivity re-evaluates the message calls. The (dep, m.x()) comma-sequence is the load-bearing trick — it makes the statement depend on the store while still evaluating to the string (a tr<T>(dep, value) helper in locale-store.ts does the same):
<script lang="ts">
import * as m from '$lib/paraglide/messages';
import { localeStore } from '$lib/i18n/locale-store';
$: lang = $localeStore;
$: saveLabel = (lang, m.common_action_save());
</script>
<button on:click={onSave}>{saveLabel}</button>
If a legacy file is too tangled to thread $localeStore through every call, you have exactly two sanctioned options: migrate the file to runes first, or keep it legacy with the $localeStore idiom. Never a third.
Attributes take the function call directly: placeholder={m.x()}, aria-label={m.x()}, title={m.x()}, alt={m.x()}. In legacy files bind them to a reactive $: label = ($localeStore, m.x()) value or they go stale after a switch.
Interpolation & ICU plurals
Pass data through message params, never concatenation that splices chrome + data:
"dashboard_catalog_result_count": "Showing {shown} of {total} reciters"
{m.dashboard_catalog_result_count({ shown, total })}
Counts use ICU plural variants. English has 2 forms; Arabic has all 6 (zero, one, two, few, many, other) — author every one in ar.json or it falls through to other and reads wrong. FORBIDDEN: {n === 1 ? '' : 's'} and sentence-from-fragments concatenation. Full ICU shapes + Arabic category boundaries are in project-conventions.md §5 and arabic-conventions.md §10.
Bundle note: per-message deep imports
The compiler emits outputStructure: 'message-modules' — one module per message under src/lib/paraglide/messages/<key>.js. The barrel import * as m from '$lib/paraglide/messages' is the normal call style and tree-shakes. For a thin-slice seam that must stay lazy (e.g. sign-in-messages.ts backing object-keyed getters), deep-import the single message: $lib/paraglide/messages/common_signin_edit_title. Mind inlang issue #668 (barrel vs deep-import) — details in paraglide.md.
Reference subfiles
| File | Read it when |
|---|---|
paraglide.md | Library specifics — project.inlang location, array pathPattern, generated output dir, compile/build-order rule, the locale rune + setLocale(reload:false), switching locale in dev, deep-import vs barrel (#668), hand-author-only constraint. |
arabic-conventions.md | Authoritative Arabic (MSA) chrome style — register, numerals, tashkeel, punctuation, the binding glossary, do-not-translate, interim bidi, gender/formality, ICU plurals, length/overflow. Apply verbatim. |
project-conventions.md | Engineering contract — key naming/prefixes, file layout, hand-author rule, what to translate, per-mode wiring, interpolation/plurals, the seams, the registry parity caveat, deferred guides, verification commands. |
Signals
- GitHub stars
- 44
- Forks
- 6
- Last commit
- Oct 2026
- Hacker News mentions
- 5
Advanced
- Item type
- skill
- Key
i18n-qud-technologies- Source
- github.com/qud-technologies/quranic-universal-audio
github.com/qud-technologies/quranic-universal-audio