Skip to content
Back to skills

Localize

ASecurity

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 hol...

  • 39 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 29, 2026
ai-agentsgoapi

Works with

  • api

Security analysis

A100/100

Pro scans all 8 files and shows the line behind each finding

Scanned September 29, 2026

npx -y skills add imgly/codesign --skill localize --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Localize?

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

Security grade badge for Localize
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/imgly-localize/badge)](https://www.skillsdirectory.com/skills/imgly-localize)

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: 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 **latest revision of the current design** (re-ground via `list()` / `history()` if unsure — handbook §1); each locale lands as its own revision, forked from the source. 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, the variable-bold trap, 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 | —                                                                           | latest revision of the current design |

**1. The source (reference)** — the latest revision of the current design in its source language (usually `en`). That revision's geometry is the grid every target locale snaps to. Never localize _from_ a non-reference locale — always fork each locale from the source revision. If the user means an earlier design, find it via `list()` / `history()` first.

**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 (fork the source revision, or reuse+snap an existing localized revision). Give each locale's revision a `note` naming the locale. Preview every locale before reporting done.

## 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

- **The workspace already has the locale** (an earlier pass built it — check `history()`): **reuse its
  build** — 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.
- **No existing build:** fork the reference revision 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.** Load the source-locale variant of that size 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.
2. **Load the target** — the localized scene to fix, or fork the reference 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 across the shared fork lineage** (localized variants forked from the same base
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 = engine.scene.getPages()[0];
const all = [];
(function walk(id) {
  all.push(id);
  for (const c of engine.block.getChildren(id)) walk(c);
})(page);
const blocks = all.filter((id) => id !== page);
const texts = blocks.filter((id) => engine.block.getType(id).includes('text'));
const graphic = blocks.find((id) =>
  engine.block.getType(id).includes('graphic')
);
await engine.design.loadResources([page]);

const meta = (id) => ({
  id,
  s: engine.block.getString(id, 'text/text'),
  fs: engine.block.getTextFontSizes(id, { unit: 'Pixel' })[0],
  y: engine.block.getPositionY(id)
});
const tm = texts.map(meta);

// 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);
const quote = rest.reduce((a, b) => (b.fs > a.fs ? b : a));
const others = rest.filter((t) => t !== quote);
const eyebrow = others.reduce((a, b) => (b.y < a.y ? b : a));
const attribution = others.find((t) => t !== eyebrow);

// capture rich text — each range carries its run's `text` substring, which is
// what semantic re-location needs (offsets are source-string indices).
const runsOf = (id) => {
  try {
    return (await engine.design.getProps(id, ['text.ranges'])).text.ranges;
  } catch (e) {
    return [];
  }
};
const runs = Object.fromEntries(
  [ghost, eyebrow, quote, attribution, footer].map((t) => [t.id, runsOf(t.id)])
);
// Re-apply captured styling as ONE ranges assignment. Run 0 is the base style
// and covers the whole block; any further run is an accent whose SOURCE offsets
// are meaningless once the string is translated, so it is re-located by its
// target-language substring (SPANS below).
// Returns false when the accent could not be placed — flag it, never silently flatten.
const restyle = (id, rs, spans = []) => {
  if (!rs.length) return true;
  const s = (await engine.design.getProps(id, ['text'])).text.text;
  const ranges = [{ ...rs[0], from: 0, to: s.length }]; // base over the whole string
  let ok = true;
  rs.slice(1).forEach((r, i) => {
    const span = spans[i]; // translator-supplied target-language accent text
    const at = span ? s.indexOf(span) : -1;
    if (at < 0) {
      ok = false; // accent lost in translation — report it, don't fake it
      return;
    }
    ranges.push({ ...r, from: at, to: at + span.length });
  });
  await engine.design.setProps(id, { text: { ranges } });
  return ok;
};
// Accent spans IN THE TARGET LANGUAGE, one per non-base run, keyed by block.
// Empty for single-run blocks — which is every block in this quote card.
const SPANS = {};
// setProps auto-reshapes on font/size/lineHeight writes — no manual reshape.
// ⚠ Leading is RE-DERIVED on the TARGET face — never copied raw from the reference.
// `text/lineHeight` multiplies the font's content area (per-face, ~1.15–1.35), so the
// same engine number renders a DIFFERENT visual gap once the script font is swapped —
// the #1 localized-leading defect. So EN.*.lh below is a VISUAL leading target (taken
// from the target locale's script band in the family file — latin/arabic/cjk — NOT the
// reference's raw engine value), converted through THIS block's now-current font via
// design.font({ uri }) → contentArea (handbook §6.1.3).
const setLeading = (id, visual) =>
  // facade converts per font (metrics cached at font resolve / loadResources)
  await engine.design.setProps(id, { text: { lineHeight: { visual } } }); // auto-reshapes, ranges preserved
const applyText = async (t, g, ls) => {
  await engine.design.setProps(t.id, {
    widthMode: 'Absolute',
    width: g.w,
    heightMode: 'Auto',
    text: {
      fontSize: g.fs + 'px',
      letterSpacing: ls,
      horizontalAlignment: 'Center'
    }
  }); // fontSize/letterSpacing writes auto-reshape, ranges preserved
  await setLeading(t.id, g.lh); // visual target → target-face content area (§6.1.3)
};

await engine.design.setProps(ghost.id, { text: { fontSize: EN.ghost.fs + 'px' } });
await setLeading(ghost.id, EN.ghost.lh);
await applyText(eyebrow, EN.eyebrow, trackOK ? EN.eyebrow.ls : 0);
await applyText(attribution, EN.attribution, trackOK ? EN.attribution.ls : 0);
await applyText(footer, EN.footer, trackOK ? EN.footer.ls : 0);
await applyText(quote, EN.quote, 0);
await engine.design.setProps(graphic, { width: EN.rule.w });
for (const t of [ghost, eyebrow, quote, attribution, footer])
  restyle(t.id, runs[t.id], SPANS[t.id]);
await engine.design.loadResources([page]);

// position (ghost measured for mirroring; others straight from EN)
const gw = engine.block.getFrameWidth(ghost.id);
await engine.design.setProps(ghost.id, {
  position: {
    x:
      ghostAnchor === 'center'
        ? (PW - gw) / 2
        : RTL
          ? PW - EN.ghost.x - gw
          : EN.ghost.x,
    y: EN.ghost.y
  }
});
for (const [t, g] of [
  [eyebrow, EN.eyebrow],
  [quote, EN.quote],
  [attribution, EN.attribution],
  [footer, EN.footer]
]) {
  await engine.design.setProps(t.id, { position: { x: g.x, y: g.y } });
}
await engine.design.setProps(graphic, {
  position: { x: EN.rule.x, y: EN.rule.y }
});

// overflow guard — shrink ONLY the quote, never move neighbours
await engine.design.loadResources([page]);
let qfs = EN.quote.fs;
const avail = belowQuoteY - EN.quote.y - 16;
while (engine.block.getFrameHeight(quote.id) > avail && qfs > 44) {
  qfs -= 2;
  // the fontSize write auto-reshapes AND auto-preserves the style ranges.
  await engine.design.setProps(quote.id, { text: { fontSize: qfs + 'px' } });
  await engine.design.loadResources([page]);
}
return { type: 'text', text: JSON.stringify({ page, qfs }, null, 1) };
```

Reference-capture read (run on the source variant to fill `EN`):

```js
const page = engine.scene.getPages()[0];
const out = [];
(function walk(id) {
  const t = engine.block.getType(id),
    isText = t.includes('text');
  const r = {
    t: t.split('/').pop(),
    x: Math.round(engine.block.getPositionX(id)),
    y: Math.round(engine.block.getPositionY(id)),
    w: Math.round(engine.block.getWidth(id)),
    wm: engine.block.getWidthMode(id)
  };
  if (isText) {
    r.s = engine.block.getString(id, 'text/text').slice(0, 14);
    r.fs = Math.round(engine.block.getTextFontSizes(id, { unit: 'Pixel' })[0]);
    r.lh = +engine.block.getFloat(id, 'text/lineHeight').toFixed(2);
    r.ls = +engine.block.getFloat(id, 'text/letterSpacing').toFixed(2);
  }
  if (id !== page) out.push(r);
  for (const c of engine.block.getChildren(id)) walk(c);
})(page);
return {
  type: 'text',
  text: JSON.stringify({
    pw: engine.block.getWidth(page),
    ph: engine.block.getHeight(page),
    b: out
  })
};
```

## Video and animated designs

Applies when the page has a `playback.duration`, timed blocks, `animations`, an `audio` voice-over
or a `captionTrack`. Geometry still snaps to the reference; the **timeline** is re-derived from the
new language's speech and reading time.

- **Reading time sets the hold.** A line must stay on screen ≈**0.3 s per word** after its
  entrance finishes. Expansion languages (German, French, Spanish run 15–35 % longer) need longer
  holds: extend the block's `playback.duration`, shift what follows, and grow the page's
  `playback.duration` — never shorten reading time or speed up speech to fit the source length.
- **Word-by-word reveals scale with the target.** A `typewriter_text` (or any text animation whose
  `textAnimationWritingStyle` is `Word` / `Character`) reveals per word or per character: scale its
  duration by target ÷ source word (or character) count, and check that the reveal ends with the
  0.3 s-per-word hold still ahead of it. The duration is not in the animation spec:
  `engine.block.setDuration(engine.block.getInAnimation(id), s) // engine.block: animation duration`.
- **Re-voice, don't re-use.** Generate the voice-over again from the translated script with a
  `text2speech` model — `asset_generate({ capability: 'text2speech' })` lists them (e.g.
  `elevenlabs/eleven-v3-tts`); keep the master's voice across locales via `params`
  (`asset_generate({ model, schema: true })` names the model's inputs). Swap the file on the
  existing audio block — `setProps(audio, { audio: { fileURI } })` — so its offset and volume stay.
- **Captions come from the new take.** Transcribe it with a `speech2text` model
  (`elevenlabs/scribe-v2`, via `params` — check its inputs with `schema: true`); the result carries
  word-level `start` / `end`. Rebuild the captions from those words: a caption per phrase (break at
  sentence ends, and at commas once a phrase has two words), `timeOffset` = the audio block's
  offset + the first word's `start`, held until the next caption. Carry the master captions' frame
  and style onto **every** new caption (it is per caption, not shared) — and their typeface:
  captions have their own font (`caption/typeface`, `caption/fontFileUri`), so the script face goes
  there too, not only on text blocks.
- **Re-snap the timeline to the new speech — one rule.** Every timed block that enters on a spoken
  cue keeps its **lead**: new `timeOffset` = master `timeOffset` − master cue time + target cue
  time. A block that ended where another began still ends there; a block that ran to the end of
  the page still does; page duration = max(master duration, voice-over offset + last word's `end` +
  the master's closing silence).
- **RTL mirrors the motion, not only the layout.** For Arabic / Hebrew, mirror horizontal
  directions: `slide` / `pan` `direction` θ → `π − θ` (`0` ↔ `π`; it is the direction of travel),
  `wipe` `Left` ↔ `Right`, `caption/horizontalAlignment` `Left` ↔ `Right`. The do-not-mirror
  inventory (icons, logos, numerals, photos) holds for motion too.
- **Preview at several `time`s, per locale.** Mid-reveal of each word-by-word line, the end of each
  hold, each caption change and the last second (`preview({ revision, time })`; a scene that refuses
  `time` needs the Video-mode switch in the `resize` skill), then export the
  mp4 — a caption out of sync with the voice or a line that leaves mid-sentence shows only in time.

## Carrying rich text across a translation

Styled ranges are stored as character offsets into the block's string. Translate the string and
those offsets point at different words — sometimes at nothing. So a localized variant must carry
rich text **semantically**, not numerically.

- **Capture with `getProps(id, ['text.ranges'])`, never `getTextColors(id)[0]`.** The latter reads run 0 only.
  Re-applying it whole-block is what silently flattens a two-color headline.
- **Run 0 is the base style**; apply it across the whole translated string first.
- **Every further run is an accent**, and you need its **target-language substring** to place it.
  Ask for it as part of the translation: translate `"Save 20% today"` **and** tell me which
  target-language span carries the accent (`"20 %"`, `"٪٢٠"`, …). Locate that span with
  `indexOf`, then re-apply.
- **A span that can't be located is a reportable defect**, not something to paper over. Apply the
  base style, leave the accent off, and say so — a flattened headline that nobody mentions is
  exactly the failure this section exists to prevent.
- **The accent may not survive as a span at all.** Word order moves it, agglutinative targets fuse
  it into a longer word, and some languages have no equivalent fragment. When that happens, prefer
  moving the emphasis to the phrase the target language actually stresses over splitting a word.
- **Paragraph state is separate from runs.** List style/level and per-paragraph alignment/leading
  live on paragraph indices, not character offsets — re-apply them per paragraph after any string
  re-write, and keep the item count matched (a translation that merges two bullets into one
  silently drops a list item).
- **`text.case` is not a translation artefact.** All-caps set through the `case` prop re-cases the
  target string automatically; all-caps typed into the string is frozen English and cannot be
  correctly cased in scripts that have no case at all. Carry the property, not typed capitals.

## Common mistakes / red flags

- **Per-variant re-centering / re-sizing** → drift. If you're re-centering a block because the
  translation is a different length, STOP — snap it to the reference position instead.
- **Line-height copied raw across the font swap** → wrong gap. `text/lineHeight` is content-area
  scaled and per-face; re-derive it on the target face (`setLeading`, §6.1.3), never carry the
  reference's engine number. The single most common localized-leading defect.
- **Whole block landed a few px low (ar/ja "moved down")** → **baseline drift**. A `position.y` write
  snaps the frame **top** (= ascender line), but the script face's ascender/descender differ from
  the Latin source, so identical `y` renders a different baseline. Snap **baseline-relative** when
  the face changes: after positioning + `loadResources`, nudge `y` by the ascent delta
  `(refAscent − targetAscent) × fontSize` (both `ascender/unitsPerEm` via `design.font({ uri }).metrics`), or
  align the block by a shared optical anchor rather than the frame top. Confirm against the source
  render (the `judge` source-parity check).
- **"Bold" from a variable TTF** → renders Regular. Use a static Bold file (`reference/font-loading.md`).
- **Weight not carried onto the swapped face (text "too light / unreadable")** → the swap lands on
  the family's lightest cut. Capture the reference block's weight and re-apply it on the target
  face (verify it actually renders — the variable-bold trap, `font-loading.md`); parity is checked
  by the `judge` source-parity render diff.
- **Appearance not carried (fill/gradient/effect/corner-radius/overlay differs from the source)** →
  fork the source revision so surface styling is inherited; if you rebuilt instead of forked, or a
  reshape/recolor dropped it, re-capture and re-apply. Don't re-create a fill or effect "by eye" —
  read its values off the source.
- **Reshape wiped the accent color / bold / underline** → capture with `getProps(id, ['text.ranges'])` before the
  reshape and re-apply after. If you captured with `getTextColors(id)[0]` you captured run 0 only,
  and re-applying it whole-block is itself the flattening bug.
- **Accent landed on the wrong word after translation** → run offsets were replayed verbatim. They
  are source-string indices; re-locate the accent by its target-language substring instead.
- **Bullets became separate blocks / lost their hanging indent in the variant** → list state is
  per-paragraph and does not survive a string re-write. Re-apply `text: { listStyle }` (and the level via raw `setTextListLevel`)
  per paragraph after translating.
- **Letter-spacing on Arabic** → severs joins. `letterSpacing = 0`.
- **RTL flip disassembled an icon / broke in-frame centering** → mirror operates on **groups, not
  loose blocks**, and **icons/logos/brand marks/numerals/charts/photos must not mirror** at all.
  Group a composed icon and flip the group as a unit (or exempt it); see the do-not-mirror
  inventory in `reference/arabic.md`.
- **Plausible page, wrong font** → silent sans fallback / tofu. Preview every locale.
- **Extra or missing decorative glyph** vs the reference → reconcile the count.
- **Non-native export size** → export the page at its own pixel dimensions.

## Cross-references

- **`reference/font-loading.md`** — non-Latin / non-handbook font mechanics (this skill's font body).
- **`reference/language-rules.md`** — cross-language quality rules + pass checklist.
- **`reference/latin-expansion.md` / `arabic.md` / `cjk.md` / `vietnamese.md` / `cyrillic.md`** — per-script-family conventions, leading bands, quotation marks.
- handbook §6.1 — general typographic thresholds (leading, tracking, all-caps & diacritic leading, the content-area line-height conversion (`design.font`), smart punctuation, reshape-then-recolor); §6.7 — the Latin font baseline; and the handbook's reshape / `loadResources` / dirty-on-throw runtime quirks.
- **`judge`** — the acceptance gate (the `localization` axis scores this skill's output).
- **`resize`** — the sibling skill for changing size/format (re-compose for a new aspect).

Files in this skill

  • SKILL.md29.7 KB
  • reference/arabic.md4.4 KB
  • reference/cjk.md4.9 KB
  • reference/cyrillic.md1.6 KB
  • reference/font-loading.md8.9 KB
  • reference/language-rules.md12.7 KB
  • reference/latin-expansion.md6 KB
  • reference/vietnamese.md1.9 KB

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.