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

Локализация — бизнес-правила

Бизнес-правила локализации 1Формы: роли языков и fallback-цепочка при чтении, разделение Business/Data объектов, nullable FK-паттерн, автоперевод, кеш, права, экспорт/импорт и известные ограничения. Для администраторов и разработчиков при настройке и сопровождении мультиязычной конфигурации.

1. Язык по умолчанию и международный язык

В системе ровно один язык помечен IsDefault=1 и ровно один — IsInternational=1. Гарантируется уникальными filtered-индексами на Languages.

Типичная конфигурация: - Default: Русский (ID=1) - International: English (ID=2)

Роль Флаг Назначение
Default IsDefault=1 Язык, на котором хранятся исходные значения. Fallback третьего приоритета при чтении
International IsInternational=1 Международный язык. Fallback второго приоритета при чтении
User language Users.LanguageID Язык пользователя. Первый приоритет при чтении

При получении локализованного значения (GetValue) система проверяет языки в порядке:

  1. Язык пользователя (Users.LanguageID)
  2. Международный язык (IsInternational=1)
  3. Язык по умолчанию (IsDefault=1)
  4. null — используется исходное (нелокализованное) значение из целевой таблицы

Система не перебирает все доступные языки — только три приоритетных. Если значения нет ни для одного из них, возвращается null.

Тот же порядок отката действует и для SQL-читателей локализованных значений: его реализует функция dbo.fn_AllLocalized_DataLocalizedDescriptionEx, и на ней держатся отображаемые имена пользователя (dbo.UserNameModes).

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

Описанный порядок относится к локализованным значениям сущностей. У статических подписей веб-интерфейса свой порядок отката — см. Локализация — Администрирование. Шаблоны системных комментариев ленты (dbo.EventTypeTemplates) — третья ось, самая короткая: язык читателя, а при отсутствии шаблона — сразу язык по умолчанию, международный не подставляется. Устройство выбора шаблона и кеша — comments/business.md, раздел «Системные типы».

Выбор языка интерфейса и язык профиля

Язык интерфейса пользователь выбирает в настройках профиля (поле «Язык»). Выбранное значение хранится в профиле (Users.LanguageID) и после входа является единственным источником языка интерфейса: обновление страницы, вход из другого браузера или с другого устройства язык не меняют, а смена языка, сделанная из мобильного приложения или администратором в карточке пользователя, видна при следующей загрузке страницы.

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

Язык профиля меняется только явным выбором самого пользователя: браузер язык профиля не переписывает, поэтому перевоплощение в другого пользователя язык его профиля не меняет.

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

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

Культура пользователя: форматирование и локальные словари

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

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

  • формат дат в ленте, карточке задачи и журнале подписей;
  • формат дат на клиенте собирает общий пайп: он кэширует форматтеры по ключу «локаль + опции», поэтому смена языка интерфейса переформатирует даты, а разделители дней в списке чатов и лентах не создают форматтер заново на каждом проходе обнаружения изменений — опции форматирования передаются стабильным кортежем (см. Фронтенд чата, раздел «Форматирование дат в списке чатов»);
  • формат чисел и денежных сумм, включая разделители и набор валют (числа считает штатный механизм браузера Intl.NumberFormat по культуре);
  • служебные подписи таблиц — меню столбцов, фильтры, пагинация, агрегаты (словарь ag-grid-<культура>);
  • заголовок окна журнала подписей и подписи его колонок.

Эти надписи и форматы следуют культуре пользователя, а не закреплены за русским языком: при культуре, отличной от ru-RU, меню таблиц, журнал подписей, даты и числа показываются на языке пользователя и в его формате.

Отображаемый текст не участвует в логике

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

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

Язык по умолчанию — источник языка и для системных записей, у которых нет известного языка автора: например, запись истории правок при холодной перекомпиляции смарт-скрипта берётся по нему (см. C# (Roslyn) — типизированные смарт-скрипты, подраздел «Культура записи фоновой компиляции»).

Язык на старте приложения. Пока пользователь не вошёл, язык интерфейса определяется на стороне браузера. После входа язык приходит из данных сессии (culture в ответе /api/auth/info), а если это значение не заполнено — используется язык браузера. Предвыбор языка на форме входа, сделанный до входа, переносится в профиль однократно (см. auth/business.md).

2. Двухуровневая модель: Business vs Data

Локализация разделена на два типа объектов с разными характеристиками.

Свойство Business Data
Сущности Категории, статусы, ДП, подписи, кнопки, меню ФИО пользователей, тексты задач, значения ДП, орг. единицы
Кто редактирует Администратор Пользователи (через UI/SmartAction)
Экспорт/импорт Да, по GUID (ID перегенерируется) Нет
Автоперевод Да (Azure Translate) Нет
Денормализация Нет (из кеша) Да (Task -> TasksInSubcat)
Fulltext search Нет Да (LocalizedDataObjectValues.Value)

Каждая локализуемая сущность имеет nullable FK-колонку (LocalizedLastNameId, LocalizedDescriptionId и т.д.).

Правила nullable FK (LocalizedXxxId): - Колонка = NULL означает, что сущность не имеет локализации - При первом Merge создаётся запись в LocalizedXxxObjects, FK заполняется - При удалении локализации (Delete) FK обнуляется - FK не имеет CASCADE DELETE в сторону целевой таблицы — удаление сущности не удаляет локализацию автоматически (осиротевшие записи)

3. Entity Type Mapping

93 значения в LocalizationEntityType enum. Каждое значение маппится на: - C# тип сущности (Entity) - Имя FK-колонки (PropertyName) - Тип объекта (Business / Data) - Имя исходного поля (OriginalValuePropertyName)

Маппинг выполняет singleton LocalizationMappingService.

Ключевые entity types:

ID Enum ObjectType Сущность
1 Category Business Раздел
2 Subcategory Business Категория
3 SubcategoryEntity Business Поле Entity категории
4 StateRouteInSubcat Business Переход по маршруту
5 State Business Статус
6 ExtParam Business ДП
7 ExtParamBlock Business Блок ДП
8 Signature Business Подпись
9 SignatureResolutionType Business Резолюция подписи
10 Task Data Текст задачи
11 UserLastName Data Фамилия
12 UserFirstName Data Имя
13 UserMiddleName Data Отчество
23 ExtParamOption Business Значение комбобокса
30 OrgStructureName Data Название орг. единицы
39 TextExtParamValues Data Значения текстовых ДП

Локализованные ФИО пользователя: автокопия и ручной перевод

Локализованные ФИО (UserLastName, UserFirstName, UserMiddleName) хранятся отдельно по каждому языку. Значения делятся на два вида:

  • Автокопия — локализованное значение совпадает с прежним значением в карточке пользователя (Users.LastName / FirstName / MiddleName) на момент сохранения. Такие значения система поддерживает актуальными сама.
  • Ручной перевод — значение отличается от прежнего, то есть введено человеком. При сохранении карточки такие значения не изменяются.

Правило. Сохранение карточки пользователя обновляет локализованные ФИО не только для языка текущей сессии оператора, но и для каждого языка, где локализованное значение совпадает с прежним Users.* (автокопия). Языки с ручным переводом остаются без изменений. Признак «введено человеком» в схеме не хранится — разделение делается сравнением с прежним значением в момент сохранения. Если прежнее значение пустое, совпавшими считаются все незаполненные языки: при первом заполнении ранее пустого поля локализованные значения заводятся по всем языкам.

То же правило действует при переименовании через AD-синхронизацию, интеграцию с 1С и смарт-действие «Обновить пользователя»: эти пути передают прежние ФИО явно, из снимка до записи.

Значения, разошедшиеся с прежним Users.* раньше (и потому считающиеся ручным переводом), автоматически не чинятся — для них нужен отдельный перенос.

4. Автоперевод, логирование и кеширование

Правила автоперевода (Azure Translate): - Применяется только к Business-объектам (конфигурация) - Переводятся только языки, для которых нет значения (Value = NULL или пусто) - Уже заполненные значения не перезаписываются - Если значение для языка пользователя не изменилось и все языки заполнены — перевод не запускается - Выполняется асинхронно (AsyncRunner.Run) — UI не блокируется - Если Azure Translate недоступен — значение записывается только для языка по умолчанию - Удаление сущности во время фонового перевода не даёт ошибки и не оставляет переводов без родителя: результат перевода не записывается, объект локализации заново не заводится - Если переводы живой сущности удалили осознанно, автоперевод по изменению наименования их не возвращает: фон отличает пустой ключ «переводов ещё не было» от обнулённого «переводы удалили» - Перевод выполняется только после сохранения изменений в базе: откат транзакции сохранения оставляет сущность без перевода

Изменения логируются для: - OrgStructureName — через Admin.LogAction (полный diff: язык, старое/новое значение)

Для остальных типов сущностей отдельного лога изменений локализации нет.

  • LocalizationCache — ключ: ComplexKey(EntityId, EntityType), значение: List<LocalizedValueDto>
  • Инвалидируется при каждом Merge (точечно, по ключу)
  • Полная загрузка при старте: все Business + Data значения
  • Прямая запись в БД не инвалидирует кеш — это документированное ограничение

5. Права доступа, экспорт/импорт и known issues

LocalizationCheckPermissionsService проверяет: - При чтении: доступ к сущности (CheckPermissions) - При записи: доступ к редактированию сущности (CheckEditPermissions)

Проверка привязана к целевой сущности (категория, пользователь и т.д.), не к записям локализации напрямую.

  • LocalizedBusinessObjects.Guid используется для перемаппинга FK при импорте
  • Id (PK) перегенерируется при импорте (не входит в KeepIdentity)
  • Двухфазный процесс: сначала импорт объектов с новыми ID, затем перемаппинг FK по GUID
  • Data-объекты не экспортируются (нет Guid в LocalizedDataObjects)
  • Перенос между площадками. Номера языков в справочнике Languages назначает база (IDENTITY), поэтому на разных площадках один номер означает разные языки. Утилита переноса выгружает справочник языков источника в _meta/languages.json и на импорте подставляет номер того же языка на приёмнике: сначала по коду LangAlias, при несовпадении — по культуре. Если языка на приёмнике нет или его код неоднозначен, локализованное значение не импортируется и попадает в отчёт переноса; чужой язык не подставляется. Корневая сущность со ссылкой на язык при этом сохраняется: необязательная ссылка обнуляется, обязательная получает язык приёмника по умолчанию. Архивы без файла языков разбираются по номеру, как раньше, с предупреждением в журнале

Ключ и подпись пишутся буквой своего алфавита. Ключ ресурса, имя настройки и свойство в структуре данных — латиница; текст подписи — буквы того языка, на котором она написана. Буква другого алфавита, совпадающая начертанием, отличает ключ от правильного только для глаза: потребитель по имени его не находит, а текст не находится поиском. Если значение с верным, на вид, написанием поиском не находится, строку проверяют посимвольно: в ней может стоять буква другого алфавита. Исправляют символ в том значении, которое участвует в поиске; намеренно смешанные строки тестовых данных не трогают, если они проверяют обработку такого входа. После правки убеждаются, что запись находится по обычному написанию, а смысл отображаемого текста не изменился. Поиск по набору контентных иконок устроен так же: сопоставление идёт по буквальному составу имени и тегов.

Known issues:

  1. SmartAction CreateUser/UpdateUser записывает ФИО только для одного языка — того, что задан параметром действия; параметров для мультиязычного ФИО нет. Распространение по языкам-автокопиям выполняется при сохранении карточки пользователя (см. «Локализованные ФИО пользователя: автокопия и ручной перевод»).
  2. Группировка в гриде по локализованным ДП не применяет локализацию (заголовки групп на языке денормализации).
  3. Осиротевшие записи в LocalizedXxxObjects/Values при удалении целевой сущности — нет CASCADE DELETE от целевой таблицы к таблицам локализации.

  4. Осиротевшие записи при перезаписи значения локализуемого текстового ДП. В версиях с 2.263.412 по 2.268 серверное изменение значения ДП (смарт-действие, API, синхронизация, импорт) обнуляло ссылку на локализацию: на каждую перезапись заводилась новая запись, прежняя оставалась без ссылок. Исправлено в 2.268. Накопленные сироты зачищаются штатной миграцией релиза — порциями с защитным лимитом в 50 000 строк; на базе, где сирот больше, очистка выполняется отдельным DBA-скриптом. Поиск — admin.md раздел 6; вручную не удалять.

  5. Удалённые переводы возвращаются при изменении наименования в узком окне. Фон проверяет сущность и её переводы до записи, но между проверкой и записью остаётся окно: если переводы живой сущности удалили именно в нём, автоперевод заведёт объект локализации заново и вернёт только что удалённые значения. Так же не подхватывается замена ключа локализации при изменении наименования. Ограничение принято осознанно; ветка пользовательских данных (в отличие от конфигурационных объектов) такой проверкой не защищена.

  6. Проверка непустоты значений под collation. Под collation Cyrillic_General_CI_AS символы без веса (казахская буква, эмодзи) сравниваются с пустой строкой наравне, поэтому проверка непустоты сравнением со строкой — x > '', x <> '', x = '' — считает заполненное значение пустым. Проверка через len(x) > 0 от collation не зависит и безопасна. На клиентских данных класс виден так: задача, у которой текстовый ДП заполнен одним символом вне cp1251, не находится фильтром грида «значение не пусто».

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

6. Подписи интерфейса и значения данных

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

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

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

Сообщения сервера переводятся так же. Подписи интерфейса — не единственный текст, который локализуется. Ошибки и уведомления серверной части берутся из словарей, а не набираются русской строкой в коде, — это касается не только API, но и сервисов администрирования, интеграции, скриптов и работы с документами. Непереведённый словарь отдаёт английское значение по умолчанию; если сообщение сервера показывается не на языке интерфейса, значит для этого языка перевода нет, а не то, что текст не локализуется вовсе.

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