- name
- localize
- description
- Use when creating or fixing translated / localized variants of an existing CoDesign / CE.SDK design (e.g. de/ja/ru/ar editions); when a localized edition's text blocks or decorative elements have drifted, re-centered, or resized away from the source layout; when translated copy is tofu, overflowing, mis-tracked, or the wrong font after a language swap; or when loading a non-Latin script (Cyrillic / CJK / Arabic RTL / Vietnamese) font into a scene. Also for animated / video designs: longer holds, re-timed reveals, a new voice-over and captions per language.
# localize
Point this skill at the current design and the target locales, and it produces or repairs the localized editions. It operates on the **design in the current session**, and each locale is **saved as its own `.imgly` file**. Single home for localizing CoDesign designs: the **operational workflow**, the **font-loading mechanics**, and the **per-language quality rules**. It builds on two things it does _not_ restate: handbook §6.1 (general typographic thresholds — leading, tracking, all-caps, the content-area line-height conversion (`design.font`), smart punctuation, reshape-recolor) and handbook §6.7 (general Latin font mechanics — single-URI `text.font` declarations, static-vs-variable, blank-glyph diagnosis). This skill is the non-Latin / localization extension of both.
**Scoring:** localizing is judged with the `judge` skill — every applicable axis ≥ 8, no averaging, N/A = 10. A single tofu glyph, an overflow after expansion, un-mirrored RTL, or a block that drifted off the source grid fails the edition (the `localization` axis).
## Reference files (load on demand)
This `SKILL.md` holds the always-needed spine: inputs, core principle, workflow, block
classification, and the snap-to-reference routine. Two situational bodies of rules live beside it
and are pulled in only when the task needs them:
- **`reference/font-loading.md`** — getting a non-Latin / non-handbook face to render (jsDelivr
picks, weights from a variable file, font-declaration gotchas, the silent-sans failure). Read it whenever the
target locale needs a Cyrillic / CJK / Arabic / Vietnamese font, or copy came out tofu / wrong
face.
- **`reference/language-rules.md`** — the cross-language spine: preserve-vs-localize, translation
quality, layout integrity after translation, and the localization pass checklist.
- **`reference/<family>.md`** — per-script-family conventions, numeric leading bands, and
quotation marks, one file per family: `latin-expansion.md` (de/fr/es…), `arabic.md`, `cjk.md`
(ja/zh/ko), `vietnamese.md`, `cyrillic.md` (ru…). Read the file(s) for the **target locale(s)**
you're building, e.g. `reference/arabic.md`.
Several locales of one design, or re-applying a master fix to locales you already built: the
**master → editions** workflow, `../resize/editions.md`.
For an animated or video design, read **Video and animated designs** below as well.
## Inputs — what to localize, and into what
Resolved before any work by the intake contract — derive, ask once, echo, invent nothing:
`../handbook/intake.md`. **R** = required, no default; **A** = ask if
underived; **D** = defaultable.
| Parameter | Kind | Chips / values | Default |
| -------------- | ------- | --------------------------------------------------------------------------- | --------------------------------- |
| target locales | **R** | `German` · `French` · `Spanish` · `Japanese` · `Chinese` · `Arabic` · Other | none |
| source locale | A | detected from the design's copy · Other | detected, stated in the echo |
| source design | derived | — | the design in the current session |
**1. The source (reference)** — the design in the current session, in its source language (usually `en`). **Save it first** (`export({ format: 'imgly' })`) and keep the `uri`: its geometry is the grid every target locale snaps to, and every locale starts by re-importing that file. Never localize _from_ a non-reference locale.
If the user means a design saved earlier, its `uri` IS the reference `uri`: `open` it to `preview` and capture it, skip the first save, then `import` a copy of it per locale.
A session holds one design, so the locales are built in sequence and the saved reference is what you return to between them (handbook §1):
```
export({ format: 'imgly' }) → REFERENCE_URI (save the source FIRST)
per locale:
import({ source: { uri: REFERENCE_URI } }) → a copy of the reference is the session
edit(…) translate + swap the script font + snap to the reference geometry
preview() → check it against the reference render
export({ format: 'imgly' }) → that locale's own file
```
**2. The locales** — the target languages, e.g. `de ja ru ar`. If omitted, **ask** —
_**Which languages do you need?**_ — with the chips above; there is no defaulting to a language the
user didn't name. Then echo the resolved set ("1 design × 3 locales, source `en` (detected)") before
building.
**Never invent copy.** A translation is the user's own content: if a block's meaning is ambiguous,
or a term is a name/product you cannot safely render in the target script, ask rather than guess —
see the keep-byte-identical list in `reference/language-rules.md` §1.
**Per locale, then:** run the Workflow below (re-import the reference, or load an existing localized file and snap it). Give each locale's `edit` a `note` naming the locale, `preview` it, and **save it** — an unsaved locale is one the user never receives. Report every locale's `uri` at the end.
## Core principle — localize, then snap back to the reference
Localizing is **surgical**: change the words + the font the new script forces, and **nothing
else**. Then **snap every block back to the reference variant's geometry** so the localized
edition is positionally identical to the source.
- The **reference** is the source-locale variant (usually `en`) **of the same size**. Each size
has its own reference grid.
- **Never re-center or re-size a block per variant "to fit better."** That independent re-fitting
is exactly what makes a localized set drift — the eyebrow creeps up, the quote grows, the rule
and footer slide down, and the editions stop matching. If there is room for the translated text
at the reference position/size, keep the reference position/size.
- Deviate from the reference **only** when the translated text genuinely overflows (expansion
languages) — then shrink _that block's_ font within its band; do not move its neighbours.
- **Swap the script font per block, not per scene.** Apply the new face **only to blocks whose
copy is actually in the target script.** A block that stays in the source script — a personal
name, brand/wordmark, `@handle`, URL, email, number, date, or product/tech name (the "keep
byte-identical" list in `reference/language-rules.md` §1) — **keeps its original typeface.**
Re-fonting it to the script face is drift even though its text was correctly preserved: a Latin
name rendered in the Arabic/CJK serif is the classic tell. So a localized edition differs from
its reference in **only the translated blocks' text + face**; every untranslated block is
byte-identical (position mirrored for RTL).
## When to use
- Producing new locale editions (de/ja/ru/ar/…) of a finished design, across one or more sizes.
- Fixing a localized set whose blocks drifted / re-centered / resized vs the source.
- Post-translation breakage: tofu, overflow, wrong tracking, Latin-looking sans where a script
font should be.
- Loading a non-Latin or non-handbook typeface into a scene.
**Not for:** the original design build (that's the handbook + §6); pure copy edits in the
same language. To change
the **size/format** — re-compose a design for a new aspect rather than translate it — use the **resize** skill
(localize snaps geometry; resize re-derives it).
## Reuse an existing localized build vs build fresh
- **A saved file for the locale already exists** (an earlier pass built it — the user has its `uri`):
**reuse its build** — `open({ uri })` it (`import` would give you a copy); the translated copy,
script fonts, and RTL are already solved — but still **snap it to the reference geometry**.
Earlier builds commonly drift; reuse the assets, fix the positions. Your fix goes to that design
itself.
Save it when you are done.
- **No existing build:** `import` a copy of the reference file and swap text + font.
Either way the last step is the same: every block lands on the reference grid.
## Workflow
1. **Capture the reference geometry.** With the source-locale design in the session, `preview` it
(that render is your parity reference for the `judge` gate) and read, per
block: `x, y, width, widthMode, heightMode, fontSize (Pixel), lineHeight, letterSpacing,
alignment`, plus page `w/h`, the ghost's anchor (left / centered), and the rule position. Do this
**once**; the captured JSON is what you carry from locale to locale.
2. **Load the target** — `open` the existing localized file to fix, or `import` a copy of the
reference file for a fresh build.
3. **Classify blocks by role** (see below).
4. **Apply the reference geometry** to each corresponding block; keep the localized **text** and
the **script font** (for a non-Latin/non-handbook face see `reference/font-loading.md`). Set
per-locale: `letterSpacing = 0` for CJK & Arabic; RTL ghost mirrored to the right; alignment as
the reference (centered-symmetric stays centered — see `reference/arabic.md`).
5. **Reconcile ghost count.** Keep the reference's number of decorative glyphs. If the localized
scene has extra ones (e.g. an Arabic closing-quote bookend the English lacks), **destroy the
extras**; mirror the kept one for RTL.
6. **Overflow guard.** After setting the reference font size, measure the quote's frame height; if
it collides with the element below, step the font size down (only that block) until it fits.
Never move neighbours to make room.
7. **Re-apply rich text after a TRANSLATED string lands.** Font/size/line-height changes via
`setProps` auto-reshape with every style range preserved — no manual step. A **translated
string** is different: the write replays ranges at their OLD offsets, which are source-string
indices. Capture each block with `getProps(id, ['text.ranges'])` first (each range carries its
`text` substring), re-apply semantically after. ⚠ `getTextColors(id)[0]`
is **not** a capture — it reads the first run only, so re-applying it flattens a two-color
headline onto one color and the accent is gone with no error. Paragraph state
(`getProps(id, ['text.paragraphs'])` — listStyle/listLevel per paragraph) survives a same-string reshape but not
a **re-write**, so re-apply it whenever the string itself changed. **Ranges are index-based on
the source string** — a translated string has different offsets, so never replay `from`/`to`
verbatim; re-locate the styled substring in the target language (§ "Carrying rich text across
a translation").
8. **`loadResources` → `preview`.** One preview per locale — tofu / RTL / overflow /
position match are only visible in the render.
A video design needs several per locale, at different `time`s (below).
Then apply the per-locale quality rules in `reference/language-rules.md` **and the family
file(s) for the target locale(s)** (`reference/latin-expansion.md`, `arabic.md`, `cjk.md`,
`vietnamese.md`, `cyrillic.md`), and dry-run the localization pass checklist against the render
before reporting done.
## Block-role classification
Classify by measurable properties, not hard-coded ids (ids are session-scoped). For a
quote-card family the roles are:
- **ghost** (oversized decorative quote) — largest font size **and** trimmed string length ≤ 2.
A design may have **more than one** (opening + closing bookend); collect all.
- **footer / handle** — text containing `@`.
- **quote / headline** — the largest remaining text after removing ghosts + footer.
- **eyebrow** vs **attribution** — the two small texts; eyebrow sits **above** the quote,
attribution **below**.
- **rule / divider** — the graphic (non-text) block.
Roles are design-specific. The general rule: **map each localized block to its reference block by
role, or by index** (every locale is imported from the same reference file, so they keep the same
child order), then restore that reference block's geometry.
## The snap-to-reference routine
A single CoDesign `edit` body. Fill the `EN` reference geometry (from step 1) and the locale
flags, then it classifies, restores geometry, reconciles ghosts, applies per-locale tracking /
RTL mirroring, guards overflow, and re-applies colors across the reshape.
⚠ This routine is a **worked example, not a template**: every number in `EN` below is from one
example quote-card, and the classification heuristics (`fs >= 150`, `s.includes('@')`, …) are
that card's — never reuse either literally. Fill `EN` only from your own reference capture (the
read at the bottom), and rewrite the classification for the actual design's roles.
```js
// --- config: reference geometry (read from the source-locale variant of THIS size) ---
const PW = 1080,
PH = 1350; // page dims
const EN = {
// lh = VISUAL leading target (from the target locale's script band, NOT the reference's
// raw engine number) — setLeading() converts it through each block's target face. For a
// localized display headline in a diacritic language, this is where the conditional +0.2
// lives (latin-expansion.md): bump ≤1.2 → ~1.35 only if the rendered gap needs it.
ghost: { x: 70, y: 55, fs: 300, lh: 1 }, // Auto-sized; keep size+lh, reposition
eyebrow: { x: 110, y: 395, w: 860, fs: 16, lh: 1, ls: 0.25 },
quote: { x: 110, y: 529, w: 860, fs: 62, lh: 0.85 }, // ls always 0
rule: { x: 504, y: 930, w: 72 }, // graphic
attribution: { x: 110, y: 960, w: 860, fs: 16, lh: 1, ls: 0.25 },
footer: { x: 110, y: 1225, w: 860, fs: 15, lh: 1, ls: 0.12 }
};
const RTL = false; // true for Arabic
const trackOK = true; // false for CJK/Arabic → force letterSpacing 0 on eyebrow/attr/footer
const ghostAnchor = 'left'; // 'left' (post/square) mirrors to the right for RTL; 'center' (story) stays centered
const belowQuoteY = EN.rule.y; // element the quote must stay above
const [page] = await engine.design.findByType('page');
const blocks = [];
const walk = async (id) => {
for (const c of await engine.design.getChildren(id)) {
blocks.push(c);
await walk(c);
}
};
await walk(page);
const typeOf = async (id) => (await engine.design.getProps(id, ['type'])).type;
const texts = [];
let graphic;
for (const id of blocks) {
const type = await typeOf(id);
if (type === 'text') texts.push(id);
else if (type === 'graphic') graphic ??= id;
}
await engine.design.loadResources([page]);
const meta = async (id) => {
const p = await engine.design.getProps(id, [
'text.string',
'text.fontSize',
'position'
]);
return {
id,
s: p.text.string,
fs: parseFloat(p.text.fontSize),
y: p.position.y
};
};
const tm = [];
for (const id of texts) tm.push(await meta(id));
// classify
const ghosts = tm.filter((t) => t.fs >= 150 && t.s.trim().length <= 2);
let ghost =
ghosts.find((t) => t.s.includes('“')) ||
ghosts.slice().sort((a, b) => a.y - b.y)[0];
for (const g of ghosts.filter((t) => t !== ghost))
await engine.design.destroy(g.id); // drop extra bookend glyphs
const footer = tm.find((t) => t.s.includes('@'));
const rest = tm.filter((t) => !ghosts.includes(t) && t !== footer);
Voir sur GitHub