i18n stress test
90% макетов сделаны под английский. Локализаторы потом плачут. Этот скилл — превентивная проверка.
Стресс-наборы
DE (длинные слова)
- «Кнопка» →
Bestätigungseinstellungen
- «Настройки» →
Anwendungseinstellungen
- «Загрузить» →
Herunterladen
AR (RTL + другая ширина)
إعدادات الإشعارات
- Тестирует: направление текста, иконки рядом с текстом, выравнивание чисел.
JA (плотный текст без пробелов)
通知設定をカスタマイズする方法
- Тестирует: word-break, длина строк.
ZH (вертикальная плотность)
Emoji-переполнение
Скрипт
templates/i18n-stress.mjs:
import { chromium } from 'playwright';
import { pathToFileURL } from 'node:url';
import path from 'node:path';
import fs from 'node:fs/promises';
const file = process.argv[2];
if (!file) { console.error('Usage: node i18n-stress.mjs <file>'); process.exit(1); }
const samples = {
de: ['Anwendungseinstellungen', 'Benachrichtigungseinstellungen', 'Bestätigungs-E-Mail'],
ar: ['إعدادات الإشعارات', 'احفظ التغييرات', 'تسجيل الدخول'],
ja: ['通知設定をカスタマイズ', '保存', 'サインイン'],
emoji: ['🌟⭐✨💫🌠☄️', '🎉🎊🥳', '👨👩👧👦'],
};
const browser = await chromium.launch();
for (const [lang, words] of Object.entries(samples)) {
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
await page.goto(pathToFileURL(path.resolve(file)).href);
// RTL для арабского
if (lang === 'ar') await page.evaluate(() => document.documentElement.setAttribute('dir', 'rtl'));
// Заменим текстовые узлы
await page.evaluate((words) => {
function walk(node) {
if (node.nodeType === 3 && node.textContent.trim().length > 3) {
node.textContent = words[Math.floor(Math.random() * words.length)];
}
for (const c of node.childNodes) walk(c);
}
walk(document.body);
}, words);
// Найдём переполнения
const overflows = await page.evaluate(() => {
const out = [];
document.querySelectorAll('*').forEach(el => {
if (el.scrollWidth > el.clientWidth + 1 && el.clientWidth > 0) {
out.push({ tag: el.tagName, text: el.textContent.slice(0, 40), w: el.scrollWidth, cw: el.clientWidth });
}
});
return out.slice(0, 20);
});
await page.screenshot({ path: `i18n-${lang}.png` });
await page.close();
console.log(`\n[${lang.toUpperCase()}] screenshot → i18n-${lang}.png`);
if (overflows.length) {
console.log(' Переполнения:');
for (const o of overflows) console.log(` ${o.tag} (${o.cw}px < ${o.w}px): "${o.text}"`);
} else {
console.log(' ✓ нет переполнений');
}
}
await browser.close();
Использование
node i18n-stress.mjs index.html
# → i18n-de.png, i18n-ar.png, i18n-ja.png, i18n-emoji.png
# + список элементов, у которых текст не помещается
Что нужно сделать в макете заранее
- Не используй
width: <px> для контейнеров с текстом. Используй min-width + max-width.
- Иконка рядом с текстом — через flex с gap, не через абсолют.
- Числа в сложных форматах — через
Intl.NumberFormat, не вручную.
- Дата —
Intl.DateTimeFormat.
- Множественные формы — через
Intl.PluralRules, не «1 файл / 2 файлов».
- Поддержка RTL —
margin-inline-start вместо margin-left, inset-inline-end вместо right. CSS logical properties.
- Шрифт с поддержкой нужных алфавитов —
font-family: Inter, "Noto Sans Arabic", "Noto Sans JP", sans-serif.
Что точно сломается
- Кнопки фиксированной ширины.
- Tabs с фиксированными % ширины.
- Сетки
grid-template-columns: 1fr 200px где справа — текст.
- Иконки с
position: absolute; right: 16px — в RTL должны быть слева.
- Хедеры с лого, навом и кнопкой в одну строку — на DE сжимается.
Legacy reference
Прежняя расширенная версия скилла (дерево @2026-04-30) сохранена целиком в references/legacy-i18n-stress-test.md. Секции там: 4 категории риска, Test fixtures, Stress-test mode, Чек-лист, Решения проблем, Антипаттерны.
1---2name: i18n-stress-test3description: Стресс-тест локализации перед сдачей макета: немецкий, арабский RTL, японский, emoji-переполнение. Триггеры: «длинные слова», «RTL тест».4---56# i18n stress test7890% макетов сделаны под английский. Локализаторы потом плачут. Этот скилл — превентивная проверка.910## Стресс-наборы1112### DE (длинные слова)13- «Кнопка» → `Bestätigungseinstellungen`14- «Настройки» → `Anwendungseinstellungen`15- «Загрузить» → `Herunterladen`1617### AR (RTL + другая ширина)18- `إعدادات الإشعارات`19- Тестирует: направление текста, иконки рядом с текстом, выравнивание чисел.2021### JA (плотный текст без пробелов)22- `通知設定をカスタマイズする方法`23- Тестирует: word-break, длина строк.2425### ZH (вертикальная плотность)26- `通知设置自定义方法`2728### Emoji-переполнение29- `🌟⭐✨💫🌠☄️🌌🌃🌁🌉🌆🏙️`3031## Скрипт3233`templates/i18n-stress.mjs`:3435```js36import { chromium } from 'playwright';37import { pathToFileURL } from 'node:url';38import path from 'node:path';39import fs from 'node:fs/promises';4041const file = process.argv[2];42if (!file) { console.error('Usage: node i18n-stress.mjs <file>'); process.exit(1); }4344const samples = {45 de: ['Anwendungseinstellungen', 'Benachrichtigungseinstellungen', 'Bestätigungs-E-Mail'],46 ar: ['إعدادات الإشعارات', 'احفظ التغييرات', 'تسجيل الدخول'],47 ja: ['通知設定をカスタマイズ', '保存', 'サインイン'],48 emoji: ['🌟⭐✨💫🌠☄️', '🎉🎊🥳', '👨👩👧👦'],49};5051const browser = await chromium.launch();52for (const [lang, words] of Object.entries(samples)) {53 const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });54 await page.goto(pathToFileURL(path.resolve(file)).href);5556 // RTL для арабского57 if (lang === 'ar') await page.evaluate(() => document.documentElement.setAttribute('dir', 'rtl'));5859 // Заменим текстовые узлы60 await page.evaluate((words) => {61 function walk(node) {62 if (node.nodeType === 3 && node.textContent.trim().length > 3) {63 node.textContent = words[Math.floor(Math.random() * words.length)];64 }65 for (const c of node.childNodes) walk(c);66 }67 walk(document.body);68 }, words);6970 // Найдём переполнения71 const overflows = await page.evaluate(() => {72 const out = [];73 document.querySelectorAll('*').forEach(el => {74 if (el.scrollWidth > el.clientWidth + 1 && el.clientWidth > 0) {75 out.push({ tag: el.tagName, text: el.textContent.slice(0, 40), w: el.scrollWidth, cw: el.clientWidth });76 }77 });78 return out.slice(0, 20);79 });8081 await page.screenshot({ path: `i18n-${lang}.png` });82 await page.close();8384 console.log(`\n[${lang.toUpperCase()}] screenshot → i18n-${lang}.png`);85 if (overflows.length) {86 console.log(' Переполнения:');87 for (const o of overflows) console.log(` ${o.tag} (${o.cw}px < ${o.w}px): "${o.text}"`);88 } else {89 console.log(' ✓ нет переполнений');90 }91}92await browser.close();93```9495## Использование9697```bash98node i18n-stress.mjs index.html99# → i18n-de.png, i18n-ar.png, i18n-ja.png, i18n-emoji.png100# + список элементов, у которых текст не помещается101```102103## Что нужно сделать в макете заранее1041051. **Не используй `width: <px>` для контейнеров с текстом.** Используй `min-width` + `max-width`.1062. **Иконка рядом с текстом** — через flex с gap, не через абсолют.1073. **Числа в сложных форматах** — через `Intl.NumberFormat`, не вручную.1084. **Дата** — `Intl.DateTimeFormat`.1095. **Множественные формы** — через `Intl.PluralRules`, не «1 файл / 2 файлов».1106. **Поддержка RTL** — `margin-inline-start` вместо `margin-left`, `inset-inline-end` вместо `right`. CSS logical properties.1117. **Шрифт с поддержкой нужных алфавитов** — `font-family: Inter, "Noto Sans Arabic", "Noto Sans JP", sans-serif`.112113## Что точно сломается114115- Кнопки фиксированной ширины.116- Tabs с фиксированными % ширины.117- Сетки `grid-template-columns: 1fr 200px` где справа — текст.118- Иконки с `position: absolute; right: 16px` — в RTL должны быть слева.119- Хедеры с лого, навом и кнопкой в одну строку — на DE сжимается.120121## Legacy reference122123Прежняя расширенная версия скилла (дерево @2026-04-30) сохранена целиком в `references/legacy-i18n-stress-test.md`. Секции там: 4 категории риска, Test fixtures, Stress-test mode, Чек-лист, Решения проблем, Антипаттерны.