Перейти к содержанию

08 · i18n · Native EN strategy + RU↔EN switch

Зачем этот файл

GOLDJAXE — холдинг для глобального B2B/B2C. Native English обязателен для: - investor-portal (US/EU LP audience) - .ai/.com/.org marketing pages - Certificate (LinkedIn share-friendly) - OG-images social sharing - Future GATE / Syndicate enterprise customers

Native ≠ machine translation. Также есть branding immutable (термины не переводятся: [ ACADEMY ], [ VISION ], INITIATION PROTOCOL).

Этот файл — правила переключения, decision tree «переводить или branding», JS-паттерны для динамических элементов (date / numbers / currency).


Branding vs translatable · decision tree

НЕ переводить (branding immutable)

Категория Примеры Почему
Названия продуктов goldjaxe.com / .ai / .org / academy / syndicate / gate Бренд
Wordmarks секций [ ACADEMY ] / [ VISION ] / [ INNOVATIONS ] / [ VAULT ] Часть брендинга, square brackets identity
Course codes INITIATION PROTOCOL / NEUROOPERATOR / AI-ARCHITECT / GOD MODE VIP / INVESTOR PORTAL Названия как в IT industry — Photoshop, Linux Kernel etc.
Tier коды L0 / L1 / L2 / L3 / L4 / L5 Internal classification
Tech idents JWT / cookie / Stripe / SWIFT / DDEX ERN / PWA / API Industry standard terms
Brand caps ▸ CERTIFICATE OF COMPLETION / ▸ SERIAL · VERIFY / ▸ ISSUED TO Premium signature, EN universal
URLs verify.goldjaxe.ai / academy.goldjaxe.ai Технические endpoints
Currency codes USD / EUR / RUB / AED International convention

Переводить (native EN/RU)

Категория RU EN
UI кнопки Купить / Продолжить / Скачать Buy / Continue / Download
Status labels Активен / Заблокировано / В процессе Active / Locked / In progress
Cert text ВЫДАН / Модулей / Архитектура Превосходства ISSUED TO / Modules / Architecture of Excellence
Section headings Все протоколы / Мои протоколы All protocols / My protocols
Form fields Имя / Email / Телефон Name / Email / Phone
Footer copyright © 2026 GOLDJAXE © 2026 GOLDJAXE (символ universal)
Course descriptions Понимание AI и план... Understanding AI and a plan...
Error messages Что-то пошло не так Something went wrong

Edge case · brand caps в RU контексте

▸ CERTIFICATE OF COMPLETION — это branding (premium signature) или translatable?

Решение CEO 2026-05-06: оставлять EN в обеих версиях (это premium международная signature как Made in USA, Premium Edition). Альтернатива — переводить ▸ СВИДЕТЕЛЬСТВО О ЗАВЕРШЕНИИ ломает premium feel.

См. feedback_certificate_canonical.md v4.4.17 update.


data-i18n + theme-lang.js pattern

Архитектура

window.ECO_I18N — глобальный namespace:

// ru.js
window.ECO_I18N = window.ECO_I18N || {};
window.ECO_I18N.ru = {
  "common": { "back": "← Назад", "next": "Дальше →", ... },
  "academy_certificate": { "issued_to": "▸ ВЫДАН", ... },
  "academy_courses": { ... },
  // ... namespace per screen / section
};
// en.js — параллельная structure
window.ECO_I18N.en = {
  "common": { "back": "← Back", "next": "Next →", ... },
  "academy_certificate": { "issued_to": "▸ ISSUED TO", ... },
  "academy_courses": { ... },
};

HTML markup

<!-- Базовый -->
<div data-i18n="academy_certificate.issued_to">▸ ВЫДАН</div>

<!-- Inline span внутри dynamic content -->
<div class="cert-stat-num">4<span data-i18n="academy_certificate.stat_hours_unit">ч</span></div>

<!-- Кнопка с inline-content -->
<button data-i18n="academy_certificate.toolbar_print" onclick="window.print()">🖨 Печать</button>

theme-lang.js (apply mechanism)

theme-lang.js слушает клик [data-lang-btn] → меняет html[data-lang] + html[lang] → MutationObserver применяет text из window.ECO_I18N[lang][key] ко всем [data-i18n] элементам.

Default lang определяется script в <head>:

var l = localStorage.getItem('eco-lang') || 'ru';
document.documentElement.setAttribute('data-lang', l);
document.documentElement.lang = l;

Namespace conventions

  • common.X — кросс-экранные общие keys (sign in / log out / save / cancel)
  • [product]_[screen].X — per-screen keys (academy_certificate / gate_dashboard / syndicate_landing)
  • marketing.X — для landing pages (если кросс-product)

При добавлении нового экрана — создавать новый namespace [product]_[screen], не размывать в common.

Анти-pattern

❌ Hardcoded text без data-i18n атрибута (не переключится)
❌ JS textContent = '...' без проверки lang (override i18n)
❌ Дублирование keys в разных namespaces (single source of truth)
❌ Missing key в en.js (приведёт к видимому RU тексту в EN режиме)


4 паттерна i18n атрибутов (canonical · v4.7.x)

theme-lang.js поддерживает 3 атрибутa + есть bilingual JS-namespace для data в JS-литералах. Применяй правильный паттерн в зависимости от типа контента.

1. data-i18n · text content (90% случаев)

Для статичных текстов в HTML без inline-разметки.

<button data-i18n="academy_quiz.btn_check">[ ПРОВЕРИТЬ → ]</button>
<div class="card-title" data-i18n="academy_settings.card_account_title">Аккаунт</div>

Когда: простой текст, без <strong>, <code>, <em> внутри.

2. data-i18n-html · innerHTML с inline-разметкой

Для текстов с inline-эмфазами (<strong>, <code>, <em>, <a>).

<p data-i18n-html="academy_lesson.content_p2">
  Первый принцип Архитектора — <strong>тот же подход, но к бизнес-процессам</strong>.
</p>

<p data-i18n-html="academy_lesson.content_with_meta">
  Время вашего участия: <code>0 минут</code>. Работает 24/7.
</p>

В словаре пишется как HTML-строка с тегами:

"content_p2": "Первый принцип Архитектора — <strong>тот же подход, но к бизнес-процессам</strong>.",
"content_with_meta": "Время вашего участия: <code>0 минут</code>. Работает 24/7."

Когда: есть inline <strong> или <code> внутри текста. ⚠️ XSS-safe только для self-controlled словарей (наши i18n/*.js).

3. data-i18n-attr · атрибуты (placeholder, title, aria-label)

Для атрибутов формы, тултипов, accessibility.

<input type="text"
  data-i18n-attr="placeholder:academy_settings.field_telegram_placeholder"
  placeholder="@username" />

<button
  data-i18n-attr="aria-label:academy_quiz.modal_close_aria,title:common.close"
  aria-label="Закрыть"
  title="Закрыть">×</button>

Синтаксис: attr1:key1,attr2:key2 (запятая-разделитель пар).

Когда: placeholder, title, aria-label, alt — любой атрибут вне textContent.

4. Bilingual JS-namespace · для data в JS-литералах

Когда в HTML текст вставляется через JS (innerHTML, textContent) и не покрывается data-i18n. Типичные случаи: QUIZ_DATA, MODULES_DATA, PATHS_DATA, dynamic tier-name.

Анти-pattern (не делать):

const QUIZ_DATA = [
  {
    question: "Какой процесс лучше всего подходит...",
    options: ["...", "...", "...", "..."],
    explanation: "Правильный ответ: 3...",
    correct: 2
  },
  ...
];

Canonical (v4.7.3 academy_quiz_data):

// 1. В JS только structure — id + answer index + type
const QUIZ_STRUCTURE = [
  { id: 'q1', correct: 2 },
  { id: 'q2', correct: 0 },
  // ...
];

// 2. Тексты — отдельный namespace в i18n словарях
// ru.js
"academy_quiz_data": {
  "q1_question": "Какой процесс <strong>лучше всего</strong>...",
  "q1_opt0": "Стратегические переговоры с ключевым клиентом",
  "q1_opt1": "...",
  "q1_explanation": "<strong>Правильный ответ: 3.</strong>...",
  // ... q2_*, q3_*, q4_*, q5_*
}
// en.js симметрично

// 3. Lookup helper в HTML JS
function getQuestionData(idx) {
  const s = QUIZ_STRUCTURE[idx];
  const lang = document.documentElement.getAttribute('data-lang') === 'en' ? 'en' : 'ru';
  const dict = window.ECO_I18N[lang].academy_quiz_data;
  return {
    question: dict[s.id + '_question'],
    options: [0,1,2,3].map(i => dict[s.id + '_opt' + i]),
    explanation: dict[s.id + '_explanation'],
    correct: s.correct
  };
}

4 канонических bilingual namespace в академии (v4.7.5):

Namespace Источник Ключи × 2 = JS-литералов Релиз
onboarding_paths 4 пути × 9 полей (area / module / artifact / 3 steps × {title+desc}) 36 × 2 = 72 v4.7.0
academy_completed_modules 6 модулей × 3 поля (title / nextTitle / nextDesc) 18 × 2 = 36 v4.7.0
academy_quiz_data 5 вопросов × 6 полей (question / 4 options / explanation) 30 × 2 = 60 v4.7.3
academy_lesson_modules 6 модулей × 4-5 lesson titles 30 × 2 = 60 v4.7.4

Сумма: 114 ключей × 2 = 228 переведённых JS-литералов через bilingual namespace pattern.

Когда применять: если в JS массиве/объекте есть RU-only тексты которые рендерятся в DOM. Если массив < 5 текстов — можно через data-i18n и static HTML, если ≥ 5 — bilingual namespace.


Reactive lang switching · MutationObserver pattern

theme-lang.js applyLang() перерисовывает все [data-i18n] / [data-i18n-html] / [data-i18n-attr] элементы автоматически. Но JS-rendered контент (tier-chip, форматированные даты, прогресс «8 / 29 lessons», динамические tx-rows) — нет. Для них применяй MutationObserver pattern.

Canonical скелет

(function() {
  function getLang() {
    const l = document.documentElement.getAttribute('data-lang')
           || document.documentElement.lang
           || 'ru';
    return l === 'en' ? 'en' : 'ru';
  }
  function getDict(ns) {
    const lang = getLang();
    return (window.ECO_I18N && window.ECO_I18N[lang] && window.ECO_I18N[lang][ns]) || {};
  }

  function applyLangAware() {
    const lang = getLang();
    const d = getDict('academy_billing');

    // Tier chip
    document.getElementById('user-tier-chip').textContent =
      userTier + ' · ' + (lang === 'en' ? 'ACTIVE' : 'АКТИВЕН');

    // Date с locale
    const locale = lang === 'en' ? 'en-GB' : 'ru-RU';
    document.getElementById('tier-from').textContent =
      tierFromDate.toLocaleDateString(locale, { day: 'numeric', month: 'long', year: 'numeric' });

    // Re-render dynamic rows из i18n словаря
    rerenderTransactions(lang, d);
  }

  // Initial render
  applyLangAware();

  // Reactive · re-apply on lang switch без перезагрузки страницы
  new MutationObserver(applyLangAware).observe(document.documentElement, {
    attributes: true,
    attributeFilter: ['lang', 'data-lang']
  });
})();

Применено в академии (v4.7.0–v4.7.4): dashboard, lesson-completed, profile, login, auth-magic-link, billing, lesson-quiz, lesson-viewer, settings.

Когда нужен MutationObserver

  • Tier chip L0 · АКТИВЕН ↔ L0 · ACTIVE
  • Даты с locale (15 апреля 2026 ↔ 15 April 2026)
  • Тарифные суффиксы (★ ФЛАГМАН ↔ ★ FLAGSHIP) через replace
  • Пагинация / прогресс с pluralization
  • Tx-rows / lesson-rows / комментарии rendered через innerHTML
  • Quiz options re-render при текущем вопросе (если bilingual namespace)
  • Confirm/alert тексты в onclick handlers (lookup в момент клика)

Когда НЕ нужен

  • Статичный текст в HTML с data-i18n — theme-lang.js делает это сам
  • Кнопки и labels с data-i18n атрибутами
  • Атрибуты с data-i18n-attr

In-form language switchers · window.ecoLang.apply()

Если в форме (как в settings.html секция «Язык интерфейса») есть собственный switcher [data-page-lang="ru/en"] помимо header [data-lang-btn] — он должен дёргать window.ecoLang.apply(l), а не ставить attr вручную.

❌ Анти-pattern (v4.7.1 баг · CEO-feedback)

// Не работает реактивно — body не перерисуется до reload
document.querySelectorAll('[data-page-lang]').forEach(btn => {
  btn.addEventListener('click', () => {
    const l = btn.getAttribute('data-page-lang');
    document.documentElement.setAttribute('data-lang', l);  // <-- attr меняется
    localStorage.setItem('eco-lang', l);
    // Header switcher активность синхронизирована, но
    // theme-lang.js applyLang() НЕ ВЫЗВАН → body остаётся на старом языке
  });
});

Почему ломалось: theme-lang.js слушает только [data-lang-btn] clicks (binds в init()), не attribute-mutations. Мой ручной setAttribute('data-lang', l) атрибут устанавливал, но re-render data-i18n элементов не запускался.

✅ Canonical (v4.7.2 fix)

document.querySelectorAll('[data-page-lang]').forEach(btn => {
  btn.addEventListener('click', () => {
    const l = btn.getAttribute('data-page-lang');
    if (window.ecoLang && window.ecoLang.apply) {
      window.ecoLang.apply(l);  // ✓ atomic: attr + localStorage + applyLang() + header sync
    } else {
      // Fallback если theme-lang.js не загрузился
      document.documentElement.setAttribute('data-lang', l);
      try { localStorage.setItem('eco-lang', l); } catch(e) {}
    }
    // Active state на самом form-switcher (header sync делает theme-lang.js)
    document.querySelectorAll('[data-page-lang]').forEach(b =>
      b.classList.toggle('eco-lang-btn-active', b.getAttribute('data-page-lang') === l)
    );
  });
});

window.ecoLang.apply() — public API экспортированный из theme-lang.js (строка 141 модуля). Atomic операция: устанавливает attr + localStorage + перерисовывает все [data-i18n] + синхронизирует active state header switcher.


Lang-aware JS-rendered content без if(user) блока

Legacy паттерн: chip / date / tier-name обновлялись внутри if (user) { ... }. Если user === null (demo-режим без логина) — chip/date оставались на дефолтном RU при EN-переключении.

❌ Анти-pattern (v4.7.1 legacy)

const user = window.EcoState.getUser();
if (user) {
  // Только если залогинен — chip обновляется lang-aware
  document.getElementById('user-tier-chip').textContent =
    user.tier + ' · ' + (document.documentElement.lang === 'en' ? 'ACTIVE' : 'АКТИВЕН');
  document.getElementById('field-name').value = user.name;
  // ...
}
// Без user chip остаётся "L0 · АКТИВЕН" hardcoded в HTML, не реагирует на lang

✅ Canonical (v4.7.2 fix)

const user = window.EcoState && window.EcoState.getUser();
const userTier = user && user.tier ? user.tier : 'L0';  // ← дефолт L0

// User-зависимое — отдельный if (user) блок
if (user) {
  document.getElementById('user-initial').textContent = (user.name || 'С').charAt(0).toUpperCase();
  document.getElementById('field-name').value = user.name || '';
  document.getElementById('field-email').value = user.email || '';
}

// Lang-aware — ВНЕ if (user), работает в demo тоже
function applyTierChip() {
  const lang = document.documentElement.getAttribute('data-lang') === 'en' ? 'en' : 'ru';
  document.getElementById('user-tier-chip').textContent =
    userTier + ' · ' + (lang === 'en' ? 'ACTIVE' : 'АКТИВЕН');
}
applyTierChip();
new MutationObserver(applyTierChip).observe(document.documentElement, {
  attributes: true,
  attributeFilter: ['lang', 'data-lang']
});

Правило: lang-aware UI работает всегда (с user и без). Внутри if (user) — только user-specific data (имя, email, mock-history).


Validation · symmetry + JSON parse

Перед commit/push — обязательная статическая валидация.

Скрипт-валидатор (Python)

import re, json
for f in ['ui-kit/i18n/ru.js', 'ui-kit/i18n/en.js']:
    s = open(f).read()
    m = re.search(r'window\.ECO_I18N\.\w+ = (\{.*\});', s, re.DOTALL)
    d = json.loads(m.group(1))  # parse валидность
    nsk = sum(len(v) if isinstance(v, dict) else 1 for v in d.values())
    print(f'{f}: ✓ {len(d)} namespaces · {nsk} keys')

# Symmetry RU vs EN
ru = json.loads(re.search(r'window\.ECO_I18N\.\w+ = (\{.*\});', open('ui-kit/i18n/ru.js').read(), re.DOTALL).group(1))
en = json.loads(re.search(r'window\.ECO_I18N\.\w+ = (\{.*\});', open('ui-kit/i18n/en.js').read(), re.DOTALL).group(1))
for ns in ['academy_quiz', 'academy_quiz_data', 'academy_lesson_modules']:
    rk = set(ru.get(ns, {}).keys())
    ek = set(en.get(ns, {}).keys())
    diff = (rk - ek) | (ek - rk)
    print(f'{ns}: ru={len(rk)} en={len(ek)} ' + ('✓ symmetric' if not diff else f'⚠ diff={diff}'))

Грамотная проверка покрытия

# data-i18n count в HTML
grep -c 'data-i18n' ecosystem-prototype/academy/lesson-quiz.html

# ?v= unique versions (должна быть одна свежая)
grep -oE '\?v=[0-9.]+' ecosystem-prototype/academy/lesson-quiz.html | sort -u

# RU lines outside i18n (sanity check after localization)
python3 -c "
import re
with open('lesson-quiz.html') as fh: s = fh.read()
clean = re.sub(r'<style[\s\S]*?</style>', '', s)
clean = re.sub(r'<script[\s\S]*?</script>', '', clean)
clean = re.sub(r'<[^>]+>', ' ', clean)
ru = [l.strip() for l in clean.split('\n') if re.search(r'[А-Яа-я]{3,}', l)]
print(f'{len(ru)} RU lines remain (should be ≤ data-i18n count)')
"

Метрика: data-i18n countRU lines count — иначе непокрытый RU выпрыгнет в EN режиме.


Verification · Chrome MCP testing pipeline

После HTML/CSS/JS правок — обязательная live-проверка через Chrome MCP (или ручной reload).

Скелет batch-проверки

// 1. Navigate с cache-bust
{ name: "navigate", input: { url: "http://localhost:8088/ecosystem-prototype/academy/lesson-quiz.html?_nocache=473", tabId } }
// 2. Wait для DOM ready
{ name: "computer", input: { action: "wait", duration: 2, tabId } }
// 3. Click EN button + JS-eval ключевых элементов
{ name: "javascript_tool", input: { action: "javascript_exec", text: "document.querySelector('[data-lang-btn=\"en\"]').click(); JSON.stringify({lang: document.documentElement.getAttribute('data-lang'), h1: document.querySelector('[data-i18n=\"academy_quiz.h1\"]')?.textContent, ...})", tabId }}
// 4. Screenshot для visual confirm
{ name: "computer", input: { action: "screenshot", tabId } }

Chrome MCP в browser_batch — atomic-операция

Используй mcp__Claude_in_Chrome__browser_batch для serialization (navigate → wait → click → screenshot одной транзакцией).

Что проверять

  • [ ] Все data-i18n элементы переключились (через Object.keys(window.ECO_I18N.en[ns]))
  • [ ] Bilingual JS-namespace загружен (Object.keys(window.ECO_I18N.en.academy_quiz_data).length > 0)
  • [ ] data-i18n-attr placeholder/title переключились
  • [ ] data-i18n-html сохранил inline <strong> теги
  • [ ] MutationObserver pattern работает (chip/date/tier-name обновляются)
  • [ ] In-form lang switcher (если есть) работает реактивно
  • [ ] No console errors

Cache invalidation · gotchas

?v= query bump — обязателен при правке словарей

При правке i18n/ru.js или i18n/en.jsобязательно поднимаешь ?v= на всех HTML-экранах которые их используют. Иначе браузер берёт старый закэшированный словарь и видит data-i18n ключи без перевода (фаллбек на default text).

# Bump на всех затронутых HTML
sed -i '' 's/?v=4.7.3.1/?v=4.7.4.1/g' ecosystem-prototype/academy/lesson-viewer.html

Chrome агрессивно кэширует HTML

Cache-Control: no-store в server response не всегда обходится Chrome. Симптом: после Edit + reload браузер показывает старый HTML.

Решения (по приоритету): 1. Force-reload Ctrl+Shift+R / Cmd+Shift+R (или location.reload(true) в JS) 2. URL parameter ?_nocache=Nсамый надёжный для testing (Chrome видит уникальный URL → fetch заново) 3. DevTools → Network → Disable cache (для стабильной работы во время разработки)

В Chrome MCP testing pipeline всегда добавляй ?_nocache=N к URL.

Stale mirror server gotcha

Серверы (Python http.server, nginx) могут раздавать mirror snapshot вместо актуального working tree. Симптом: curl показывает старый файл, хотя на диске новый.

# Проверка cwd процесса
lsof -p $(ps aux | grep 'serve.py' | grep -v grep | awk '{print $2}' | head -1) | grep cwd
# Должен указывать на правильный design.goldjaxe.ai/ , не на /private/tmp/goldjaxe-mirror/

Прецедент v4.7.1: сервер на 8088 имел cwd /private/tmp/goldjaxe-mirror/ (snapshot 2026-05-04). Я давал CEO ссылки на 8088 — он видел stale файлы и считал что мои правки не применились. Fix: перезапуск с явным cd ROOT && python3 serve.py 8088.


Content body rule · CEO-feedback v4.7.4

«Это API placeholder» / «это data, не UI» — НЕ оправдание оставлять RU при EN. Любой видимый текст на UI должен переключаться.

Прецедент v4.7.4

Первая версия lesson-viewer.html локализовала UI (sidebar / breadcrumb / tabs / right-sidebar / 24 lesson rows) но оставила тело урока (h2/p/blockquote/ol — «Принцип 1: Процессы работают сами») в RU с отговоркой «в production читается из Academy LMS API».

CEO открыл EN и сказал: «здесь по центру на русском даже если англ раскладка».

Правило: demo content тоже локализуется. Production когда контент придёт из API — заменит i18n keys. Но в prototype/demo — i18n keys обязательны для всего видимого тела.

Решение: добавить content_* ключи (intro / h2_X / p1-pN / blockquote / h3_without / h3_with / lists / outro) в namespace экрана + расставить data-i18n / data-i18n-html на каждом параграфе.


Native EN качество · 7 правил из academy v4.7.x

Уроки из 5 релизов локализации академии (v4.7.0 → v4.7.5):

  1. Read aloud test — каждое EN-предложение прочитать вслух. Звучит как machine translation? Переписать.
  2. Idiomatic > literal — «Architecture of Dominance» (canonical CEO-approved) > «Architecture of Excellence» (machine). «What's the cost of delay?» > «What does it cost not to do this today?» «Spot the weak areas and lock the principles in» > «Open weak places and reinforce principles».
  3. Tone parity — RU sharp/punchy → EN sharp/punchy. «PICK THIS» вместо «выбирают». «Pick a tier» вместо «Choose a tariff».
  4. Cultural anchors — «$50k+ contract» вместо «$50k+ договор» (USD-style). «WhatsApp every day» (natural English phrasing). «1×100 leverage» (preserves brand metaphor).
  5. Verb form sustainability — «Processes work on their own» (verb sustains across uses) vs «Processes are working autonomously» (passive heavy).
  6. Punchy prepositions — «Assets over services» > «Assets vs services» > «Assets instead of services». Один char делает копию sharp.
  7. Brand context preservation — «Take a Kommo trial (14 days)» сохраняет product name. «GOLDJAXE [ ACADEMY ]» square brackets — branding immutable во всех языках.

13/13 academy completion (v4.7.5 final · 2026-05-07)

# Экран Релиз data-i18n Bilingual JS
1 onboarding.html v4.7.0 50 onboarding_paths (36)
2 dashboard.html v4.7.0 47
3 courses.html v4.7.0 48
4 lesson-completed.html v4.7.0 35 academy_completed_modules (18)
5 profile.html v4.7.0 53
6 login.html v4.7.0 14
7 auth-magic-link.html v4.7.0 22
8 settings.html v4.7.1 + v4.7.2 80
9 billing.html v4.7.1 44
10 lesson-quiz.html v4.7.3 63 academy_quiz_data (30)
11 lesson-viewer.html v4.7.4 112 academy_lesson_modules (30)
12 certificate.html v4.7.5 (bump) 21
13 investor-portal.html v4.7.5 (bump) 20

Cumulative: - ~609 data-i18n атрибутов в HTML × 13 экранов - 4 bilingual JS namespace · 114 ключей × 2 = 228 переведённых JS-литералов - ~1,330 ключей в i18n/ru.js + i18n/en.js (симметрия RU/EN ✓) - 5 релизов · 7 commits · ~3,700 строк изменений


7 закрытых прецедентов v4.7.x (для синдиката / gate-admin / .ai/landing)

  1. page-lang switcher reactive (v4.7.2) — in-form switcher должен вызывать window.ecoLang.apply(l), не ручную установку attr. theme-lang.js слушает только [data-lang-btn] clicks.
  2. applyLangAware из IIFE (v4.7.2) — chip/date/JS-литералы должны быть lang-aware независимо от if (user). Demo-режим не должен видеть RU при EN.
  3. MutationObserver на html[lang] (v4.7.0+) — canonical для re-render JS-rendered контента без перезагрузки страницы. 7+ экранов академии используют.
  4. Bilingual JS-namespace для data-в-литералах (v4.7.0+) — QUIZ_DATAQUIZ_STRUCTURE + getQuestionData() lookup. 4 namespace в академии (228 JS-литералов).
  5. Cache invalidation pitfall (v4.7.1+) — Chrome агрессивно кэширует HTML, Cache-Control: no-store не всегда помогает. В testing использовать ?_nocache=N.
  6. Локализация content body (v4.7.4) — «это API placeholder» НЕ оправдание для RU при EN. Demo content тоже локализуется.
  7. Stale mirror server gotcha (v4.7.1) — сервер может раздавать snapshot вместо working tree. Перед debug lsof -p PID | grep cwd для верификации.

Re-entry для Claude (новая сессия · syndicate / gate-admin / .ai)

«прочти контекст academy-i18n-v4.7» → этот файл (08-i18n.md) + memory feedback_academy_i18n_canonical.md + ROOT_LOG entry от 2026-05-07.

Применить к новому экрану: 1. Прочитать существующий HTML — посчитать grep -c 'data-i18n' (текущее покрытие) 2. Найти namespace в i18n/ru.js (grep -n '"<product>_<screen>"' ui-kit/i18n/ru.js) — расширить или создать 3. Найти JS-литералы с RU-only текстами — вынести в bilingual namespace 4. Расставить data-i18n / data-i18n-html / data-i18n-attr атрибуты в HTML body (без trogania immutable: branding caps, tier-codes, JWT/cookie/n8n) 5. Применить MutationObserver pattern для chip/date/dynamic 6. Если есть in-form lang switcher → window.ecoLang.apply(l) pattern 7. Bump ?v= query на затронутом HTML 8. Validate JSON + symmetry RU↔EN 9. Live test через Chrome MCP (RU↔EN navigation + JS-eval) 10. Native EN review (read aloud test, 7 правил выше) 11. Commit + push + CEO ревью


Реактивные элементы (date / numbers / currency)

Что НЕ покрывается i18n keys (потому что динамическое): - Даты: 06 МАЯ 2026 / 06 MAY 2026 - Числа с unit: 4ч / 4h - Currency conversions: 149₽ / $149 - Pluralization: 1 курс / 2 курса / 5 курсов - Percentages с context: 28% завершено / 28% completed

Pattern: MutationObserver на html[lang]

function updateCertDate() {
  const isEn = document.documentElement.lang === 'en';
  const months = isEn
    ? ['JANUARY','FEBRUARY','MARCH','APRIL','MAY','JUNE','JULY','AUGUST','SEPTEMBER','OCTOBER','NOVEMBER','DECEMBER']
    : ['ЯНВАРЯ','ФЕВРАЛЯ','МАРТА','АПРЕЛЯ','МАЯ','ИЮНЯ','ИЮЛЯ','АВГУСТА','СЕНТЯБРЯ','ОКТЯБРЯ','НОЯБРЯ','ДЕКАБРЯ'];
  const el = document.getElementById('cert-date');
  if (el) {
    const now = new Date();
    el.textContent = now.getDate().toString().padStart(2,'0') + ' ' + months[now.getMonth()] + ' ' + now.getFullYear();
  }
}

updateCertDate();  // initial
new MutationObserver(updateCertDate).observe(
  document.documentElement,
  { attributes: true, attributeFilter: ['lang','data-lang'] }
);

Применяется на v4.4.17 cert.html. Reactive ко всем lang switches.

Pattern: inner span для unit

Для 4ч / 4h — number static, unit translatable через i18n key:

<div class="cert-stat-num">4<span data-i18n="academy_certificate.stat_hours_unit">ч</span></div>

Pattern: pluralization (RU complex)

RU имеет 3 формы plural (1 / 2-4 / 5+), EN имеет 2 (1 / many).

function pluralRu(n, one, few, many) {
  const mod10 = n % 10;
  const mod100 = n % 100;
  if (mod10 === 1 && mod100 !== 11) return one;
  if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return few;
  return many;
}

// Usage
const courseLabel = isEn 
  ? (n === 1 ? 'course' : 'courses')
  : pluralRu(n, 'курс', 'курса', 'курсов');

Pattern: currency conversion

При показе цен: - RU primary: 149₽ (rouble symbol after) - EN primary: $149 (dollar before) - Можно показать оба 149₽ ≈ $1.50 для transparency

function formatPrice(rub, lang) {
  if (lang === 'en') return `$${(rub / 95).toFixed(2)}`;
  return `${rub.toLocaleString('ru-RU')} ₽`;
}

Date / number / currency formats

Полная таблица locale-specific formatting:

Тип RU EN
Date 06 МАЯ 2026 (DD MONTH YYYY) 06 MAY 2026 (DD MONTH YYYY · UK style)
Date short 06.05.2026 05/06/2026 (US) или 06/05/2026 (UK)
Time 14:30 2:30 PM (US) или 14:30 (UK 24h)
Currency RUB 14 900 ₽ (space thousands, символ после) 14,900 ₽ (если EN фраза с RUB)
Currency USD $149 $149
Currency EUR €149 €149
Phone +7 (XXX) XXX-XX-XX +7 (XXX) XXX-XX-XX (international format same)
Percentage 28% 28%
Time duration ~6-8 ч ~6-8h
Decimal 0,5 (запятая) 0.5 (точка)
Big numbers 1 234 567 (space separator) 1,234,567 (comma)
Date range с 06.05 по 12.05 May 6 — May 12

Decision

GOLDJAXE использует UK style для EN (DD MONTH YYYY · 24h time) — universal для US/EU/UK audience. US format May 6, 2026 НЕ используется (избегает confusion с EU readers).


Hardcoded EN audit (Призма 4 план)

Project: project_native_en_localization.md. Запускать после v4.4.13+ финал (~2026-05-15+).

4 этапа

Этап 1 · 5 критических экранов

Native copywriter 18+ лет (Upwork ~$300): - landing.ai (главная .ai) - landing.com (.com VISION) - landing.org (.org VAULT) - syndicate/landing (forum entry) - investor-portal (LP-facing)

Этап 2 · i18n/en.js audit (Upwork ~$200)

Coverage scan vs ru.js: - Missing keys (RU есть, EN нет) - Outdated keys (расходятся по смыслу) - Native polish existing translations

Скрипт для audit:

const ruKeys = Object.keys(window.ECO_I18N.ru).flatMap(ns => 
  Object.keys(window.ECO_I18N.ru[ns]).map(k => `${ns}.${k}`)
);
const enKeys = Object.keys(window.ECO_I18N.en).flatMap(ns => 
  Object.keys(window.ECO_I18N.en[ns]).map(k => `${ns}.${k}`)
);
const missing = ruKeys.filter(k => !enKeys.includes(k));
console.log('Missing in EN:', missing);

Этап 3 · Hardcoded EN в HTML (Upwork ~$300)

Поиск text без data-i18n:

grep -rn '>\(.*\)<' ecosystem-prototype/ | grep -v 'data-i18n'

Wrap каждый в data-i18n + добавить key в обе language files.

Этап 4 · Marketing copy native polish (Upwork ~$700)

856 строк marketing text → native copywriter 18+ лет polish: - Headlines / subheadlines - Feature descriptions - Pricing copy - CTAs - Social proof testimonials

Бюджет общий: ~$1.5K через Upwork.

Альтернатива · Claude polish

CEO ранее решил: полировка EN перевода через Claude (не подрядчик), потому что Claude знает контекст GOLDJAXE.

См. project_native_en_localization.md для финального решения.


investor-portal special (EN-first + RU UI overlay)

Investor portal targets US/EU LP. Особый case:

Поведение

  • HTML lang="en" default (EN-first)
  • Контент EN canonical (хедлайны / описания / CTAs)
  • RU UI overlay = инструменты управления (lang switcher / theme toggle)
  • При switch на RU — переводится только UI, контент остаётся EN

Markup pattern

<html lang="en" data-lang="en">
<!-- ... -->
<a href="#reports" class="font-mono uppercase text-[12.5px] text-white opacity-70" 
   data-i18n="investor.nav_reports">Reports</a>

Если data-lang="ru" через theme-lang.js → текст переключается на «Отчёты». Но дефолт en.

Why

Investor decision-makers — глобальные LP / family offices / institutional. Они ожидают EN-first. RU overlay — для удобства Сергея personally при работе с интерфейсом.

См. feedback_brand_endorsement_strategy.md + feedback_holding_positioning_vs_infobiz.md.


i18n quality bar

Native EN check list (per text)

  • [ ] Не machine translation (естественность, не дословный перевод)
  • [ ] Idiomatic (используем native idioms)
  • [ ] Tone matches RU (если RU formal — EN formal; если RU casual — EN casual)
  • [ ] Punctuation согласно EN style (не RU кавычки «» в EN)
  • [ ] Capitalization согласно EN convention (Title Case в headlines, sentence case в body)
  • [ ] Spacing single (не double space после punctuation)
  • [ ] Hyphens vs em-dashes correct (em-dash для prosaic breaks )

RU quality check list

  • [ ] Без англицизмов (см. 06-anti-patterns.md группа D)
  • [ ] Падежи правильные (особенно при interpolation)
  • [ ] Pluralization correct (3 формы)
  • [ ] Не «гугло-русский» (естественные обороты)
  • [ ] Capitalization Sentence case (НЕ Title Case как EN)

Когда читать

  • Перед добавлением переключаемого текста — обязательно
  • При создании нового экрана с public-facing контентом — да
  • Перед Призмой 4 (EN перевод audit) — обязательно
  • При работе с investor-portal / marketing landings — обязательно
  • При создании multi-currency pricing UI — да (currency formats секция)