# Локализация — Администрирование

Документ описывает администрирование локализации в 1Форме: управление языками в таблице `Languages`, поштучную и пакетную локализацию сущностей, автоперевод через Azure Translate, строки интерфейса `LocalizationResources`, встроенную локализацию в контроллерах и SQL-диагностику.

## 1. Управление языками

Таблица `Languages`. Администратор может добавлять/удалять языки. Ограничения:

- Ровно один язык с `IsDefault=1` (уникальный filtered index)
- Ровно один язык с `IsInternational=1` (уникальный filtered index)
- `LangAlias` уникален (unique index)
- Удаление языка каскадно удаляет все значения в `LocalizedBusinessObjectValues` и `LocalizedDataObjectValues` (CASCADE DELETE)

Поля для настройки:
- `LangAlias` — код языка (`rus`, `eng`, `de`, `fr`, `cn`). Он же служит переносимым идентификатором языка между площадками: номера языков в базах разных установок не совпадают, поэтому при переносе данных — например, при импорте почтовых шаблонов — язык определяется по алиасу
- `LangDescr` — отображаемое имя
- `culture` — .NET culture code для форматирования дат/чисел (`ru-RU`, `en-US`)
- `LanguageCode` — ISO-код для Azure Translate API
- `StatesOnSignPrefix` — префикс для подписей на этом языке

**Шаблоны системных комментариев.** У системных записей ленты («выполнен переход», «запрошена подпись», «изменены плановые трудозатраты» и другие) своя таблица шаблонов — `dbo.EventTypeTemplates`, где шаблон привязан к событию и языку. В админке локализации они не редактируются: значок локализации к ним не относится, состав шаблонов для языка задаётся миграцией. Поэтому, добавляя новый язык в `Languages`, нужно убедиться, что шаблоны для него заведены: без них читатель с этим языком получает текст на языке по умолчанию, а не на международном.

#### Как добавить новый язык (чек-лист)

Новый язык появляется в списке выбора, когда закрыты все пять точек. Пример — таджикский (`tg-TJ`):

1. **Запись в `Languages`** — заводится кодовой миграцией, а не через API: контроллер и сервис языков умеют только читать, а прямой `INSERT` проходит мимо кеша языков и не воспроизводится на других площадках. Миграция идемпотентна: отметка о накате плюс проверка, что такой культуры ещё нет, а в конце сбрасывается кеш языков.
2. **Код культуры** — подбирается так, чтобы файл локали Angular существовал. Загрузчик отбрасывает последний сегмент кода и грузит файл по остатку: для таджикского выбран код `tg-TJ`, который приводит к `tg.js`, а код с письменностью (`tg-Cyrl-TJ`) запросил бы `tg-Cyrl.js`. Письменность задаёт содержимое ресурсного файла, а не код культуры; перед выбором кода сверьтесь с составом пакета локалей установленной версии Angular.
3. **Ресурсные файлы интерфейса** — словари языка: общий, табличного представления и табеля (`tg-TJ`, `ag-grid-tg-TJ`, `timesheet-tg-TJ`), и их регистрация в `bootstrap/resource-providers.ts`. Данные локали Angular в репозиторий не кладутся — каталог локалей копируется сборкой. Шрифт проверяется на все глифы языка во всех начертаниях.
4. **Серверные тексты** — шаблоны системных комментариев для нового языка и префикс подписей «на подписи» (`StatesOnSignPrefix`): без них читатель получает текст на языке по умолчанию. Таджикская миграция этот префикс не заполняет — значение в таблице пустое.
5. **Проверка** — переключить интерфейс на новый язык: переведённое показывает перевод, непереведённые места показывают русский текст (это и есть остаток непереведённого), сырого имени ключа не видно. Подписи, которые длиннее русских более чем в полтора раза, проверяются на вёрстку, спорные термины — носителем языка.

## 2. Локализация сущностей: поштучно и пакетно

Рядом с названием каждой локализуемой сущности (категория, статус, ДП, кнопка перехода и т.д.) отображается значок локализации. Такой же значок стоит в профиле пользователя справа от полей «Фамилия», «Имя», «Отчество».

Локализовать можно только уже существующий объект. В окнах создания и копирования категории значка локализации нет — объект ещё не создан; чтобы задать перевод названия новой категории, сначала создайте её, затем откройте переименование и введите перевод там.

![Иконки локализации (глобус) справа от полей «Фамилия», «Имя», «Отчество» в профиле пользователя](https://help.1forma.ru/help-images/localization/localization-01.png)

По клику на значок открывается окно ввода значения для каждого активного языка системы.

<img src="https://help.1forma.ru/help-images/localization/localization-02.png" alt="Окно локализации фамилии пользователя: поле для каждого активного языка" width="360">

Сохранение: `POST app/v2/api/localization/{entityType}/{entityId}` с массивом `{LanguageId, Value}`.

Чтение: `GET app/v2/api/localization/{entityType}/{entityId}` — возвращает значения для всех языков.

Раздел админки для массовой локализации позволяет выгрузить названия в Excel, перевести и загрузить обратно.

**Выгрузка:**

1. Выбрать язык файла локализации (например, русский)
2. Отметить категории или типы сущностей для локализации
3. Нажать «Выгрузить файл» → Excel с колонками: EntityType, EntityId, LanguageId, Value

**Загрузка:**

1. Перевести значения в Excel. `LanguageId` должен соответствовать `Languages.ID` в БД
2. Загрузить файл через форму "Загрузить файл"
3. Система объединяет переводы для каждой строки

Подробнее: [Системные настройки](https://help.1forma.ru/domains/system/admin.md).

## 3. Локализация текста задач и текстовых ДП

Текст задачи и текстовые ДП («Текст», «Большой текст с форматированием / без форматирования») можно вести на нескольких языках. Возможность включается настройкой категории «Локализовать текст задач»; для перевода с форматированием дополнительно нужна настройка «Разрешить HTML в тексте задач», а для ДП — опция «Локализуемый» в расширенных настройках. Локализованное значение задаётся уже в карточке созданной задачи.

По нажатию кнопки «Локализовать» в карточке задачи открывается окно ввода текста для каждого активного языка системы; в самой карточке текст показывается на языке, выбранном в профиле пользователя.

![Кнопка «Локализовать» в карточке задачи](https://help.1forma.ru/help-images/localization/localization-03.png)

Для обычного текста окно содержит по одному полю на каждый язык.

![Окно локализации текста задачи: поле для каждого активного языка](https://help.1forma.ru/help-images/localization/localization-04.png)

Если для категории разрешён HTML, поля заменяются редакторами с форматированием — тот же режим применяется и к локализуемым текстовым ДП.

![Окно ввода локализованных значений с форматированием для текста задачи и текстовых ДП](https://help.1forma.ru/help-images/localization/local_tasktext.png)

Список языков в окне ограничен активными языками системы (настраивается в окне «Доступные языки» общих настроек приложения).

⚠️ Фильтрация в гриде по локализованным текстовым ДП (Текст, Большой текст с форматированием / без форматирования) недоступна: при включённом флаге «Локализуемый» фильтрующий столбец не отображается. На текстовые ДП без флага локализации ограничение не распространяется.

## 4. Azure Translate

Автоматический перевод при создании/обновлении конфигурационных сущностей.

**Как работает**

При создании или обновлении сущности:
1. Определяются языки, для которых нет значения
2. Асинхронно запускается перевод недостающих значений
3. Azure Translate API переводит текст на недостающие языки
4. Результат сохраняется

**Где применяется**

- Создание/переименование категории
- Создание/переименование статуса
- Создание/изменение опции ДП

**Лог**

Все вызовы Azure Translate логируются в `AzureTranslationLog`: время, пользователь, исходный и целевой языки, переведённый текст.

**Конфигурация**

Ключ Azure Translate API хранится в настройках системы. Если ключ не настроен, автоперевод не выполняется — значения остаются только для языка по умолчанию.

Если перевод не появился, потому что сущность удалили во время перевода, ошибки не будет: фоновая задача перечитывает сущность и пропускает запись. Так же пропускается запись, когда переводы сущности удалили осознанно. Объект локализации заново не создаётся, строк без родителя не остаётся.

## 5. LocalizationResources и встроенная локализация

Таблица `LocalizationResources` содержит статические строки интерфейса (метки, названия, подписи). Двуязычная структура: `RussianValue` + `EnglishValue`.

- Кешируется в памяти приложения
- Группировка по `BlockName` (например `EntityNames`)
- Используется интерфейсом для отображения на выбранном языке

Для добавления/изменения строк: прямая правка в БД или миграции. Отдельной формы в интерфейсе для администрирования нет.

Помимо отдельного Localization API, локализация встроена в контроллеры соответствующих сущностей. При обновлении категории, статуса, ДП через Admin API автоматически обновляются локализованные значения (включая автоперевод через Azure Translate).

**Статические строки SPA-интерфейса** (подписи, тултипы, пункты меню шапки, боковой панели, окна создания чата, фильтров ленты) локализуются отдельно от таблицы `LocalizationResources`. Они хранятся во фронтенде — в ресурсных файлах по одному на язык (`apps/spa/src/app/resources/*.ts`, все 13 языков платформы) — как карты «ключ → значение», сгруппированные по блокам (например, блок `common`). В отличие от `LocalizationResources` (двуязычная структура `RussianValue`/`EnglishValue`, бэкенд и legacy-контроллеры), эти файлы покрывают все поддерживаемые языки. Поэтому подпись элемента веб-интерфейса правится в соответствующем ресурсном файле SPA, а не в БД.

**Подписи логических значений «Да» и «Нет».** Там, где значение ДП типа «Флажок» показывается текстом, а не квадратом с галочкой, подпись берётся из тех же ресурсных файлов — блок `common`, ключи `yes` и `no` — и подчиняется общей цепочке отката: нет ключа в файле языка, значение берётся из английского файла, затем из русского. На русском интерфейсе это «Да» и «Нет», на остальных языках — перевод своего языка. Так выводятся колонка ДП «Флажок» в таблице резолюций, снимок задачи и карточка задачи в мобильном приложении. Само значение параметра остаётся машинным — переводится только подпись на экране.

К этим же файлам относятся подсказки значков графа маршрута в дизайнере процессов — «На переходе есть подзадачи», «Подписи: …», «Хранимые процедуры: …», «Есть автоисполнители», «Есть ограничения по дп», «Есть действия с ДП»: их переводы правятся там же, а не в базе.

**Экраны карточки задачи.** Кроме страниц администрирования ресурсными подписями покрыты экраны карточки задачи: настройка повторений (включая её прежнюю версию), логика и представление карточки, снимок задачи, журнал подписей, история шагов, обновление и постановка задач из CSV, поля дополнительных параметров и их гриды, блок категории и подкатегории, мобильная карточка задачи, встреча в задаче, генерация файлов. Новые ключи интерфейса заводятся сразу в файлы `ru-RU` и `en-US`, остальные языки догоняются пакетным переводом словарей. Позиция, которая пользовательским текстом не является — значение, уходящее на сервер, ключ данных, текст в журнале, — остаётся литералом с пометкой о причине; сравнения по русскому тексту в условиях на ключи не переводятся.

**Если строка не переведена.** Когда ключа нет в ресурсном файле выбранного языка, значение берётся из английского файла, а если и там нет — из русского. Поэтому непереведённая подпись показывается на английском и не пропадает с экрана.

Английская надпись в интерфейсе на другом языке бывает и тогда, когда ключ в файле языка есть, но значение в нём оставлено английским: запасной язык в этом случае не нужен, строка показывается так, как записана. На экране оба случая неотличимы, различить их можно только по ресурсному файлу языка. Отсутствующий ключ исправляется добавлением строки, английское значение — правкой самого значения в файле языка.

**Если на месте подписи видно имя ключа.** Вместо подписи на экран может попасть само имя ресурсного ключа, например `addingAPublication`. Так бывает, когда ключа нет ни в одном ресурсном файле и цепочка «язык пользователя → английский → русский» не нашла значения, либо когда ключ записан без префикса блока. Лечится это правкой ресурсных файлов, а не шаблоном страницы. Английская надпись там, где ожидается другой язык, — другой случай, он описан выше.

**Если на другом языке видна русская подпись.** Русская надпись на интерфейсе другого языка бывает не только из словаря: к ключу в разметке бывает приписано запасное значение — русский текст, который пайп `resx` подставляет, когда ключ не найден ни в одном ресурсном файле. Запасное значение стоит в разборе последним, а не перед цепочкой: если цепочка «язык пользователя → английский → русский» нашла ключ, показывается найденное значение, и до запасного текста дело не доходит — он подставляется только на пустом результате, а когда запасного значения нет, на экран попадает имя ключа. Поэтому такой дефект выглядит обычной русской строкой и на русском экране незаметен: отсутствующий во всех файлах ключ он не выдаёт, а проявляется на остальных языках. Лечится это правкой кода и словарей — запасное значение убирают, а ключ заводят в `ru-RU` и `en-US`; если ключ в файлах есть, а русская подпись всё равно видна, причина в цепочке отката — это случай выше.

**Покрытие страниц администрирования.** Подписи, названия пунктов меню, диалоги подтверждения и уведомления на страницах отчётов и фильтров отчётов, в дашбордах и их виджетах, в настройках категории и её разделов (блоки дополнительных параметров, кнопки, ресурсы, опросы, тулбары, файлы категорий), в автоматизации на смарт-выражениях и выражениях Lua (включая историю версий и параметры скриптов), на страницах источников данных (настраиваемые пользовательские источники, параметры представлений, мобильные источники данных задач), в группах избранного и настройках шкал календаря, на общих экранах редактора сущностей и очередей, на страницах сервисов и интеграций (список сервисов и форма сервиса, синхронизация с 1С, почтовые ящики — пользовательские, сервисные и категорийные, настройки синхронизации папок ящика), на страницах обслуживания системы (задания и настройки расписаний, кеши, вставки CSS и JS, объекты и параметры таблиц базы данных, денормализация, очереди сообщений, секреты и журнал аудита секретов, рекомендованные и недостающие индексы, импорт документации на диск, системные файлы), на страницах пользователей и доступа (настройки пользователей по умолчанию и карточка пользователя, синхронизация с AD, настройки подписи и плагина ЭП, файловые хранилища, права и источники отчётов, настройки личных задач и интерфейса пользователя), а также в порталах, контейнерах, дизайнере процессов и на стартовой странице администрирования берутся из ресурсных файлов и показываются на языке интерфейса пользователя. На русском подписи прежние, на остальных языках — из файла своего языка. Если подпись здесь показана по-английски, причина та же, что выше: либо ключа нет в файле этого языка, либо значение в нём оставлено английским. Отдельный случай — подпись, которую страница успевает нарисовать до загрузки словаря: она показывается запасным значением из кода, пока словарь не пришёл.

**Покрытие общих компонентов.** Подписи, подсказки, заголовки колонок, пункты меню и кнопки общих компонентов, которые встречаются на разных экранах платформы, — календаря и просмотра события, досок и канбана, пакетной обработки задач, хлебных крошек категории, диаграммы Ганта и Ганта подзадач, оргдиаграммы, дашбордов, редактора кода, навигации, мобильных карточек и мобильного меню, ресурсной таблицы, конструктора процесса категории, контекстного меню помощника, окна копирования категории, планировщика, селектора категории, подсказок горячих клавиш, пространств, настроек темы оформления, ленты событий и динамического мастера — берутся из ресурсных файлов и показываются на языке интерфейса пользователя; диалоги подтверждения и всплывающие уведомления этих компонентов локализованы так же. На русском подписи совпадают с прежними. Ключи заводятся в `ru-RU` и `en-US`, перевод на остальные культуры догоняется пакетным переводом словарей.

**Покрытие каркаса веб-приложения.** Подписи и подсказки элементов оболочки — левого меню навигации, верхней шапки, панели быстрого доступа, избранного, чата, панели сессий помощника, кнопок-тикеров, всплывающих уведомлений и модальных окон уровня приложения — берутся из тех же ресурсных файлов, блок `common`. В разметке подпись выводится пайпом `resx`, а надписи, которые каркас собирает в коде, читаются из снимка словаря: снимок складывает английский словарь блока и словарь языка пользователя так, что значение языка пользователя перекрывает английское. Поэтому ключ, заведённый только в `ru-RU` и `en-US`, на остальных языках берёт английское значение, а не остаётся пустой подписью — прямой доступ к словарю сам по себе общий откат не применяет. Значения, которые пользователю не показываются — ключи данных, параметры запросов, текст технических журналов, — остаются литералами с пометкой о причине.

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

**Покрытие общих экранов.** Подписи общих элементов, встречающихся вне администрирования, — формы единиц времени и размера файла, пресеты периодов в фильтрах («Сегодня», «Текущая неделя», «Последние 3 месяца» и подобные), виджеты портала (фильтр, график, диаграмма Ганта, избранное, контакты, лента социальной сети, кнопка-тикер, меню, новости, поиск задач), создание задачи, мобильные профиль, подписи и настройки, диалоги подтверждения и всплывающие уведомления общих сервисов, экраны загрузки приложения, страницы входа, регистрации и восстановления доступа по логину, профиль пользователя в веб-интерфейсе (просмотр и редактирование, подписи, настройки уведомлений, заместители, токены доступа), страницы Диска, почты и списка писем, просмотра файла, контролов, канбана подкатегории, поиска, резолюций, пространств и журнала событий — берутся из тех же ресурсных файлов и показываются на языке интерфейса пользователя; на русском формулировки прежние. Если подпись на другом языке показана по-английски, причина та же, что выше: либо ключа нет в файле этого языка, либо значение в нём оставлено английским.

**Покрытие ленты, публикаций, комментариев и соцсети.** Подписи, подсказки, заголовки колонок, пункты меню, кнопки и тексты диалогов подтверждения и уведомлений на экранах ленты, публикаций, комментариев и соцсети берутся из тех же ресурсных файлов и показываются на языке интерфейса пользователя: лента задачи и общая лента вместе с календарным видом, настраиваемыми элементами и виджетами, новостные карточки, публикации и их карточки, список публикаций, комментарии и лента комментариев, редактор комментария с панелью разметки и окном вложений, треды и их списки, опросы (создание, настройка, статистика), реакции на комментарии, а также карточки, панели и диалоги соцсети — профиль, сообщества, подписчики, администраторы, рекомендованные подписчики, подписки и панель подписки. В разметке подпись выводится пайпом `resx`, а подписи, которые компонент собирает в коде, читаются из словаря. Счётные подписи — число комментариев, голоса опроса, «и ещё N человек» — тоже берутся из словаря: числительное подставляется в значение ключа, а не склеивается строкой. Значения, уходящие на сервер, остаются литералами с пометкой о причине: так оставлены заголовок треда в запросе создания обсуждения и имя файла-вложения. Ключи заводятся в `ru-RU` и `en-US`, остальные культуры догоняет пакетный перевод словарей; на русском подписи совпадают с прежними до буквы.

**Подписи меню администрирования.** Меню админки — статический файл конфигурации: рядом с каждым пунктом хранится и прежний русский текст, и ключ ресурса, а на экран выводится значение ключа, поэтому подписи меню следуют языку интерфейса. Русская подпись остаётся на экране там, где ключа нет: если настройкой пути к панели администрирования подключён внешний файл без ключей, его пункты показываются по записанному в нём тексту.

**Страницы «Пользователи и права».** Карточка пользователя, включая вкладку «Сервис», настройки группы и редактор прав работают по тем же ресурсным файлам: заголовки вкладок и колонок, подписи кнопок и полей, тексты диалогов подтверждения и всплывающих уведомлений берутся по ключам и показываются на языке администратора. Значения, подставленные в такой текст — имя пользователя, прежний и новый SID, описание учётной записи, название орг. единицы, — вставляются по месту заполнителя при выводе диалога и разметкой не являются.

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

**Что на этих экранах не переводится.** Значения, которые форма получает с сервера — список типов сервисов, очереди и потоки, элементы справочников в выпадающих списках, — переводятся серверными ресурсами, а не файлами SPA, поэтому на английском интерфейсе могут оставаться русскими. Это ожидаемое состояние, а не ошибка.

**Страницы публикаций, универсальных шаблонов, индикаторов и сводных разделов.** Подписи этих страниц администрирования берутся из тех же ресурсных файлов SPA: заголовки и подписи кнопок, заголовки колонок гридов, подписи полей формы, названия вкладок и тексты диалогов подтверждения удаления выводятся по ключам на языке администратора. Заголовки, собираемые в коде, строятся после получения словаря, поэтому имя ключа на экран не попадает. Тексты сообщений об ошибках приходят с сервера и переводятся серверными ресурсами, а не файлами SPA.

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

**Экраны маршрутов, статусов и подписей.** Дизайнер маршрута категории, страница статуса и настройки подписей, включая подписи на переходах и настройки ЭЦП, берут подписи интерфейса из тех же ресурсных файлов: подписи полей, названия вкладок и пунктов списков, тексты диалогов подтверждения и удаления показываются на языке администратора. Поиск по выпадающему списку идёт по видимой подписи, поэтому на нерусском языке в списке набирают перевод, а не русское название.

**Экраны оргструктуры, иерархии задач и подзадач маршрутов.** Дерево оргструктуры и карточка орг. единицы, страница иерархии задач, настройка подзадач, создаваемых при переходе по маршруту, берут подписи из тех же ресурсных файлов: подписи кнопок и полей, названия вкладок, заголовки колонок гридов, пункты выпадающих списков, тексты диалогов подтверждения и уведомлений выводятся по ключам на языке пользователя. Подстановки в этих текстах — имя и идентификатор орг. единицы, название связанной группы, имя родительской категории — приходят с сервера и подставляются в значение ключа на месте заполнителя, поэтому строка переводится целиком, а заполнители переносятся в перевод без изменений. Заголовок окна редактирования орг. единицы собирается после получения словаря, поэтому имя ключа на экран не попадает.

Отдельно от подписей интерфейса хранятся описания настроек ЭЦП: в базе остаются языконезависимые маркеры вида `Attached`, `Detached`, `Smart: <выражение>`, `ExtParam: <ДП>`, `None`, а в таблице настроек они выводятся подписями текущего языка. Значения, записанные прежними версиями клиента по-русски, при выводе распознаются и показываются так же. Подписи этих экранов правятся ресурсными ключами, а не переименованием статусов, переходов или шаблонов подписей.

### Локализация подписей автоадминки

Названия форм автоадминки, названия их секций (вкладок), заголовки колонок и подсказки под полями переведены на общий контур локализации: связка в `dbadmin.AdminCaptionLocalizations` ведёт к объекту локализации `dbo.LocalizedBusinessObjects`, а текст по каждому языку лежит в `dbo.LocalizedBusinessObjectValues`. Значение выбирается по языку пользователя, и если перевода на этот язык нет — показывается русское. Так подписи читают и дерево форм в админке, и процедура, собирающая конфигурацию формы для грида (`dbadmin.DbaQueryFormSchema`).

Русское значение контура — эталонное. Оно поддерживается в актуальном состоянии при правке строки в самой админке: сохранение формы, секции или колонки записывает её `Caption` (для колонки — и `Notes`) как значение `ru-RU`, а удаление снимает связки элемента вместе с вложенными — форма уносит свои секции, секция — свои колонки. Английские значения заведены переносом из `CaptionAlt`/`NotesAlt`; сохранением строки они не обновляются — перевод на другой язык заводят и правят в самом контуре.

Описания объектов и колонок, пришедшие от самого источника данных, в контур не входят: таблицы описаний (`DbaObjects`, `DbaColumns`) зеркалят структуру базы и перезаписываются при её пересборке, поэтому перевод в них не сохранился бы. Переводятся только подписи, заданные на форме.

### Качество значения в ресурсном файле

Дефектом словаря считается не только пропущенный ключ, но и сам текст значения. Что проверяется при правке:

- **Знак препинания в конце.** Значение, заканчивающееся знаком, которого нет у того же ключа в соседних культурах, — обычно след строки, скопированной из кода; такой знак убирается. Сверка идёт с `ru-RU` и `en-US`.
- **Латинская буква внутри кириллического текста.** Смешанная запись не видна ни в редакторе, ни в сравнении версий: в подсказке о допустимых символах пароля диапазон «(а-я)» может начинаться латинской `a` (U+0061) вместо кириллической `а` (U+0430). Различает буквы только код символа. Обратный случай тоже дефект — кириллическая буква в латинском слове или в диапазоне «(a-z)».
- **Пробел перед знаком препинания.** «Сохранить изменения ?» — дефект: перед «?», «!», «:», «;» пробел не ставится. Исключение — значение, где знак сам является перечисляемым символом: в перечне допустимых символов пароля «?» стоит в списке, а не в конце фразы.
- **Двойной пробел между словами.** Два идущих подряд горизонтальных пробельных символа — обычный, неразрывный (U+00A0), узкий неразрывный (U+202F) — в интерфейсе видны как двойной промежуток. Это же касается стыка с французскими кавычками: между словом и « ставится один обычный пробел, а узкие неразрывные пробелы внутри кавычек сохраняются. Дефект возвращается незаметно — при пакетном переводе и при правке значения вручную, — поэтому однострочные значения словарей всех культур проверяет юнит-тест `resources-no-double-space.spec.ts`.
- **Пробелы по краям значения.** Для подписи, которую шаблон склеивает с соседним текстом или с числом, пробел по краю — часть значения, а не оформление: без него надпись слипается. Так, « набор элементов » и « из таблицы » несут пробел с обеих сторон; «Настройка маски », «Шаблон нумератора » и «Я принимаю условия » — в конце, перед следующим фрагментом; суффиксы длительности подзадач на диаграмме Ганта « дн.» и « ч.» — в начале, между числом и единицей. Эталон края — русское значение: пробел стоит там же, где он есть в `ru-RU`, а текст между краями при переводе не меняется. Исключение — китайский и японский: в этих языках слова пишутся без пробелов, и значение без краевого пробела для них верно. Там, где шаблон пробела не требует, пробел по краю — дефект: в интерфейсе виден двойной промежуток, поэтому край проверяется по шаблону потребителя, а не по словарю.
- **Подстановки.** Подстановка `${variable}` — часть значения, а не оформление: при переводе она переносится без изменений. Потерянная подстановка не косметика — пользователь не видит подставляемого значения, например имени столбца.
- **Спецсимволы языка.** Узбекская орфография требует знаков U+02BB (в буквах `o` и `g`) и U+02BC (разделитель); обычный апостроф вместо них — ошибка письма, а не вариант начертания.

- **Сырая обёртка табличного формата.** Значение, залитое в словарь пакетным импортом переводов из таблицы, может сохранить обёртку этого формата: на экране пользователь читает `"EP ""Data da"""` вместо `EP "Data da"`. В файле словаря значение хранится обычной строкой: внешние обрамляющие кавычки снимаются, удвоенные кавычки внутри становятся одиночными, а внутренние кавычки экранируются средствами языка, а не удвоением по правилам таблицы. Дефект возвращается с каждым пакетным импортом, поэтому значения всех культур проверяет юнит-тест `csv-quoted-resources.spec.ts`: он сверяет число кавычек с тем же ключом в `ru-RU` и считает дефектом только расхождение с источником — закавыченные слова, которых столько же и в русском значении, это смысл фразы, а не обёртка.

- **Значение, посимвольно равное английскому.** В машинной культуре строка, набранная буква в букву как в `en-US`, — не перевод, и запасной язык её не справит: ключ в файле есть, поэтому цепочка отката отдаёт именно это значение. На экране такой случай неотличим от пропущенного ключа — оба показывают английскую подпись, — но лечится иначе: правится само значение в файле культуры. Дефектом он бывает не всегда: в части культур английская форма законна — название продукта, аббревиатура, термин, устоявшийся в словарях этой культуры, — и тогда решение записывается в реестр подтверждений (см. подраздел «Как догоняются машинные культуры»). Очередь таких пар снимает `env/l10n/english-debt-scan.py`; правило `untranslated` линтера `env/l10n/lint.py` этот класс не ловит — оно сверяет значение с русским источником, а не с английским.

- **Метка порядка байтов в начале файла.** Ресурсный файл словаря хранится в UTF-8 без метки порядка байтов: файл начинается сразу с `export default {`. Невидимые байты в начале файла — не содержание словаря: они возвращаются в каждую ветку, трогающую словарь, дают лишний ханк и мешают слиянию, а редакторы, которые пишут метку сами, заносят её обратно при следующей правке файла. Снимая метку, ключи и значения не меняют.

Эти инварианты — знак в конце, латиница внутри кириллицы, пробел перед «?», сохранность подстановки, узбекские апострофы — закреплены юнит-тестом `localized-values.spec.ts` рядом со словарями: он перечисляет затронутые ключи поимённо и падает, если значение снова испортится. Края значений отдельно сверяет с русским словарём юнит-тест `resources-edge-space.spec.ts`, двойные пробелы внутри значения — `resources-no-double-space.spec.ts`.

### Серверные словари сообщений

Тексты, которые сервер отдаёт пользователю, лежат в 14 словарях `Language*.resx`: нейтральный файл с английскими значениями и по файлу на каждую культуру — `ru-RU`, `da-DK`, `de-DE`, `es-ES`, `fr-FR`, `it-IT`, `ja-JP`, `kk-KZ`, `pl-PL`, `tg-TJ`, `uz-Cyrl-UZ`, `uz-Latn-UZ`, `zh-CN`. Клиентские правила качества значения применимы к ним с одной поправкой: подстановка здесь `{0}`, `{1}`, а не `${variable}`.

| Правило | Как это выглядит на практике |
|---|---|
| Ключ есть во всех словарях | Добавили ключ в `ru-RU` — он должен появиться и в остальных тринадцати. Пропавшего ключа не бывает: непереведённый словарь отдаёт значение по умолчанию. |
| Непереведённое значение — английское | В `da-DK`, `es-ES`, `it-IT` и других непереведённых языках значение ключа английское. Это норма, а не ошибка. |
| В `ru-RU` значение до символа прежнее | Русский текст служит эталоном: при правке перевода русский вариант не трогают, а если трогают — только вместе с кодом. |
| Подстановки совпадают во всех культурах | Набор `{0}`, `{1}` в значении обязан совпадать с `ru-RU`. Перевод, потерявший `{0}`, ломает сообщение молча: номер задачи или имя файла просто исчезают. |
| Без кириллицы в непереведённых значениях | В переводимых словарях кириллицы быть не должно; документированных исключений — восемь. |
| Имя ключа и значение — буквами своего алфавита | Подменённые буквы ищет сторож, см. §7 «Подменённые буквы в ключах и подписях». |

Словари — часть сборки, поэтому перевод попадает на площадку только релизом: правка значения вручную на стенде затирается следующим обновлением.

### Как догоняются машинные культуры

Одиннадцать машинных словарей (`de-DE`, `es-ES`, `fr-FR`, `it-IT`, `ja-JP`, `kk-KZ`, `pl-PL`, `tg-TJ`, `da-DK`, `uz-Latn`, `zh-CN`) ветка не заполняет: правка заводит новый ключ только в `ru-RU` и `en-US`, а машинные значения приходят отдельным пакетным догоном. Догон — повторяемый цикл, а не разовая акция: очередь снимается заново по каждой ревизии `dev`, потому что обычные правки снова приносят ключи лишь в русский и английский словари.

**Две половины очереди.** Первая — пропуски: ключа в культуре нет, и цепочка отката отдаёт английскую подпись; её снимает `env/l10n/catchup-scan.py`, а вносит `env/l10n/catchup-apply.py` — только вставками, без изменения `ru-RU` и `en-US` и без перезаписи значения, которое в культуре уже есть. Вторая половина — долг: ключ есть, а значение посимвольно равно `en-US`, поэтому запасной язык не работает и подпись останется английской навсегда; её снимает `env/l10n/english-debt-scan.py`, а `env/l10n/english-debt-apply.py` заменяет только те строки, чьё текущее значение совпадает с английским. Правило `untranslated` линтера `env/l10n/lint.py` долг не видит: оно сверяет значение с русским источником, а не с `en-US`, поэтому одиночное латинское слово при кириллическом русском проходит линтер молча.

**Реестр подтверждений.** Часть английских значений законна — название продукта, аббревиатура, термин, уже устоявшийся в словарях этой культуры. Такое решение записывается в `env/l10n/confirmed-english.tsv` парой «культура — ключ», и проверка долга считает пару закрытой; пока записи нет, пара возвращается в очередь на следующем круге. Пример защитимой формы — `da-DK navigationAppearanceLogoUpload = 'Upload logo'`: словарь `da-DK` системно пишет `Upload` как глагол (`Upload fil`, `Upload en avatar`), то есть форма следует норме самой культуры, а не остаётся недопереводом.

**Пакетный импорт вносит собственные дефекты.** Обрезанный крайний пробел у разделителя списка: `control-dropdown` склеивает выбранные значения этим разделителем, и без пробела список рисуется как `a,b` вместо `a, b`. Многословная копия английского среди внесённых значений. Ключи вида `luaDoc.RESULT.descr`, записанные в словаре свойствами в кавычках, должны читаться как строки. Всё это перечисляет поимённо и проверяет спецификация `apps/spa/src/app/localization/l10n-dict-catchup.spec.ts` рядом со словарями: новый класс дефекта пакетного импорта лечится новым тестом рядом, а не правкой перевода на месте.

**Остаток догона — следующая очередь, а не дефект ветки.** Прогон закрывает очередь на момент своей базы: ключи, приехавшие в русский словарь позже замера, в панель перевода не попадали и закрываются следующим прогоном. Готовая работа при переносе на свежую базу не патчится — сдвиг строк в словарях ломает патч, и переезд выглядит пустым диффом; восстанавливать догон надо из таблицы результатов панели («ключ, язык, итоговое значение, исходная строка»), применяя её к дереву поимённо.

## 6. Диагностика локализации

**Проверка наличия локализации у сущности**

```sql
-- Business-объект (например категория)
SELECT s.SubcatID, s.Description, s.LocalizedDescriptionId,
       bov.LanguageId, bov.Value
FROM Subcategories s
LEFT JOIN LocalizedBusinessObjectValues bov
  ON bov.LocalizationId = s.LocalizedDescriptionId
WHERE s.SubcatID = @id

-- Data-объект (например пользователь)
SELECT u.UserID, u.LastName, u.LocalizedLastNameId,
       dov.LanguageId, dov.Value
FROM Users u
LEFT JOIN LocalizedDataObjectValues dov
  ON dov.LocalizationId = u.LocalizedLastNameId
WHERE u.UserID = @id
```

**Проверка локализации значения текстового ДП**

```sql
SELECT v.TaskID, v.ExtParamID, v.LocalizedExtParamValueId,
       dov.LanguageId, LEFT(dov.Value, 100) AS Value
FROM ExtParamValues v
LEFT JOIN LocalizedDataObjectValues dov
  ON dov.LocalizationId = v.LocalizedExtParamValueId
WHERE v.TaskID = @taskId AND v.ExtParamID = @epId
```

**Записи локализации без ссылки (сироты)**

```sql
SELECT COUNT(*) AS orphan_values_cnt,
       SUM(CAST(DATALENGTH(dov.Value) AS bigint)) / 1048576 AS orphan_mb
FROM LocalizedDataObjects lo WITH (NOLOCK)
JOIN LocalizedDataObjectValues dov WITH (NOLOCK) ON dov.LocalizationId = lo.Id
WHERE NOT EXISTS (SELECT 1 FROM ExtParamValues v WITH (NOLOCK) WHERE v.LocalizedExtParamValueId = lo.Id)
  AND NOT EXISTS (SELECT 1 FROM Tasks t WITH (NOLOCK)
                  WHERE t.LocalizedTextId = lo.Id OR t.LocalizedDescriptionId = lo.Id)
  AND NOT EXISTS (SELECT 1 FROM Users u WITH (NOLOCK)
                  WHERE u.LocalizedLastNameId = lo.Id OR u.LocalizedFirstNameId = lo.Id
                     OR u.LocalizedMiddleNameId = lo.Id)
  AND NOT EXISTS (SELECT 1 FROM OrgStructureUnit o WITH (NOLOCK) WHERE o.LocalizedNameId = lo.Id)
```

Запрос перечисляет все целевые таблицы Data-объектов (`ExtParamValues`, `Tasks`, `Users`, `OrgStructureUnit`) — без проверки `Tasks`, `Users` и `OrgStructureUnit` в счёт попадали бы живые локализации текстов и описаний задач, имён пользователей и названий подразделений. `COUNT(*)` считает строки значений (по одной на язык), а не локализации.

Сироты этого класса появляются, если значение текстового ДП переписывалось серверным путём на версиях 2.263.412–2.268 (см. [business.md](https://help.1forma.ru/domains/localization/business.md) раздел 5): прежние версии значения остаются в таблице без ссылок, занимают место и читаются при полной загрузке `LocalizationCache`. Удалять найденное вручную нельзя: у `LocalizedDataObjectValues` связь с `LocalizedDataObjects` объявлена `CASCADE DELETE`, а колонки-ссылки в целевых таблицах — `ON DELETE SET NULL`, поэтому удаление живой записи `LocalizedDataObjects` уносит её значения и обнуляет ссылку, то есть локализация текста пропадает. Чистка выполняется скриптом разработки.

**Проверка языков**

```sql
SELECT ID, LangAlias, LangDescr, IsDefault, IsInternational, culture, LanguageCode
FROM Languages
ORDER BY ID
```

**Отображаемое имя пользователя не совпадает с данными карточки**

Отображаемое имя — в подписях, полях выбора пользователя и результатах поиска — берётся не из `Users.*`, а из денормализованной таблицы `dbo.UserNameModes`: строка на каждую пару «режим имени + язык». Поэтому сверяют два уровня — локализованные ФИО пользователя и пересобранные по ним отображаемые имена:

```sql
-- локализованные ФИО по языкам
SELECT u.UserID, u.LastName, u.LocalizedLastNameId,
       dov.LanguageId, dov.Value
FROM Users u
LEFT JOIN LocalizedDataObjectValues dov
  ON dov.LocalizationId = u.LocalizedLastNameId
WHERE u.UserID = @id

-- отображаемые имена по языкам
SELECT UserNameMode, LanguageID, UserID, DisplayName
FROM dbo.UserNameModes
WHERE UserID = @id
ORDER BY LanguageID, UserNameMode
```

Как читать результат: если для недефолтного языка в `LocalizedDataObjectValues` лежит прежнее значение, прежним останется и отображаемое имя на этом языке — пересборка имён читает фамилию недефолтного языка именно из локализации. Расхождение в самой локализации даёт ручной перевод, который система не перезаписывает: сохранение карточки обновляет локализованные ФИО для языка оператора и для языков, где значение совпадало с прежними данными пользователя (см. [business.md](https://help.1forma.ru/domains/localization/business.md), раздел «Локализованные ФИО пользователя: автокопия и ручной перевод»). Если локализация верна, а `DisplayName` в `dbo.UserNameModes` прежние, пересборка имён ещё не выполнялась либо запись кэша отдаётся из прежнего состояния — кэш имён инвалидируется не на всех путях пересборки.

**В журнале записи о пропущенном переводе — это не ошибка**

Фоновая задача перевода пишет в журнал о пропуске, когда писать нечего: сущность удалена, переводы удалены осознанно или сущность недоступна. Такие записи — ожидаемый результат, а не сбой: обработка завершается штатно, в базе ничего не меняется. Уровень записи — предупреждение, у записи о реальном падении — ошибка, и к ней приложены тип и идентификатор сущности.

| Запись журнала | Когда | Что произошло |
|---|---|---|
| `Skip background localization translate: entity was deleted` | до или после вызова перевода | Сущность удалили, перевод не выполнялся или его результат не записан |
| `Skip background localization translate: localization was removed` | перед записью | У сущности обнулили ссылку на объект локализации — переводы удалили осознанно |
| `Skip localization values merge: LocalizedBusinessObjects {id} is missing` | в момент вставки | Родительская строка исчезла между проверкой и вставкой |
| `Background localization translate failed` | при любом другом сбое | Настоящее падение: к записи приложены тип и идентификатор сущности, разбирать как обычную ошибку |

**Кеш не обновился после прямой записи в БД**

Прямая запись в `LocalizedXxxObjectValues` через SQL не инвалидирует кеш. Решения:
1. Рестарт AppPool
2. Использовать API вместо прямой записи

**После очистки кешей подпись на исходном языке, хотя перевод сохранён**

Очистка кешей не меняет ни переводы, ни то, что видит пользователь. Если после неё подпись показана на исходном языке, сначала проверьте окно локализации сущности: значение на месте — это дефект чтения, повторно вводить перевод не нужно. Если окно пустое, сверьтесь с таблицей значений по SQL выше: строки есть — потеряна ссылка сущности на перевод, строк нет — перевод не сохранён. Случай заводится задачей.

**Нода стартует долго из-за локализации**

Полная загрузка кэша локализации читает таблицу значений целиком, поэтому время старта растёт с её объёмом. Оценить объём:

```sql
-- объём значений локализованных данных
SELECT COUNT(*)                            AS rows_total,
       SUM(DATALENGTH(Value)) / 1048576.0  AS total_mb,
       SUM(CASE WHEN DATALENGTH(Value) > 8000
                THEN 1 ELSE 0 END)         AS over_8kb,
       MAX(DATALENGTH(Value)) / 1024.0     AS max_kb
FROM dbo.LocalizedDataObjectValues
```

`over_8kb` — значения, не влезающие в один пакет TDS: именно на них асинхронное чтение деградировало, поэтому полная загрузка выполняется синхронно. Если строк миллионы, а объём измеряется гигабайтами, загрузка на старте занимает минуты; ручное обновление кэшей в этот период не применяется по пределу `CacheFullUpdateTimeoutSeconds`.

#### Что заведомо не переводится

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


## 7. Подменённые буквы в ключах и подписях

Кириллическая и латинская буквы, совпадающие начертанием, на экране неразличимы, но в коде, в ключе настройки и в базе это разные символы. Частые пары: `а`/`a`, `с`/`c`, `е`/`e`, `о`/`o`, `р`/`p`, `х`/`x`, `у`/`y` и заглавные к ним. Проверку значений клиентских словарей описывает раздел «Качество значения в ресурсном файле» выше; здесь — серверные ключи и совместимость.

Как это проявляется: ключ перестаёт совпадать с тем, что ищет потребитель, и значение не подставляется, а флаг молча игнорируется; подпись или запись системного перечня не находится поиском по правильному слову, хотя на экране выглядит обычно.

Порядок действий:

1. **Ключ, имя поля, имя модуля, JSON-свойство — только латиница.** Ключ переименовывается сразу во всех языках, в сгенерированных файлах ресурсов и во всех местах вызова: иначе значение перестанет находиться у потребителя, который запрашивает новое имя.
2. **Если по имени уже есть сохранённые данные — совместимость, а не перевыпуск.** Старое написание остаётся читаемым: настройки, сохранённые до исправления, применяются как раньше, а новые сохранения пишутся латинским ключом; выданные ранее лицензии распознаются наравне с новыми, потому что код читает оба написания.
3. **Текст, лежащий в базе** — подписи администрирования, описания системных перечней, локализованные значения — правится новой парной миграцией для обеих баз. Уже накатанную миграцию править нельзя: накатчик выбирает скрипт по ключу, а не по содержимому, поэтому изменённый текст не доедет туда, где скрипт однажды применился.
4. **За подменой в коде следит тест-страж:** новое слово с подменённой буквой вне перечня законных случаев роняет сборку и называет файл, строку и слово. Под проверку попадают и исходники надстройки Outlook, которая поставляется с 1Формой; сторонние библиотеки и результаты сборки надстройки — зависимости (`node_modules/`), каталог сборки (`dist/`) и замок зависимостей (`package-lock.json`) — из проверки исключены.

## 8. Перевод базы на Unicode

Колонки базовых таблиц MS SQL переводятся на `nvarchar` версионированными миграциями, и накат управляется рубильником — строкой `dbo.SettingsCustom` с ключом `UnicodeConversion` и пустым `UserId`.

**Если накат прошёл при закрытом рубильнике.** Скрипт ничего не изменил и записал в журнал наката строку `UNICODE-SKIP`, а накатчик считает его выполненным и повторно не запускает. База при этом остаётся на ANSI-колонках: чтобы перевести её после открытия рубильника, нужен ручной прогон скриптов перевода. Перед этим стоит убедиться, что рубильник открыт: проверка стоит первой инструкцией каждого пакета скрипта, поэтому части применяются согласованно.

**Проверка, что база переведена.** Типы колонок:

```sql
SELECT OBJECT_NAME(c.object_id) AS TableName, c.name AS ColumnName,
       t.name AS TypeName, c.max_length
FROM sys.columns c
JOIN sys.types t ON t.user_type_id = c.user_type_id
WHERE c.name IN ('DisplayName', 'Description', 'Content', 'ExtParamValue',
                 'ExtParamOldValue', 'ExtParamValueForSort', 'Trigram')
  AND t.name NOT IN ('nvarchar', 'nchar')
```

Пустой результат — все перечисленные колонки уже Unicode. Состояние рубильника:

```sql
SELECT [Key], [Value] FROM dbo.SettingsCustom WHERE [Key] = 'UnicodeConversion'
```

**Порядок работ.** Сначала переводятся базовые колонки, последним шагом пересоздаются денормализованные таблицы и выставляется флаг `IsDenormalizationUC`: они хранят копии текстов, поэтому читать уже переведённые значения могут только после того, как колонки-источники стали Unicode.

**Пока база не переведена.** Текст вне кодовой страницы 1251 заменяется на `?` при записи — это касается и значений, попадающих в поиск и денормализацию. Поэтому база без перевода не считается локализованной: подписи интерфейса могут быть переведены, а данные — нет. Колонки PostgreSQL переводить не нужно.

**Связанные документы:**

- [Системные настройки](https://help.1forma.ru/domains/system/admin.md) — пакетный перевод
