--- name: cjk-text-wrap-audit description: > Diagnose and fix CJK (Korean, Japanese, Chinese) text-wrapping issues in web UIs. Systematically traces mid-syllable breaks, orphaned glyphs, and awkward line splits through a layered fix strategy covering global CSS cascade, component-library overrides, headline balancing, and translation-level phrase binding. Produces an evidence-based diagnosis and verification report before any code change is applied. license: MIT compatibility: - Claude Code - Cursor metadata: type: execution category: review maturity: draft estimated_time: 15 min --- # Skill: CJK Text Wrap Audit **Type:** Execution ## Purpose Turn ad-hoc CJK (Korean, Japanese, Chinese) line-break firefighting into a layered, evidence-first audit. CJK wrap bugs are a class of cross-cutting cascade defects: a missing `word-break` rule on `body`, an Ant Design / MUI / Mantine `cssinjs` override that wins on specificity, an over-eager `text-wrap` algorithm, and a translation string with no phrase boundary can each independently cause text to shatter mid-syllable or strand a single glyph at line edge. This skill prescribes a fixed sequence of diagnostic gates so the audit produces an evidence-backed report — codepoint dump, computed-style trace, and verification matrix — *before* any code is changed. --- ## When to Use - Korean / Japanese / Chinese hero or headline text breaks mid-syllable. - A single glyph or word is orphaned at the end or start of a line. - The bug appears only in CJK locales while Latin locales render fine. - A user reports an awkward break in a screenshot but the code "looks right". - Browser zoom (WCAG 1.4.4 Resize Text) reveals breaks that are invisible at 100%. - A previous "fix" used `
`, hardcoded widths, or `!important` and the bug came back. --- ## When NOT to Use - Pure Latin or RTL wrapping issues (use a generic CSS audit instead). - Font-loading or glyph-substitution problems (FOUT / FOIT, missing glyphs, fallback fonts). - Print or PDF rendering pipelines (different break algorithm). - Plain-text terminals or markdown `pre` blocks (wrap is a terminal concern, not CSS). - Layout overflow caused by fixed widths or `white-space: nowrap`, not by wrapping behavior. --- ## Inputs Required Do not run this skill without: - [ ] Screenshot of the offending render at the actual breakpoint. - [ ] Locale code and the raw translation string (file path + key). - [ ] Path to the component file containing the element. - [ ] Live URL or deterministic repro steps. - [ ] CSS / component framework in use (Tailwind, Ant Design, MUI, Mantine, Chakra, plain CSS, …). - [ ] Browser and zoom level where the bug reproduces. Optional but recommended: - [ ] List of all CJK locales the project ships. - [ ] Light/dark mode information if the affected text changes color. - [ ] Prior commit(s) that touched the same string or component. Without asking the user (unless unavailable), gather these inputs directly from the repository and the running app. --- ## Output Format Produce a structured 7-section report. Do not produce code changes until Section 7 is approved. 1. **Symptom Report** — locale, breakpoint, browser, zoom, screenshot reference, exact observed break (quote the broken line). 2. **Diagnosis** — codepoint dump of the raw string, computed values of `word-break` / `overflow-wrap` / `text-wrap` / `hyphens` / `line-break`, and the cascade trace identifying the *winning* declaration with its specificity. 3. **Root Cause** — name the failing layer: global CSS, component library injector, headline balancing algorithm, or translation string itself. 4. **Fix Strategy** — which of the four layers to touch, in what order, and *why* additional layers are or are not needed. 5. **Proposed Patch** — minimal concrete diffs (file path + before / after). No speculative refactors. 6. **Verification Plan** — explicit matrix of zoom levels × breakpoints × locales × theme modes to be re-checked. 7. **Approval Gate** — explicit statement: "Awaiting approval before applying patch." Do not edit code until the user confirms. --- ## Procedure ### Gate 0 – Symptom Capture Collect screenshot, URL, locale code, breakpoint width, browser, and zoom level. If any item is missing, request it before proceeding. Quote the offending broken line verbatim from the screenshot so later gates have an unambiguous target. Halt if no screenshot or repro is available — visual bugs cannot be audited from descriptions alone. --- ### Gate 1 – Raw String Forensics Read the translation file containing the offending key. Dump the target string as Unicode codepoints (e.g., `U+C774 U+B3D9 U+D558 U+C138 U+C694`) and annotate: - Existing NBSP (`U+00A0`), zero-width joiner (`U+200D`), zero-width space (`U+200B`), or soft hyphen (`U+00AD`) — any of these may already be influencing the wrap. - Logical phrase boundaries — mark which adjacent words form a semantic unit that should not split (e.g., Korean 어절 / Japanese 文節 / Chinese 词组). - Latin-script substrings — these still wrap by Latin rules even inside a CJK string. Output: a labeled codepoint table and a phrase-boundary map. --- ### Gate 2 – Computed Style Inspection Read the affected component file. Then trace the computed CSS for the broken element. Document the *winning* declaration (with selector and specificity) for each of: - `word-break` - `overflow-wrap` (and legacy `word-wrap`) - `text-wrap` (`wrap` / `nowrap` / `balance` / `pretty`) - `hyphens` - `line-break` Note any `white-space` value other than `normal` — it changes wrap semantics entirely. If the component is wrapped by a UI library (Ant Design `Typography`, MUI `Typography`, Mantine `Text`, Chakra `Text`, …), record the library's injected class and where its rule comes from. --- ### Gate 3 – Override Tracing For each library identified in Gate 2, locate the injected stylesheet (usually via `cssinjs` / `emotion` / `stitches`) and capture the rule that is in effect. Calculate its specificity. Common offenders: - Ant Design `Typography`: injects `.ant-typography { word-break: break-word }` (specificity 0,1,0). - MUI `Typography`: injects `.MuiTypography-root { … }` similarly. - Tailwind preflight: `body` defaults can be overridden by any component class. Identify a *safe* override location and selector. Prefer: - The doubled-class trick: `.ant-typography.ant-typography { … }` (specificity 0,2,0) — beats the library's single-class rule *without* `!important`. - A scoped wrapper class on the page or layout level. Avoid: - `:where(.ant-typography)` — `:where()` collapses specificity to `0,0,0`. The override will *lose* every cascade contest. - `!important` — wins, but pollutes future overrides and is hard to audit. --- ### Gate 4 – Layered Fix Proposal Propose the minimum number of layers needed. Stop at the first layer that fully resolves the symptom; only escalate if the verification matrix in Gate 5 still has failing cells. **Layer 1 — Global cascade.** Add a base rule on `body` (or the nearest app-shell element): ```css body { word-break: keep-all; overflow-wrap: break-word; } /* Always exempt monospace contexts. */ code, pre, kbd, samp { word-break: normal; overflow-wrap: normal; } ``` `word-break: keep-all` tells the browser not to break *between* CJK syllables (which is the default behavior in most engines for historical reasons). `overflow-wrap: break-word` is the safety net for very long Latin tokens that would otherwise overflow. **Layer 2 — Component-library override.** If a UI library injects a stronger rule, neutralize it at the same scope: ```css .ant-typography.ant-typography { word-break: keep-all; overflow-wrap: break-word; } ``` (Adjust the selector for whichever library is in use. Use the doubled-class trick rather than `!important`.) **Layer 3 — Headline-specific balancing.** For hero / headline text where line-length aesthetics matter, add a balanced wrap on the specific element: ```html

``` `text-wrap: balance` (Tailwind utility `text-balance`, or raw CSS) distributes characters evenly across lines and prevents single-word orphans on the last line. Use `text-pretty` only if `balance` is too expensive (it's a hint, not a guarantee). **Layer 4 — Translation-level phrase binding.** When a specific phrase must move as a unit (e.g., a verb + its object), insert NBSP (`\u00a0`) between the words *in the translation file*, not in JSX: ```json { "hero.title2": "더 스마트하게 구축하고, 더\u00a0빠르게\u00a0이동하세요" } ``` Use NBSP only for short, semantically tight phrases (≤4 words). Long NBSP runs prevent any wrapping at all and will overflow on narrow screens. --- ### Gate 5 – Verification Matrix Build an explicit matrix and mark pass/fail for every cell: | Axis | Values | | ----------- | ----------------------------------------------- | | Zoom | 100%, 125%, 150%, 200% (WCAG 1.4.4) | | Breakpoint | sm, md, lg, xl (whatever the project ships) | | Locale | Every CJK locale + at least one Latin control | | Theme | Light, Dark (only if rendering depends on it) | Re-test all cells after every layer added in Gate 4. The fix is only complete when every cell passes. Latin-locale regressions count as failures even if the CJK bug is resolved. --- ### Gate 6 – Approval Gate Present the full report (Sections 1–6 above) plus the proposed patch. State explicitly: "Awaiting approval before applying patch." Do not edit any file until the user confirms. After approval, apply the patch *exactly* as proposed and re-run Gate 5 on the live build. Report the matrix again. --- ## Guardrails - Never use `!important` to win a specificity contest. Use the doubled-class trick (`.foo.foo`) instead. - Never wrap framework overrides in `:where(...)`. It collapses specificity to 0 and the override will silently lose. - Never apply `word-break: keep-all` to `code`, `pre`, `kbd`, or `samp`. Always carve out monospace contexts. - Never edit a translation file without first dumping the existing string as Unicode codepoints. Hidden characters (NBSP, ZWJ, soft hyphens) are common and easy to miss. - Use NBSP only for short, semantically tight phrases (≤4 words). Long NBSP runs prevent wrapping entirely and cause overflow on narrow screens. - Do not add hardcoded `max-width` or `width` to "fix" wrapping. It masks the cascade bug and breaks at other breakpoints. - Do not use `
` for wrap control. It does not survive translation length variance and breaks the moment another locale is added. - Verify the fix in the *actual* component, not in DevTools-only edits. DevTools changes don't capture cssinjs injection order. - Always test at ≥150% browser zoom. WCAG 1.4.4 requires text to remain usable at 200%, and CJK breaks often only appear when zoomed. - Always test every CJK locale the project ships, not just the reported one. The same string can wrap differently across `ko`, `ja`, and `zh`. - Confirm the fix does not regress Latin locales (`en`, `de`, `fr`, `es`, …). German is ~30% longer and is the canonical regression case for headline wrapping. - Do not change directory structure or extract shared utilities as part of this audit. Stay scoped to the bug. - Honor the project's existing styling convention. If the project uses Tailwind utilities, prefer `text-balance` over inline `style={{ textWrap: 'balance' }}`. --- ## Failure Patterns Common bad outputs: - Applying `word-break: keep-all` globally without exempting `code` / `pre` — breaks code blocks across the entire app. - Wrapping the override in `:where(.ant-typography)` — specificity drops to 0, the library rule wins, and the "fix" silently has no effect. - Adding `text-wrap: balance` *without* fixing the underlying cascade — balances the broken text into a *different* broken layout. - Inserting NBSP between every word in a string — text refuses to wrap on narrow screens, causing horizontal overflow. - Editing the JSX but not the *Korean* translation file (or only the English one) — the bug persists in the actual locale that has the problem. - Testing only at 100% zoom — ships a fix that breaks at 150% or 200%. - Using `
` to force a break — works once, breaks the moment copy changes or another locale is added. - Hardcoding a `max-width` to dodge the issue — masks the cascade bug and reintroduces overflow at unrelated breakpoints. - Treating a single-glyph orphan as "by design" without first checking the cascade for a missing rule. - Skipping the codepoint dump in Gate 1 — missing pre-existing NBSP or ZWJ that would have explained the symptom in seconds. --- ## Example 1 (Minimal Context) **Input:** A single Korean `

` in a plain Tailwind project breaks as `이동하세` / `요` at the `md` breakpoint. No UI library is in use. Project has no global `word-break` rule. **Output:** 1. **Symptom Report** — locale `ko`, breakpoint `md` (768px), Chrome 123, zoom 100%, line broken as `이동하세` / `요`. 2. **Diagnosis** — codepoints `U+C774 U+B3D9 U+D558 U+C138 U+C694` (no hidden characters). Computed `word-break: normal` (UA default). Cascade winner: user-agent stylesheet. 3. **Root Cause** — Layer 1 (global cascade). UA default `word-break: normal` allows breaks between any two CJK syllables. 4. **Fix Strategy** — Layer 1 only. No library override or balancing needed. 5. **Proposed Patch** — add to `src/styles/globals.css`: ```css body { word-break: keep-all; overflow-wrap: break-word; } code, pre, kbd, samp { word-break: normal; overflow-wrap: normal; } ``` 6. **Verification Plan** — re-test at 100/125/150/200% × sm/md/lg × `ko` and `en`. 7. **Approval Gate** — Awaiting approval before applying patch. --- ## Example 2 (Realistic Scenario) **Input:** D.Hub2 portal Korean login hero shows three rotating slogans built with Ant Design ``. Slogan 2 reads `"더 스마트하게 구축하고, 더 빠르게 이동하세요"` and breaks as `"이동하세"` / `"요"` at `lg`. Project uses Tailwind 3 + Ant Design 5. Hero element is `src/pages/login/index.tsx`. Translation key `login.hero.title2` lives at `public/locales/ko/common.json:1373`. **Output:** 1. **Symptom Report** — locale `ko`, breakpoint `lg` (1024px), Chrome 123, zoom 100% and 150%, line broken as `이동하세` / `요`. Bug reproduces only in `ko`; `en` renders fine. 2. **Diagnosis** — codepoint dump of the raw string contains no NBSP / ZWJ / soft hyphens. Computed style on the `

` shows `word-break: break-word` winning from `.ant-typography` (specificity 0,1,0), overriding the project's `body { word-break: keep-all }` (specificity 0,0,1). `text-wrap` is `wrap` (default). 3. **Root Cause** — Layers 1 + 2 + 3 + 4 are all required: - Global rule was missing the monospace exemption. - Ant Design's `cssinjs` rule wins over `body`. - Even after the override, `text-wrap: wrap` will orphan `더` on the previous line because the natural break lands one word short. - The phrase `더 빠르게 이동하세요` is semantically a single unit and must move together. 4. **Fix Strategy** — apply all four layers in order; verify after each. 5. **Proposed Patch:** `src/index.css`: ```css /* International text wrapping */ body { word-break: keep-all; overflow-wrap: break-word; } code, pre, kbd, samp { word-break: normal; overflow-wrap: normal; } /* Ant Design override — doubled-class beats single-class without !important */ .ant-typography.ant-typography { word-break: keep-all; overflow-wrap: break-word; } ``` `src/pages/login/index.tsx` (hero ``, three places): ```tsx <Title level={1} className="!mb-3 text-4xl md:text-5xl lg:text-6xl text-balance" style={{ color: 'var(--color-heading)' }} > ``` `public/locales/ko/common.json`: ```diff - "title2": "더 스마트하게 구축하고, 더 빠르게 이동하세요" + "title2": "더 스마트하게 구축하고, 더\u00a0빠르게\u00a0이동하세요" ``` 6. **Verification Plan** — matrix: | Cell | Result | | ----------------------------- | --------- | | 100% / lg / ko / dark | pass | | 125% / lg / ko / dark | pass | | 150% / lg / ko / dark | pass | | 200% / lg / ko / dark | pass | | 100% / md / ko / dark | pass | | 100% / xl / ko / light | pass | | 100% / lg / en / dark (control) | pass | 7. **Approval Gate** — Awaiting approval before applying patch. --- ## Notes **FAST MODE** (only if explicitly requested): - Skip Gate 5's full matrix. Test only the reported zoom and breakpoint, plus 150% as a WCAG sanity check. - Still produce Gates 1–4. Diagnosis without verification is acceptable in fast mode; fixes without diagnosis are not.