Архитектура интернационализации для приложений, созданных в чате: определяйте стабильные ключи строк, правила плюрализации и единый рабочий процесс переводов для веба и мобильных.

Первым ломается не код, а слова.
Приложения, создаваемые через чат, часто начинаются как быстрый прототип: вы пишете «Добавь кнопку с надписью Сохранить», интерфейс появляется, и вы идёте дальше. Через недели вы хотите добавить испанский и немецкий, и обнаруживаете, что эти «временные» подписи раскиданы по экранам, компонентам, письмам и сообщениям об ошибках.
Изменения текста происходят чаще, чем изменения кода. Названия продуктов переименовывают, юридические формулировки меняются, онбординг переписывают, служба поддержки просит более понятные сообщения об ошибках. Если текст живёт прямо в коде интерфейса, каждое небольшое изменение слов превращается в рискованный релиз, и вы пропустите места, где одна и та же мысль выражена по‑разному.
Ранние симптомы, которые сигнализируют о накоплении долгов по переводу:
Реалистичный пример: вы делаете простой CRM в Koder.ai. В веб‑приложении написано «Deal stage», в мобильном — «Pipeline step», а в тосте об ошибке — «Invalid status». Даже если все три фразы переведены, пользователи почувствуют несогласованность, потому что концепции не совпадают.
«Согласованность» не значит «одинаковые символы везде». Это значит:
Если вы начнёте рассматривать текст как продуктовые данные, а не как декорацию, добавление языков перестанет быть паникой и станет рутиной в процессе разработки.
Интернационализация (i18n) — это работа, которая нужна, чтобы приложение могло поддерживать много языков без переписываний. Локализация (l10n) — это конкретный набор контента для языка и региона, например французский (Канада) с подходящими словами, форматом дат и тоном.
Простая цель: каждый пользовательский текст выбирается по стабильному ключу, а не набирается прямо в коде интерфейса. Если вы можете изменить предложение, не открывая React‑компонент или Flutter‑виджет, вы на правильном пути. Это ядро архитектуры интернационализации для приложений, построенных через чат, где легко случайно запушить хардкоднутые фразы, сгенерированные в диалоге.
Пользовательский текст шире, чем многие команды думают. Он включает кнопки, метки, ошибки валидации, пустые состояния, подсказки в онбординге, push‑уведомления, письма, PDF‑экспорты и любое сообщение, которое пользователь видит или слышит. Обычно он не включает внутренние логи, названия колонок в БД, идентификаторы аналитических событий, флаги функций или отладочный вывод для админов.
Где должны храниться переводы? На практике обычно и на фронтенде, и на бэкенде, с чёткой границей ответственности.
Ошибка — смешивать ответственности. Если бэкенд возвращает готовые английские предложения для ошибок UI, фронтенд не сможет их корректно локализовать. Более правильный подход: бэкенд возвращает код ошибки (и безопасные параметры), а клиент сопоставляет этот код с локализованным сообщением.
Владение копирайтом — это продуктовое решение, а не техническая деталь. Решите заранее, кто может менять тексты и утверждать тон.
Если продукт владеет копирайтом, относитесь к переводам как к контенту: версионируйте их, проверяйте и давайте продукту безопасный способ запрашивать изменения. Если за копирайт отвечает инженерия, введите правило: любая новая UI‑строка должна идти с ключом и дефолтным переводом до релиза.
Пример: если в трёх местах потока регистрации написано «Create account», сделайте один ключ, используемый везде. Это сохраняет смысл, ускоряет переводчиков и предотвращает превращение небольшого изменения формулировки в многоппозиционную правку позже.
Ключи — это контракт между вашим UI и переводами. Если этот контракт постоянно меняется, вы получите отсутствующие строки, срочные фиксы и неоднозначные формулировки между вебом и мобильным. Хорошая архитектура i18n для чат‑проекта начинается с одного правила: ключи должны описывать смысл, а не текущее английское предложение.
Используйте стабильные идентификаторы как ключи (например, billing.invoice.payNow), а не полный текст (например, "Pay now"). Ключи‑предложения ломаются, как только кто‑то изменит пунктуацию, регистр или небольшой текст.
Практичный и читаемый шаблон: экран (или домен) + компонент + намерение. Делайте это скучно и предсказуемо.
Примеры:
auth.login.titleauth.login.emailLabelbilling.checkout.payButtonnav.settingserrors.network.offlineРешайте, когда переиспользовать ключ, а когда создать новый, задав себе вопрос: «Одинаков ли смысл в каждом месте?» Переиспользуйте ключи для по‑настоящему общих действий, но разделяйте, когда контекст меняется. Например, «Сохранить» в профиле — простое действие, а «Сохранить» в сложном редакторе может требовать иного тона в некоторых языках.
Держите общий UI‑текст в выделённых неймспейсах, чтобы он не дублировался по экранам. Полезные корзины:
common.actions.* (save, cancel, delete)common.status.* (loading, success)common.fields.* (search, password)errors.* (validation, network)nav.* (tabs, menu items)Когда формулировка меняется, но смысл остаётся тем же, оставляйте ключ и обновляйте только перевод. В этом и смысл стабильных ID. Если смысл меняется (пусть и тонко), создавайте новый ключ и оставляйте старый, пока не убедитесь, что он нигде не используется. Это предотвращает тихие рассогласования, когда старый перевод технически есть, но теперь неверен.
Небольшой пример из рабочего потока Koder.ai: ваш чат генерирует и React‑веб, и Flutter‑мобильное приложение. Если оба используют common.actions.save, переводы совпадут повсюду. Но если веб использует profile.save, а мобильный — account.saveButton, со временем вы рассинхронизируетесь, даже если сегодня английский совпадает.
Рассматривайте исходный язык (часто английский) как единый источник правды. Храните его в одном месте, ревьюьте как код и не допускайте появления строк в случайных компонентах «пока что». Это самый быстрый путь избежать хардкоднутого текста и последующей переработки.
Простое правило: приложение может отображать текст только из i18n‑системы. Если нужно новое сообщение, добавляют ключ и дефолтный текст, а затем используют ключ в UI. Это сохраняет архитектуру i18n в чат‑проектах стабильной, даже когда фичи перемещаются.
Если вы выпускаете и веб, и мобильное, вам нужен один общий каталог ключей и место для команд по фичам.
Простая организация:
Держите ключи идентичными на всех платформах, даже если реализация различается (React на вебе, Flutter на мобильном). Если вы используете платформу вроде Koder.ai для генерации обоих клиентов, экспорт исходного кода проще поддерживать, когда оба проекта ссылаются на одни и те же имена ключей и формат сообщений.
Переводы меняются со временем. Обращайтесь с изменениями как с продуктовым изменением: небольшими, проверяемыми и отслеживаемыми. Хорошее ревью фокусируется на смысле и повторном использовании, а не только на орфографии.
Чтобы ключи не расходились между командами, назначьте владение по фичам (billing., auth.) и никогда не переименовывайте ключи только потому, что поменялось формулирование. Ключи — идентификаторы, не копирайт.
Правила множественного числа зависят от языка, поэтому простая английская логика (1 vs всё остальное) быстро ломается. В одних языках есть отдельные формы для 0, 1, 2–4 и т.д. Другие меняют всю конструкцию предложения, а не только существительное. Если вы встраиваете логику плюрализации в интерфейс через if/else, вы будете дублировать тексты и пропускать краевые случаи.
Безопаснее держать одно гибкое сообщение на идею и позволять i18n‑слою выбирать форму. Сообщения в стиле ICU созданы для этого: они оставляют грамматику переводчику, а не компонентам.
Небольшой пример, который покрывает забываемые случаи:
itemsCount = "{count, plural, =0 {No items} one {# item} other {# items}}"
Этот один ключ покрывает 0, 1 и все остальные случаи. Переводчики заменят его на подходящие формы для своего языка без правки кода.
Когда нужен гендер или зависимость от роли, избегайте создания отдельных ключей вроде welcome_male и welcome_female, если продукт действительно этого не требует. Используйте select, чтобы предложение оставалось единым блоком:
welcomeUser = "{gender, select, female {Welcome, Ms. {name}} male {Welcome, Mr. {name}} other {Welcome, {name}}}"
Чтобы не загнать себя в угол с падежами, старайтесь делать предложения цельными. Не склеивайте фрагменты вроде "{count} " + t('items'), потому что во многих языках нельзя просто поменять порядок слов. Предпочитайте одно сообщение, которое содержит число, существительное и окружающие слова.
Правило, которое хорошо работает в чат‑проектах (включая проекты Koder.ai): если в предложении есть число, персона или статус, делайте его ICU‑сообщением с самого начала. Это стоит немного усилий upfront и экономит много перевода в будущем.
Если ваш React и Flutter хранят файлы переводов отдельно, они обязательно рассинхронизируются. Одна и та же кнопка получит разную формулировку, ключи будут означать разное на вебе и мобильном, и в тикетах поддержки начнут писать «в приложении написано X, а на сайте Y».
Самое простое и важное исправление: выберите один источник правды и относитесь к нему как к коду. Для большинства команд это означает единый набор файлов локалей (например, JSON с ICU‑сообщениями), которые потребляют и веб, и мобильные.
Практическая настройка — небольшой "i18n‑пакет" или папка, которая содержит:
React и Flutter становятся потребителями. Они не должны придумывать новые ключи локально. В рабочем сценарии Koder.ai (React веб, Flutter мобильный) можно генерировать оба клиента из одного набора ключей и держать изменения под ревью как любой другой код.
Выровненность с бэкендом — часть той же истории. Ошибки, уведомления и письма не должны быть захардкодены на английском в Go. Лучше возвращать стабильные коды ошибок (например, auth.invalid_password) и безопасные параметры, а клиенты сопоставляют код с переводом. Для серверных писем сервер может рендерить шаблоны, используя те же ключи и файлы локалей.
Придумайте небольшую книжку правил и применяйте её в код‑ревью:
Чтобы избежать дублирования ключей с разным смыслом, добавьте поле "description" (или файл с комментариями) для переводчиков и будущих разработчиков. Например, billing.trial_days_left должен пояснять, показывается ли он в баннере, в письме или в обоих местах. Это часто останавливает «довольно похожее» повторное использование, которое порождает долг.
Эта согласованность — основа архитектуры i18n для чат‑проекта: один словарь, много поверхностей и никакого сюрприза при добавлении нового языка.
Хорошая архитектура i18n начинается просто: один набор ключей сообщений, один источник правды для копирайта и одинаковые правила для веба и мобильных. Если вы работаете быстро (например, с Koder.ai), эта структура сохраняет скорость без накопления долгов.
Выберите локали заранее и решите, что делать при отсутствии перевода. Частый выбор: показывать предпочитаемый пользователем язык, иначе падать на английский, и логировать отсутствующие ключи, чтобы исправить их до релиза.
Дальше реализуйте:
billing.plan_name.pro или auth.error.invalid_password. Те же ключи везде.t("key") в компонентах. В Flutter используйте локализационный обёрток и делайте поиск ключей в виджетах. Цель — одинаковые ключи, а не одна библиотека.{count, plural, one {# file} other {# files}} и Hello, {name}. Это избавляет от if‑ветвей по всему коду.И, наконец, протестируйте один язык с длинными словами (например, немецкий) и один с иной пунктуацией. Это быстро выявит кнопки, которые переполняются, заголовки с плохим переносом и макеты, рассчитанные только на английский.
Если вы храните переводы в общем каталоге (или в генерируемом пакете) и относитесь к изменениям копирайта как к изменениям кода, веб и мобильные останутся согласованными даже при быстром создании через чат.
Переведённые UI‑строки — это только половина проблемы. Большинство приложений показывает изменяющиеся значения: даты, цены, счётчики и имена. Если вы обрабатываете эти значения как простой текст, получите неверные форматы, неправильные часовые пояса и предложения, звучащие «непо‑русски».
Форматируйте числа, валюту и даты по правилам локали, а не собственным кодом. Пользователь во Франции ожидает «1 234,50 €», пользователь в США — «$1,234.50». То же относится к датам: "03/04/2026" двусмысленна, а локализованный формат будет однозначен.
Часовые пояса — следующая ловушка. Сервера должны хранить отметки времени в нейтральном формате (обычно UTC), но пользователи ожидают видеть время в своей зоне. Решите правило для каждого экрана: показывать локальное время пользователя для личных событий и фиксированную бизнес‑зону для событий вроде окна самовывоза (и явно помечать её).
Избегайте построения предложений конкатенацией переведённых фрагментов. Это ломает грамматику, потому что порядок слов меняется. Вместо
"{count} " + t("items") + " " + t("in_cart")
используйте одно сообщение с плейсхолдерами: "{count} items in your cart". Тогда переводчик сможет безопасно поменять порядок слов.
RTL — это не только направление текста. Потоки макета меняются, некоторые иконки нужно зеркалить (стрелка назад), а смешанный контент (арабский плюс английский код товара) может отображаться в неожиданном порядке. Тестируйте реальные экраны, а не отдельные метки, и убедитесь, что компоненты поддерживают смену направления.
Никогда не переводите то, что написал пользователь (имена, адреса, тикеты поддержки, сообщения чата). Можно переводить метки вокруг этого контента и форматировать метаданные (даты, числа), но сам контент должен оставаться как есть. Если позже вы добавите автоперевод, делайте это как явную опцию с переключателем "оригинал/перевод".
Практический пример: в приложении Koder.ai может отображаться "{name} renewed on {date} for {amount}". Держите это одним сообщением, форматируйте {date} и {amount} по локали и показывайте в часовом поясе пользователя. Этот шаблон предотвращает много долгов по переводу.
Быстрые правила, которые обычно предотвращают ошибки:
Долг по переводам обычно начинается с "ещё одной быстрой строки" и перерастает в недели правок. В чат‑проектах это случается быстрее, потому что текст генерируется прямо в компонентах, формах и даже бэкенд‑сообщениях.
Самые дорогие проблемы — те, что распространяются по приложению и становятся труднодоступными для исправления.
Представьте, что веб и мобильное приложение показывают баннер выставления счёта: "You have 1 free credit left". Кто‑то поменял веб‑текст на "You have one credit remaining" и оставил ключ как полное предложение. Мобильное всё ещё использует старый ключ. Теперь у вас два ключа для одной концепции, и переводчики увидят оба.
Лучший подход — стабильные ключи (например, billing.creditsRemaining) и плюрализация через ICU, чтобы грамматика была корректной во всех языках. Если вы пользуетесь инструментом вроде Koder.ai, заведите правило: любой пользовательский текст, сгенерированный в чате, должен попадать в файлы переводов, а не прямо в компоненты или серверные ошибки. Эта небольшая привычка защищает архитектуру i18n в росте проекта.
Когда интернационализация кажется запутанной, обычно базовые вещи просто не были зафиксированы. Небольшой чек‑лист и один конкретный пример помогут вашей команде (и вам в будущем) избежать долгов по переводам.
Вот чек‑лист для каждого нового экрана:
billing.invoice.paidStatus, а не billing.greenLabel).Простой пример: вы запускаете экран биллинга на английском, испанском и японском. В UI есть: "Invoice", "Paid", "Due in 3 days", "1 payment method" / "2 payment methods" и сумма вроде "$1,234.50". С хорошей архитектурой i18n вы определяете ключи один раз (общие для веба и мобильного), и каждый язык только заполняет значения. "Due in {days} days" делается ICU‑сообщением, а форматирование суммы — через локалезированный форматтер, а не через жёсткие запятые.
Внедряйте поддержку языков фича за фичей, а не одним большим переработкой:
Задокументируйте две вещи, чтобы новые фичи оставались согласованными: правила именования ключей (с примерами) и «definition of done» для строк (никакого хардкода, ICU для плюралей, форматирование дат/чисел, добавление в общие каталоги).
Дальше: если вы строите в Koder.ai, используйте Planning Mode, чтобы определять экраны и ключи перед генерацией UI. Потом используйте снимки и откаты, чтобы безопасно итерировать копирайт и переводы для веба и мобильных без риска испортить релиз.