7 мин

Архитектура интернационализации для приложений, созданных через чат

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

Архитектура интернационализации для приложений, созданных через чат

Что ломается первым, когда вы добавляете новые языки

Первым ломается не код, а слова.

Приложения, создаваемые через чат, часто начинаются как быстрый прототип: вы пишете «Добавь кнопку с надписью Сохранить», интерфейс появляется, и вы идёте дальше. Через недели вы хотите добавить испанский и немецкий, и обнаруживаете, что эти «временные» подписи раскиданы по экранам, компонентам, письмам и сообщениям об ошибках.

Изменения текста происходят чаще, чем изменения кода. Названия продуктов переименовывают, юридические формулировки меняются, онбординг переписывают, служба поддержки просит более понятные сообщения об ошибках. Если текст живёт прямо в коде интерфейса, каждое небольшое изменение слов превращается в рискованный релиз, и вы пропустите места, где одна и та же мысль выражена по‑разному.

Ранние симптомы, которые сигнализируют о накоплении долгов по переводу:

  • Смешение языков на одном экране (некоторые строки переведены, другие всё ещё на английском).
  • Дублирование одной и той же метки с небольшими отличиями ("Sign up", "Sign Up", "Create account").
  • Ломаются макеты при увеличении длины текста (кнопки выходят за границы, заголовки плохо переносятся).
  • Веб и мобильное приложение рассинхронизируются (разные слова для одного действия).
  • Контент поддержки и системные сообщения отстают от интерфейса.

Реалистичный пример: вы делаете простой CRM в Koder.ai. В веб‑приложении написано «Deal stage», в мобильном — «Pipeline step», а в тосте об ошибке — «Invalid status». Даже если все три фразы переведены, пользователи почувствуют несогласованность, потому что концепции не совпадают.

«Согласованность» не значит «одинаковые символы везде». Это значит:

  • Одна и та же концепция использует один и тот же ключ и одно и то же значение во всех местах.
  • Веб и мобильный используют один источник правды для переводов.
  • Тон и терминология стабильны (формальный или неформальный стиль, "customer" vs "client").
  • Макеты рассчитаны на более длинные и более короткие фразы.

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

Базовые понятия и простая цель

Интернационализация (i18n) — это работа, которая нужна, чтобы приложение могло поддерживать много языков без переписываний. Локализация (l10n) — это конкретный набор контента для языка и региона, например французский (Канада) с подходящими словами, форматом дат и тоном.

Простая цель: каждый пользовательский текст выбирается по стабильному ключу, а не набирается прямо в коде интерфейса. Если вы можете изменить предложение, не открывая React‑компонент или Flutter‑виджет, вы на правильном пути. Это ядро архитектуры интернационализации для приложений, построенных через чат, где легко случайно запушить хардкоднутые фразы, сгенерированные в диалоге.

Пользовательский текст шире, чем многие команды думают. Он включает кнопки, метки, ошибки валидации, пустые состояния, подсказки в онбординге, push‑уведомления, письма, PDF‑экспорты и любое сообщение, которое пользователь видит или слышит. Обычно он не включает внутренние логи, названия колонок в БД, идентификаторы аналитических событий, флаги функций или отладочный вывод для админов.

Где должны храниться переводы? На практике обычно и на фронтенде, и на бэкенде, с чёткой границей ответственности.

  • Фронтенд отвечает за оболочку UI: навигацию, формы, меню и большую часть экранного текста.
  • Бэкенд отвечает за сообщения, которые генерирует сам: транзакционные письма, серверные ошибки валидации и всё, что возвращается как response‑ошибка.
  • Общие домены (например, метки статусов заказов) должны приходить из одного источника правды, чтобы веб и мобильное не расходились.

Ошибка — смешивать ответственности. Если бэкенд возвращает готовые английские предложения для ошибок UI, фронтенд не сможет их корректно локализовать. Более правильный подход: бэкенд возвращает код ошибки (и безопасные параметры), а клиент сопоставляет этот код с локализованным сообщением.

Владение копирайтом — это продуктовое решение, а не техническая деталь. Решите заранее, кто может менять тексты и утверждать тон.

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

Пример: если в трёх местах потока регистрации написано «Create account», сделайте один ключ, используемый везде. Это сохраняет смысл, ускоряет переводчиков и предотвращает превращение небольшого изменения формулировки в многоппозиционную правку позже.

Как структурировать ключи строк, чтобы они оставались стабильными

Ключи — это контракт между вашим UI и переводами. Если этот контракт постоянно меняется, вы получите отсутствующие строки, срочные фиксы и неоднозначные формулировки между вебом и мобильным. Хорошая архитектура i18n для чат‑проекта начинается с одного правила: ключи должны описывать смысл, а не текущее английское предложение.

Используйте стабильные идентификаторы как ключи (например, billing.invoice.payNow), а не полный текст (например, "Pay now"). Ключи‑предложения ломаются, как только кто‑то изменит пунктуацию, регистр или небольшой текст.

Практичный и читаемый шаблон: экран (или домен) + компонент + намерение. Делайте это скучно и предсказуемо.

Примеры:

  • auth.login.title
  • auth.login.emailLabel
  • billing.checkout.payButton
  • nav.settings
  • errors.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 в чат‑проектах стабильной, даже когда фичи перемещаются.

Структура папок, которая остаётся понятной

Если вы выпускаете и веб, и мобильное, вам нужен один общий каталог ключей и место для команд по фичам.

Простая организация:

  • /i18n
  • /i18n/locales/en.json (источник)
  • /i18n/locales/es.json, /i18n/locales/fr.json, ...
  • /i18n/features/billing.json, /i18n/features/auth.json, ...
  • /i18n/shared.json (кнопки, общие метки, ошибки)

Держите ключи идентичными на всех платформах, даже если реализация различается (React на вебе, Flutter на мобильном). Если вы используете платформу вроде Koder.ai для генерации обоих клиентов, экспорт исходного кода проще поддерживать, когда оба проекта ссылаются на одни и те же имена ключей и формат сообщений.

Версионирование и ревью, которые предотвращают долг по переводам

Переводы меняются со временем. Обращайтесь с изменениями как с продуктовым изменением: небольшими, проверяемыми и отслеживаемыми. Хорошее ревью фокусируется на смысле и повторном использовании, а не только на орфографии.

  • Требуйте PR‑ревью для любых изменений в исходной локали
  • Блокируйте удаление ключей без поиска и плана
  • Добавляйте пояснения для переводчиков, когда текст неоднозначен
  • Используйте простую проверку «отсутствующие переводы» в CI

Чтобы ключи не расходились между командами, назначьте владение по фичам (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‑пакет" или папка, которая содержит:

  • Файлы локалей для каждого языка (одни и те же ключи везде)
  • Правила формата сообщений (ICU для плюралей и плейсхолдеров)
  • Короткое README с инструкцией, как добавлять ключи

React и Flutter становятся потребителями. Они не должны придумывать новые ключи локально. В рабочем сценарии Koder.ai (React веб, Flutter мобильный) можно генерировать оба клиента из одного набора ключей и держать изменения под ревью как любой другой код.

Выровненность с бэкендом — часть той же истории. Ошибки, уведомления и письма не должны быть захардкодены на английском в Go. Лучше возвращать стабильные коды ошибок (например, auth.invalid_password) и безопасные параметры, а клиенты сопоставляют код с переводом. Для серверных писем сервер может рендерить шаблоны, используя те же ключи и файлы локалей.

Правила, которые поддерживают синхронность

Придумайте небольшую книжку правил и применяйте её в код‑ревью:

  • Новый UI‑текст требует нового ключа в общих файлах локалей заранее
  • Ключи должны содержать понятный неймспейс и намерение (не позицию на экране)
  • Каждый ключ должен иметь определённые плейсхолдеры, используемые одинаково везде
  • Если два выражения отличаются по смыслу, они не должны делить ключ
  • Если два ключа значат одно и то же, удалите один и оставьте победителя

Чтобы избежать дублирования ключей с разным смыслом, добавьте поле "description" (или файл с комментариями) для переводчиков и будущих разработчиков. Например, billing.trial_days_left должен пояснять, показывается ли он в баннере, в письме или в обоих местах. Это часто останавливает «довольно похожее» повторное использование, которое порождает долг.

Эта согласованность — основа архитектуры i18n для чат‑проекта: один словарь, много поверхностей и никакого сюрприза при добавлении нового языка.

Пошаговая настройка для реального проекта

Зарабатывайте пока строите
Получайте кредиты за создание контента о своём проекте или за приглашения в Koder.ai.

Хорошая архитектура i18n начинается просто: один набор ключей сообщений, один источник правды для копирайта и одинаковые правила для веба и мобильных. Если вы работаете быстро (например, с Koder.ai), эта структура сохраняет скорость без накопления долгов.

Практичная настройка (веб + мобильный)

Выберите локали заранее и решите, что делать при отсутствии перевода. Частый выбор: показывать предпочитаемый пользователем язык, иначе падать на английский, и логировать отсутствующие ключи, чтобы исправить их до релиза.

Дальше реализуйте:

  • Определите локали и fallback: выберите поддерживаемые языки, локаль по умолчанию и порядок падения. Согласуйте, как вы будете определять локаль (настройки браузера/приложения, профиль пользователя).
  • Создайте функцию перевода и соглашение по ключам: используйте стабильные, смысловые ключи (не полные предложения), например billing.plan_name.pro или auth.error.invalid_password. Те же ключи везде.
  • Подключите к React и Flutter: в React оборачивайте приложение в i18n‑провайдер и используйте t("key") в компонентах. В Flutter используйте локализационный обёрток и делайте поиск ключей в виджетах. Цель — одинаковые ключи, а не одна библиотека.
  • Поддерживайте переменные и правила плюрализации с самого начала: используйте ICU‑сообщения для плюралей и плейсхолдеров, например {count, plural, one {# file} other {# files}} и Hello, {name}. Это избавляет от if‑ветвей по всему коду.
  • Добавьте лёгкий шаг проверки копирайта: до релиза проверяйте новые или изменённые строки: нейминг ключей, отсутствие хардкоднутого UI‑текста, соответствие плейсхолдеров и то, что веб и мобильный подхватили изменение.

И, наконец, протестируйте один язык с длинными словами (например, немецкий) и один с иной пунктуацией. Это быстро выявит кнопки, которые переполняются, заголовки с плохим переносом и макеты, рассчитанные только на английский.

Если вы храните переводы в общем каталоге (или в генерируемом пакете) и относитесь к изменениям копирайта как к изменениям кода, веб и мобильные останутся согласованными даже при быстром создании через чат.

Динамический контент: даты, числа и пользовательский текст

Переведённые UI‑строки — это только половина проблемы. Большинство приложений показывает изменяющиеся значения: даты, цены, счётчики и имена. Если вы обрабатываете эти значения как простой текст, получите неверные форматы, неправильные часовые пояса и предложения, звучащие «непо‑русски».

Форматируйте числа, валюту и даты по правилам локали, а не собственным кодом. Пользователь во Франции ожидает «1 234,50 €», пользователь в США — «$1,234.50». То же относится к датам: "03/04/2026" двусмысленна, а локализованный формат будет однозначен.

Часовые пояса — следующая ловушка. Сервера должны хранить отметки времени в нейтральном формате (обычно UTC), но пользователи ожидают видеть время в своей зоне. Решите правило для каждого экрана: показывать локальное время пользователя для личных событий и фиксированную бизнес‑зону для событий вроде окна самовывоза (и явно помечать её).

Избегайте построения предложений конкатенацией переведённых фрагментов. Это ломает грамматику, потому что порядок слов меняется. Вместо

"{count} " + t("items") + " " + t("in_cart")

используйте одно сообщение с плейсхолдерами: "{count} items in your cart". Тогда переводчик сможет безопасно поменять порядок слов.

Языки с письмом справа налево (RTL)

RTL — это не только направление текста. Потоки макета меняются, некоторые иконки нужно зеркалить (стрелка назад), а смешанный контент (арабский плюс английский код товара) может отображаться в неожиданном порядке. Тестируйте реальные экраны, а не отдельные метки, и убедитесь, что компоненты поддерживают смену направления.

Контент, созданный пользователем

Никогда не переводите то, что написал пользователь (имена, адреса, тикеты поддержки, сообщения чата). Можно переводить метки вокруг этого контента и форматировать метаданные (даты, числа), но сам контент должен оставаться как есть. Если позже вы добавите автоперевод, делайте это как явную опцию с переключателем "оригинал/перевод".

Практический пример: в приложении Koder.ai может отображаться "{name} renewed on {date} for {amount}". Держите это одним сообщением, форматируйте {date} и {amount} по локали и показывайте в часовом поясе пользователя. Этот шаблон предотвращает много долгов по переводу.

Быстрые правила, которые обычно предотвращают ошибки:

  • Храните отметки времени в UTC, форматируйте по локали и часовому поясу пользователя.
  • Используйте плейсхолдеры внутри полного предложения, не склеивайте фрагменты.
  • Форматируйте валюту согласно локали и с правильным кодом валюты.
  • Тестируйте хотя бы одну RTL‑локаль на реальных экранах.
  • По умолчанию не переводите пользовательский контент.

Общие ошибки, которые создают долг по переводам

Локализуйте ошибки API правильно
Делайте Go‑бэкенды, которые возвращают коды ошибок — клиенты локализуют их согласованно.

Долг по переводам обычно начинается с "ещё одной быстрой строки" и перерастает в недели правок. В чат‑проектах это случается быстрее, потому что текст генерируется прямо в компонентах, формах и даже бэкенд‑сообщениях.

Проблемы, которые бьют сильнее всего

Самые дорогие проблемы — те, что распространяются по приложению и становятся труднодоступными для исправления.

  • Хардкод текста прямо в UI‑компонентах, включая плейсхолдеры, пустые состояния и подписи кнопок. Это нельзя проверить или переиспользовать, и любое изменение требует правки кода.
  • Позволять бэкенду возвращать английские предложения в ответах валидации и API. UI показывает смешанные языки, и нельзя надёжно сопоставить ошибки с переводами.
  • Использовать полное английское предложение как ключ перевода. Это удобно сначала, но как только текст меняется — ключи текут, старые остаются и вы теряете память переводов.
  • Копировать ключи между вебом и мобильным и редактировать их отдельно. Со временем похожие экраны расходятся, и пользователи замечают разницу.
  • Откладывать правила плюрализации до включения первого не‑англоязычного языка. Тогда вы обнаружите десятки багов вроде "1 items" и неловкой грамматики, которые не исправить простыми if‑ами.

Быстрый пример из реальной жизни

Представьте, что веб и мобильное приложение показывают баннер выставления счёта: "You have 1 free credit left". Кто‑то поменял веб‑текст на "You have one credit remaining" и оставил ключ как полное предложение. Мобильное всё ещё использует старый ключ. Теперь у вас два ключа для одной концепции, и переводчики увидят оба.

Лучший подход — стабильные ключи (например, billing.creditsRemaining) и плюрализация через ICU, чтобы грамматика была корректной во всех языках. Если вы пользуетесь инструментом вроде Koder.ai, заведите правило: любой пользовательский текст, сгенерированный в чате, должен попадать в файлы переводов, а не прямо в компоненты или серверные ошибки. Эта небольшая привычка защищает архитектуру i18n в росте проекта.

Быстрый чек‑лист, реалистичный пример и дальнейшие шаги

Когда интернационализация кажется запутанной, обычно базовые вещи просто не были зафиксированы. Небольшой чек‑лист и один конкретный пример помогут вашей команде (и вам в будущем) избежать долгов по переводам.

Вот чек‑лист для каждого нового экрана:

  • Стабильные ключи: один ключ на смысл, а не на формулировку (пример: billing.invoice.paidStatus, а не billing.greenLabel).
  • Фоллбэки: определите язык по умолчанию и что происходит при отсутствии ключа (показывать fallback, логировать или блокировать релиз).
  • Плюральные правила: используйте ICU‑сообщения для счётчиков (0, 1, много), а не текстовые хаки.
  • Форматирование: форматируйте даты, деньги и числа по локали (и держите валюту отдельно от языка).
  • RTL‑проверка: протестируйте хотя бы одну правонаправленную локаль рано, чтобы проблемы макета проявились до того, как у вас будет 200 экранов.

Простой пример: вы запускаете экран биллинга на английском, испанском и японском. В UI есть: "Invoice", "Paid", "Due in 3 days", "1 payment method" / "2 payment methods" и сумма вроде "$1,234.50". С хорошей архитектурой i18n вы определяете ключи один раз (общие для веба и мобильного), и каждый язык только заполняет значения. "Due in {days} days" делается ICU‑сообщением, а форматирование суммы — через локалезированный форматтер, а не через жёсткие запятые.

Внедряйте поддержку языков фича за фичей, а не одним большим переработкой:

  1. Начните с одного высоконагруженного потока (billing, onboarding или checkout).
  2. Перенесите хардкоднутый UI‑текст в файлы переводов и замените на ключи.
  3. Добавьте плюральные сообщения и форматирование для того же потока.
  4. Распространяйте на следующую фичу только после того, как количество отсутствующих ключей близко к нулю.

Задокументируйте две вещи, чтобы новые фичи оставались согласованными: правила именования ключей (с примерами) и «definition of done» для строк (никакого хардкода, ICU для плюралей, форматирование дат/чисел, добавление в общие каталоги).

Дальше: если вы строите в Koder.ai, используйте Planning Mode, чтобы определять экраны и ключи перед генерацией UI. Потом используйте снимки и откаты, чтобы безопасно итерировать копирайт и переводы для веба и мобильных без риска испортить релиз.

FAQ

Как называть ключи переводов?

Используйте стабильные ключи, основанные на смысле, например billing.invoice.payNow, а не саму английскую фразу. Тогда формулировку можно менять без изменения ссылок в коде и без появления дублирующихся переводов.

Какой текст нужно добавлять в файлы переводов?

Храните в системе переводов все строки, которые видит пользователь, включая ошибки, пустые состояния, письма, уведомления и подсказки при первом запуске. Внутренние логи, идентификаторы аналитики и поля базы данных обычно переводить не нужно.

Как поддерживать единые формулировки в веб- и мобильной версиях?

Используйте один общий каталог ключей и локализованных сообщений для обоих клиентов. React и Flutter могут работать с разными библиотеками, но должны ссылаться на одинаковые названия ключей и правила сообщений.

Должен ли бэкенд возвращать переведённые сообщения об ошибках?

Пусть бэкенд возвращает стабильный код ошибки и безопасные значения, а клиент показывает локализованное сообщение. Например, возвращайте auth.invalid_password вместо английской фразы.

Как обрабатывать множественные формы в разных языках?

Используйте сообщения с множественными формами в стиле ICU, чтобы каждый язык применял собственные грамматические правила. Помещайте всё предложение в одно сообщение, а не добавляйте число к отдельно переведённому существительному.

Где хранить файлы переводов?

Сделайте исходную локаль, часто английскую, источником истины и проверяйте изменения вместе с кодом. Группируйте сообщения по функциям и выделяйте общие пространства имён для типичных действий, полей, навигации и ошибок.

Нужен ли новый ключ при каждом изменении текста?

Сохраняйте существующий ключ, если смысл остаётся тем же, даже когда меняется формулировка. Создавайте новый ключ, только если меняется смысл или контекст, затем удаляйте старый, убедившись, что он больше нигде не используется.

Как локализовать даты, деньги и часовые пояса?

Храните временные метки в UTC, затем форматируйте даты, время, числа и валюту с учётом локали и часового пояса пользователя. Используйте форматтеры, учитывающие локаль, вместо жёстко заданных запятых, знаков валюты или шаблонов дат.

Что тестировать для языков с письмом справа налево?

Проектируйте целые экраны в RTL-локали, а не только подписи. При необходимости меняйте направление макета, зеркально отображайте направленные значки, например стрелки «назад», и проверяйте смешанный арабский и латинский текст, например коды продуктов.

Как проще всего добавить i18n в существующее приложение?

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

Похожие статьи