Localize logo

Localize

Organization
imgly
localize

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.

Overview

Publisherimgly
Repositorycodesign
Skill namelocalize
Stars
43
Forks
1
Bundled files
7
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 7 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by imgly on GitHub. Read the source before you install it.

Installation

Install the Localize AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/imgly/codesign/tree/main/plugins/codesign/skills/localize
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/imgly/codesign.git /tmp/codesign
mkdir -p .claude/skills
cp -r /tmp/codesign/plugins/codesign/skills/localize .claude/skills/localize
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Localize in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Localize on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Localize is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

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.

ParameterKindChips / valuesDefault
target localesRGerman · French · Spanish · Japanese · Chinese · Arabic · Othernone
source localeAdetected from the design's copy · Otherdetected, stated in the echo
source designderived—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 times (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);
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 = async (id) => {
  try {
    return (await engine.design.getProps(id, ['text.ranges'])).text.ranges;
  } catch (e) {
    return [];
  }
};
const runs = {};
for (const t of [ghost, eyebrow, quote, attribution, footer])
  runs[t.id] = await 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 = async (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 = async (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])
  await restyle(t.id, runs[t.id], SPANS[t.id]);
await engine.design.loadResources([page]);

// position (ghost measured for mirroring; others straight from EN)
const { width: gw } = await engine.design.measure(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 ((await engine.design.measure(quote.id)).height > 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] = await engine.design.findByType('page');
const ids = [];
const walk = async (id) => {
  for (const c of await engine.design.getChildren(id)) {
    ids.push(c);
    await walk(c);
  }
};
await walk(page);
const TEXT = [
  'text.string',
  'text.fontSize',
  'text.lineHeight',
  'text.letterSpacing'
];
const out = [];
for (const id of ids) {
  const { type } = await engine.design.getProps(id, ['type']);
  const p = await engine.design.getProps(id, [
    'position',
    'width',
    'widthMode',
    ...(type === 'text' ? TEXT : [])
  ]);
  out.push({
    t: type,
    x: Math.round(p.position.x),
    y: Math.round(p.position.y),
    w: Math.round(p.width),
    wm: p.widthMode,
    ...(p.text && {
      s: p.text.string.slice(0, 14),
      fs: Math.round(parseFloat(p.text.fontSize)),
      lh: +p.text.lineHeight.toFixed(2),
      ls: +p.text.letterSpacing.toFixed(2)
    })
  });
}
const { width: pw, height: ph } = await engine.design.getProps(page, [
  'width',
  'height'
]);
return { type: 'text', text: JSON.stringify({ pw, ph, 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. Patch it with the same type: setProps(id, { animations: { in: { type, duration: s } } }).
  • 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. Respell names in the translated script as they are said in that language, and keep the written names in the captions (../models/audio.md).
  • 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 times, per locale. Mid-reveal of each word-by-word line, the end of each hold, each caption change and the last second (preview({ time })) — a caption out of sync with the voice or a line that leaves mid-sentence shows only in time. Then export the mp4.

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).
  • A weight the typeface does not declare → "Failed to find a font with the specified weight". Declare it: a variable TTF takes one fonts[] entry per weight, same uri (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 — declare the weight, 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) → import the saved reference file so surface styling is inherited; if you rebuilt instead, 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).

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Localize AI skill do?

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.

Why use Localize on TypingMind?

Because you install it once and use it with any model. Localize is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Localize in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/imgly/codesign/tree/main/plugins/codesign/skills/localize. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Localize?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Localize?

As many as you like. As long as a model supports skills, you can use Localize with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Localize AI skill free?

Yes. It is published on GitHub by imgly under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇