expand-site-language · v1.0.0 · 2026-09-18 · sha256 3193428e4b0d18b2
expand-site-language v1.0.0A
Immutable. This exact content never changes and is served at /api/v1/blob/3193428e4b0d18b2.
---
name: expand-site-language
description: >
Add a NEW language to an EXISTING site across ALL its content, and translate the pages later
without blocking. Use when the owner says "add Armenian / Spanish / French to the whole site",
"make the site multilingual", "translate the whole site into X", "scale this to more languages",
"add a new language to all pages/sections", or "add a locale". This is the ONLY correct way to
add a language to existing content. Do NOT improvise it by hand and do NOT re-compose a group —
neither adds a per-page locale to existing pages, and re-composing overwrites what is there. Two
scripts in this skill's own folder do the work: fan-out-site-language.mjs (fans the language out,
seeded with the default language so the site is valid instantly, no translation API) and
translate-content-page.mjs (the non-blocking runner — you translate the strings later). Self-
sufficient: plain Node, no external service, no other agent.
version: 1.0.0
metadata:
---
# expand-site-language
> Informational, not binding. **Know a better way for the case in front of you — do it your way and
> say so.** You are trusted with the creative work on this project.
The ONE way to take a site that already has content in one or two languages and **safely scale it
to another language**. Deterministic file operations, **NO code generation, NO external translation
API** (you are the translator — subscription rule). Self-sufficient: any single agent can do it.
## 🛑 Why a dedicated capability (do not improvise)
Adding a language to an **existing** site means creating a `_data/<lang>.ts` for **every** group and
**every** post, patching each `index.ts` + `group.ts`, and protecting SEO. **No other tool does this:**
- **Creating a page by hand** — a new page is a new page; it does nothing for the dozens that already
exist, and each of them needs its own `_data/<lang>.ts`.
- **Adding the language to the menu manifest only** — the language then shows in the switcher while the
pages behind it do not exist. A visible broken language is worse than an absent one.
- **Re-composing a group** — it **overwrites** existing content. Never use it to add a language.
If you are tempted to reach for one of those to add a language: **stop and use this skill instead.**
## The model
- Languages are **build-time** (`NEXT_PUBLIC_SUPPORTED_LANGUAGES`). A language must be in the set
**before** you fan it out — add it via **manage-app-settings** first, then rebuild.
- Each post = `_data/{meta, en(base), <lang>(override), index}`. Each group = `_data/{en, <lang>, index,
group.ts}`. The fan-out writes the `<lang>` files and patches the indexes — by construction.
- **Seed = the DEFAULT language's content** (e.g. default `es` → the new language starts as a copy of
Spanish). The site is **valid the instant the build finishes** — no broken pages, no machine
translation. Every language-dependent link is rewritten to the new language.
- **🔒 Doorway guard (SEO, critical).** A seed still shows the default language's text, so each seed is
marked `needsTranslation` and the engine serves it as **`robots: noindex`** — Google never indexes a
cross-language duplicate. `canonical` + `hreflang` stay correct automatically (derived from the
language set). When a page is translated, the marker clears and it becomes indexable on the next Deploy.
- **Non-blocking.** The fan-out returns the pages that need translating in `pagesNeedingTranslation`;
**name them to the owner so a step per language can be opened in the panel**, listing those pages in
its plan. Translation happens later, in that step, possibly with a different model — the main work is
never blocked by translation limits. The scripts write no steps themselves: steps live in the
product's dossier, and one writer keeps them, not three.
## Flow
1. **Ensure the language is in the set.** Not there yet → add it with **manage-app-settings**
(the panel's Languages page, or `POST /api/config/languages`) and rebuild. The fan-out refuses a language not in the set.
2. **Fan it out.** `node fan-out-site-language.mjs --out <slot-root> --lang <L>` — `--dry-run` first (restate +
confirm, §8.2), then for real. **REBUILD** (Deploy in the panel's footer) to publish the new routes
(seeded, noindex). The menus (header / footer / left / right) update automatically.
3. **Translate later (non-blocking), when you choose.** `node translate-content-page.mjs --out <slot-root> --lang <L> --op next` in a
loop: it returns the next pending page → you translate the **strings only** (keep the block kinds and
order, keep the root anchor and `/<lang>/` links) → call again with `{ op:"write", tab, slug,
translations }`. Repeat until `remaining: 0`. Honor any owner notes on the dev-step (e.g. "focus on
Spanish law, link real statutes") for regional value.
4. **The runner does NOT deploy.** When translations are written, tell the owner: *press **Deploy** in
the footer* to publish — the translated pages then flip from noindex to indexable.
## Confirm before mutating (§8.2)
> If I understood correctly: **add language «<lang>»** across <N> groups / <M> pages, seeded with the
> default language «<def>» (noindex until translated), and open one translation step. Shall I proceed?
`--dry-run` → preview → owner yes → the real run.
## If the tool errors (not a refusal)
A **refusal** (`ok:false, refused:true` — language not in the set, structure-parity violation) → fix
and retry. A crash (`MODULE_NOT_FOUND`, an exception) means the script is **broken** — stop, report the
exact error, wait. Never hand-author the locale files as a workaround; a broken script is repaired,
never bypassed.
## How to call
- **The two commands:**
- `node fan-out-site-language.mjs --out <slot-root> --lang <L> [--dry-run]`
- `node translate-content-page.mjs --out <slot-root> --lang <L> --op next` → then `--op write --tab <tab> --slug <slug>` with the translated strings
- **Standalone (no panel at hand)** — plain file edits:
```bash
# 1) fan out (default-language seed + noindex + per-language step)
node .claude/skills/expand-site-language/fan-out-site-language.mjs --out . --lang hy --dry-run
node .claude/skills/expand-site-language/fan-out-site-language.mjs --out . --lang hy
# 2) translate, page by page (you supply the translated strings)
node .claude/skills/expand-site-language/translate-content-page.mjs --out . --lang hy --op next
node .claude/skills/expand-site-language/translate-content-page.mjs --out . --lang hy --op write \
--tab news --slug sample-1 --data translations.json
npx tsc --noEmit # then REBUILD (Deploy) to publish
```
## Self-sufficient
The skill lives in this project (`.claude/skills`) and depends on nothing outside it — a single agent
can extend a site's languages on its own.