Локализация — бизнес-правила¶
Бизнес-правила локализации 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) система проверяет языки в порядке:
- Язык пользователя (
Users.LanguageID) - Международный язык (
IsInternational=1) - Язык по умолчанию (
IsDefault=1) - 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:
- SmartAction CreateUser/UpdateUser записывает ФИО только для одного языка — того, что задан параметром действия; параметров для мультиязычного ФИО нет. Распространение по языкам-автокопиям выполняется при сохранении карточки пользователя (см. «Локализованные ФИО пользователя: автокопия и ручной перевод»).
- Группировка в гриде по локализованным ДП не применяет локализацию (заголовки групп на языке денормализации).
-
Осиротевшие записи в
LocalizedXxxObjects/Valuesпри удалении целевой сущности — нет CASCADE DELETE от целевой таблицы к таблицам локализации. -
Осиротевшие записи при перезаписи значения локализуемого текстового ДП. В версиях с 2.263.412 по 2.268 серверное изменение значения ДП (смарт-действие, API, синхронизация, импорт) обнуляло ссылку на локализацию: на каждую перезапись заводилась новая запись, прежняя оставалась без ссылок. Исправлено в 2.268. Накопленные сироты зачищаются штатной миграцией релиза — порциями с защитным лимитом в 50 000 строк; на базе, где сирот больше, очистка выполняется отдельным DBA-скриптом. Поиск — admin.md раздел 6; вручную не удалять.
-
Удалённые переводы возвращаются при изменении наименования в узком окне. Фон проверяет сущность и её переводы до записи, но между проверкой и записью остаётся окно: если переводы живой сущности удалили именно в нём, автоперевод заведёт объект локализации заново и вернёт только что удалённые значения. Так же не подхватывается замена ключа локализации при изменении наименования. Ограничение принято осознанно; ветка пользовательских данных (в отличие от конфигурационных объектов) такой проверкой не защищена.
-
Проверка непустоты значений под collation. Под collation
Cyrillic_General_CI_ASсимволы без веса (казахская буква, эмодзи) сравниваются с пустой строкой наравне, поэтому проверка непустоты сравнением со строкой —x > '',x <> '',x = ''— считает заполненное значение пустым. Проверка черезlen(x) > 0от collation не зависит и безопасна. На клиентских данных класс виден так: задача, у которой текстовый ДП заполнен одним символом вне cp1251, не находится фильтром грида «значение не пусто». -
Расхождение колонки значения и локализации у локализуемого текстового ДП. Пока у текстового ДП включена настройка «Локализуемый», значение хранится дважды: в колонке значения (
ExtParamValues.ExtParamValue) и в записи локализации. Раньше очистка HTML стояла ниже записи в колонку и работала только на ветку локализации, поэтому копии расходились: в локализации лежал очищенный текст, а в колонке — неочищенный. Пользователь видел очищенный текст (карточка локализует значение), а денормализованная копия для списков и гридов заполнялась из колонки как есть и расходилась с экраном; её читают грид, теги по значению, лукап и поиск по значению, и именно она становится видимой, когда настройку снимают. Исправлено в 2.268: очистка HTML выполняется один раз до записи, поэтому обе копии получают один и тот же очищенный текст. Правило действует для ДП типа «Большой текст с форматированием»; у типов «Большой текст без форматирования» и «Текст» сущности снимает своя логика типа. Уже сохранённые значения не перезаписываются.
6. Подписи интерфейса и значения данных¶
Переводу подлежат подписи: заголовки экранов и вкладок, названия колонок и кнопок, тексты диалогов и уведомлений. Значения данных — то, что администратор вводит и что сохраняется как содержимое сущности, — от языка интерфейса не зависят и показываются так, как записаны.
Пример: имя индивидуальной группы на карточке пользователя подставляется в редактируемое поле заготовкой вида «Индивидуальная Фамилия Имя». Это заготовка названия группы, а не подпись интерфейса, поэтому она одинакова при любом языке администратора. Названия разделов, категорий и статусов — тоже значения: в интерфейсе они показываются в переводе, который хранится отдельной локализованной записью и подставляется механизмом локализации сущностей.
Значения, уходящие на сервер, тоже не переводятся. Не каждый русский литерал в интерфейсе — текст для пользователя. Позиция, которая не является подписью, подсказкой, заголовком, пунктом меню или кнопкой, остаётся литералом: это значение, уходящее на сервер, ключ данных или текст журнала. В коде такие позиции помечают с указанием причины, ключ ресурса для них не заводят — иначе на сервер уйдёт подпись вместо значения. Сравнения по русскому тексту в условиях при переводе интерфейса не переписывают: сохранённый в настройке русский текст остаётся контрактом данных.
Сообщения сервера переводятся так же. Подписи интерфейса — не единственный текст, который локализуется. Ошибки и уведомления серверной части берутся из словарей, а не набираются русской строкой в коде, — это касается не только API, но и сервисов администрирования, интеграции, скриптов и работы с документами. Непереведённый словарь отдаёт английское значение по умолчанию; если сообщение сервера показывается не на языке интерфейса, значит для этого языка перевода нет, а не то, что текст не локализуется вовсе.
Проверять, на каком языке пришло сообщение, нужно по языку профиля: сообщение сервера уходит на языке интерфейса, а не языке браузера. Если интерфейс русский, а текст ошибки английский — перевода для этого языка пока нет; это ожидаемо и исправляется переводом на сервере, а не настройкой у пользователя. Часть сообщений сознательно остаётся технической и на язык не переводится: например, жалоба на неверный параметр приходит с кодом error_IncorrectParameterValue и показывает то, что прислал запрос. Служебные надписи — логи, отчёт о выгрузке, вывод утилит миграции — это не сообщения пользователю, они и на русском остаются по замыслу.