Локализация в СУБД Caché

    Предположим, вы написали программу, выводящую «Hello, World!», например:
      write "Hello, World!"

    Приложение работает, всё хорошо.
    Но проходит время, ваше приложение развивается, становится популярным и вот, вам нужно эту строку вывести уже на другом языке, причём количество и состав требуемых языков заранее неизвестен.
    Под катом вы узнаете, как задача локализации решается в Caché.


    Краткий обзор


    В СУБД Caché предусмотрен готовый механизм, упрощающий локализацию строк в консольных программах, интерфейса в веб-приложениях, строк в файлах JavaScipt, сообщений об ошибках и т.д.
    Примечание: Данная тема была рассмотрена вскользь в одной из предыдущих статей.

    Допустим, имеется проект со множеством классов, программ, веб-страничек, js-скриптов и т.д.
    Работает механизм локализации следующим образом:
    1. ещё на этапе компиляции проекта «выуживаются» все строки, подлежащие локализации, и сохраняются внутри базы в определённом формате.
    2. в сам откомпилированный код вместо самих строк подставляется определённый код, который уже на этапе выполнения будет в зависимости от текущего языка сессии выдавать из хранилища то или иное значение.

    Весь процесс локализации полностью прозрачен для программиста.
    Разработчик избавляется от необходимости ручного заполнения некоего хранилища строк (таблицы в БД или ресурсного файла), а также от написания кода по управлению всей этой инфраструктурой, как то: смена языка во время исполнения, экспорт/импорт данных в различные форматы для переводчика и т.д.

    В итоге мы имеем:
    1. читаемый — незагромождённый лишним — исходный код;
    2. автоматически наполняемое хранилище локализуемых строк;
      Примечание: При удалении строк из кода из хранилища они не удаляются. Для очистки хранилища от таких фантомов проще его очистить и заново перекомпилировать проект.
    3. смену текущего языка «на лету». Это касается как веб-приложений, так и обычных программ;
    4. возможность получить строку на заданном языке, из заданного домена (о доменах чуть ниже);
    5. готовые методы по экспорту/импорту хранилища в XML.

    Итак, давайте рассмотрим детальнее, как это работает, а также всевозможные примеры по локализации.

    Введение


    Создадим MAC-программу следующего содержания:

    #Include %occMessages
    test() {  
      
      
    write "$$$DefaultLanguage=",$$$DefaultLanguage,!
      
    write "$$$SessionLanguage=",$$$SessionLanguage,!
      
      
    set msg1=$$$Text("Привет, Мир!","asd")
      
    set msg2=$$$Text("@my@Привет, Мир!","asd")
      
    write msg1,!,msg2,!

    }


    Результат:
    USER>d ^test
    $$$DefaultLanguage=ru
    $$$SessionLanguage=ru
    Привет, Мир!
    Привет, Мир!

    Что же мы получили?

    Во-первых, в БД появился глобал
    ^CacheMsg("asd") = "ru"
    ^CacheMsg("asd","ru",2915927081) = "Привет, Мир!"
    ^CacheMsg("asd","ru","my") = "Привет, Мир!"

    Во-вторых, если навести курсор на макрос $$$Text, то можно увидеть код, в который он разворачивается.
    Для примера выше промежуточный (развёрнутый) код программы (INT-код) будет следующим:

    test() {  
      
    write "$$$DefaultLanguage=",$get(^%SYS("LANGUAGE","CURRENT"),"en"),!
      
    write "$$$SessionLanguage=",$get(^||%Language,"en"),!
      
    set msg1=$get(^CacheMsg("asd",$get(^||%Language,"en"),"2915927081"),"Привет, Мир!")
      
    set msg2=$get(^CacheMsg("asd",$get(^||%Language,"en"),"my"),"Привет, Мир!")
      
    write msg1,!,msg2,!
    }


    Что касается примера выше, то следует обратить внимание на следующие вещи:
    1. строки в программе следует писать изначально на том языке, который прописан по умолчанию в СУБД Caché в текущей локали (настраивается в Портале Управления);
      Примечание: При использовании идентификаторов строк вместо их хеша это уже не столь важно.
    2. для каждой строки макрос вычисляет её CRC32, и все данные — CRC32 или идентификатор строки, домен, текущий язык системы — сохраняются в глобал ^CacheMsg;
    3. вместо самой строки подставляется код, который учитывает значение в приватном глобале ^||%Language;
    4. если пользователь запросит строку на языке, для которого нет перевода (отсутствуют данные в хранилище), то вернётся исходная строка;
    5. механизм доменов позволяет логически разделять локализуемые строки, например разные переводы для одних и тех же строк и т.д.

    Если по каким-то причинам вас не устраивает текущий алгоритм работы макроса $$$Text, например язык по умолчанию хотите задавать по-другому или данные хранить в другом месте или ..., вы можете создать свой его аналог.
    И помогут вам в этом макросы ##Expression и/или ##Function.

    Продолжим наш пример.
    Давайте добавим новый язык. Для этого нужно выгрузить хранилище строк в файл и отдать его переводчикам, затем перевод загрузить обратно, но уже с другим языком.
    Данные можно выгрузить различными способами и в разных форматах.
    Мы же воспользуемся стандартными методами класса %MessageDictionary: Import(), ImportDir(), Export(), ExportDomainList():
      do ##class(%MessageDictionary).Export("messages.xml","ru")

    В каталоге нашей БД мы получим файл "messages_ru.xml". Переименуем его в "messages_en.xml", поменяем в нём язык на "en" и переведём содержимое.
    Далее импортируем его обратно в наше хранилище:
      do ##class(%MessageDictionary).Import("messages_en.xml")

    Глобал примет следующий вид:
    ^CacheMsg("asd") = "ru"
    ^CacheMsg("asd","en",2915927081) = "Hello, World!"
    ^CacheMsg("asd","en","my") = "Hello, World!"
    ^CacheMsg("asd","ru",2915927081) = "Привет, Мир!"
    ^CacheMsg("asd","ru","my") = "Привет, Мир!"

    Теперь мы можем менять язык «на лету», например:

    #Include %occMessages

    test()
    {  

      
    set $$$SessionLanguageNode="ru"

      
    set msg1=$$$Text("Привет, Мир!","asd")
      
    set msg2=$$$Text("@my@Привет, Мир!","asd")
      
    write msg1,!,msg2,!

      
    set $$$SessionLanguageNode="en"

      
    set msg1=$$$Text("Привет, Мир!","asd")
      
    set msg2=$$$Text("@my@Привет, Мир!","asd")
      
    write msg1,!,msg2,!
      
      
    set $$$SessionLanguageNode="pt-br"

      
    set msg1=$$$Text("Привет, Мир!","asd")
      
    set msg2=$$$Text("@my@Привет, Мир!","asd")
      
    write msg1,!,msg2,!

    }


    Результат:
    USER>d ^test
    Привет, Мир!
    Привет, Мир!
    Hello, World!
    Hello, World!
    Привет, Мир!
    Привет, Мир!

    Обратите внимание на последний вариант.

    Пример локализации не веб-приложения (обычного класса)


    Локализация методов класса:

    Include %occErrors

    Class demo.test Extends %Persistent
    {

    Parameter DOMAIN = "asd";

    ClassMethod Test()
    {
      
    do ##class(%MessageDictionary).SetSessionLanguage("ru")

      
    write $$$Text("Привет, Мир!"),!

      
    do ##class(%MessageDictionary).SetSessionLanguage("en")

      
    write $$$Text("Привет, Мир!"),!

      
    do ##class(%MessageDictionary).SetSessionLanguage("pt-br")

      
    write $$$Text("Привет, Мир!"),!
      
      
    #dim ex as %Exception.AbstractException
      
      
    try
      
    {

        
    $$$ThrowStatus($$$ERR($$$AccessDenied))

      
    }catch (ex)
      
    {
        
    write $system.Status.GetErrorText(ex.AsStatus(),"ru"),!
        
    write $system.Status.GetErrorText(ex.AsStatus(),"en"),!
        
    write $system.Status.GetErrorText(ex.AsStatus(),"pt-br"),!
      
    }
    }

    }
    Примечание: вы, конечно же, можете использовать и макросы, описанные выше.
    Результат:
    USER>d ##class(demo.test).Test()
    Привет, Мир!
    Hello, World!
    Привет, Мир!
    ОШИБКА #822: Отказано в доступе
    ERROR #822: Access Denied
    ERRO #822: Acesso Negado

    Обратите внимание на следующие моменты:
    • сообщения для исключений уже переведены на несколько языков. Поскольку это системные сообщения, данные для них хранятся в системном глобале %qCacheMsg;
    • имя домена мы задали один раз, так как по умолчанию макрос $$$Text рассчитан на использование в классах;
    • макрос $$$Text хоть и рассчитан на использование в веб-приложениях, тем не менее вполне подходит и для offline-окружения.

    Пример локализации веб-приложения


    Рассмотрим следующий пример:

    /// Created using the page template: Default
    Class demo.test Extends %ZEN.Component.page
    {

    /// Имя приложения, которому принадлежит эта страница.
    Parameter
    APPLICATION;

    /// Отображаемое имя для нового приложения.
    Parameter
    PAGENAME;

    /// Домен, используемый для локализации.
    Parameter
    DOMAIN = "asd";

    /// Этот блок Style содержит определение CSS стиля страницы.
    XData
    Style
    {
    <
    style type="text/css">
    </
    style>
    }

    /// Этот XML блок описывает содержимое этой страницы.
    XData
    Contents [ XMLNamespace = "www.intersystems.com/zen" ]
    {
    <
    page xmlns="www.intersystems.com/zen" title="">
      <
    checkbox onchange="zenPage.ChangeLanguage();"/>
      <
    button caption="Клиент" onclick="zenPage.clientTest(2,3);"/>
      <
    button caption="Сервер" onclick="zenAlert(zenPage.ServerTest(1,2));"/>
    </
    page>
    }

    ClientMethod clientTest(
      
    a,
      
    b) [ Language = javascript ]
    {
      zenAlert(
              $$$FormatText($$$Text(
    "Результат(1)^ %$# @*&' %1=%2"),'"',a+b),'\n',
              zenText(
    'msg3',a+b),'\n',
              $$$Text(
    "Привет из браузера!")
              );
    }

    ClassMethod ServerTest(
      
    A,
      
    B) As %String [ ZenMethod ]
    {
      
    &js<zenAlert(#(..QuoteJS($$$FormatText($$$Text("Результат(2)^ %$# @*&' ""=%1"),A+B)))#);>
      
    quit $$$TextJS("Привет из Caché!")
    }

    Method ChangeLanguage() [ ZenMethod ]
    {
      
    #dim %session as %CSP.Session
      
    set %session.Language=$select(%session.Language="en":"ru",1:"en")
      
    &js<zenPage.gotoPage(#(..QuoteJS(..Link($classname()_".cls")))#);>
    }

    Method %OnGetJSResources(ByRef pResources As %String) As %Status [ Private ]
    {
      
    Set pResources("msg3") = $$$Text("Результат(3)^ %$# @*&' ""=%1")
      
    Quit $$$OK
    }

    }


    Из новшеств следует отметить следующее:
    1. существует два варианта локализации сообщений на стороне клиента:
      • с помощью метода $$$Text, который определён в файле "zenutils.js";
      • с помощью комбинации метода zenText() на стороне клиента и серверного метода %OnGetJSResources()

      Подробности можно узнать в документации: Localization for Client Side Text
    2. некоторые атрибуты ZEN-компонент уже изначально поддерживают локализацию, например: всевозможные заголовки, подсказки и т.д.
      При необходимости создать свои собственные объектно-ориентированные компоненты — на основе, например jQuery или extJS или с нуля, — вы можете воспользоваться
      специальным типом данных %ZEN.Datatype.caption: Localization for Zen Components
    3. для смены языка можно воспользоваться свойством Language у объектов %session и/или %response: Zen Special Variables

    Изначально для сессии используется язык, заданный в браузере:

    image
    увеличить

    Создание собственного справочника сообщений об ошибках


    Рассмотренных выше средств хватит, чтобы сделать и это.
    Тем не менее есть встроенный метод, помогающий немного автоматизировать данный процесс.

    Итак приступим.

    Создадим файл "messages_ru.xml" с сообщениями об ошибках, следующего содержания:
    <?xml version="1.0" encoding="UTF-8"?>
    <MsgFile Language="ru">
      <MsgDomain Domain="asd">
        <Message Id="-1" Name="ErrorName1">Сообщение о некой ошибке 1</Message>
        <Message Id="-2" Name="ErrorName2">Сообщение о некой ошибке 2 %1 %2</Message>
      </MsgDomain>
    </MsgFile>

    Импортируем его в БД:
    do ##class(%MessageDictionary).Import("messages_ru.xml")

    В базе создались два глобала:
    • ^CacheMsg
      USER>zw ^CacheMsg
      ^CacheMsg("asd","ru",-2)="Сообщение о некой ошибке 2 %1 %2"
      ^CacheMsg("asd","ru",-1)="Сообщение о некой ошибке 1"

    • ^CacheMsgNames
      USER>zw ^CacheMsgNames
      ^CacheMsgNames("asd",-2)="ErrorName2"
      ^CacheMsgNames("asd",-1)="ErrorName1"

    Генерируем Include-файл с именем «CustomErrors»:
    USER>Do ##class(%MessageDictionary).GenerateInclude("CustomErrors",,"asd",1)
     
    Generating CustomErrors.INC ...
    Примечание: Детали см. в документации к методу GenerateInclude().

    Файл "CustomErrors.inc":

    #define asdErrorName2 "<asd>-2"
    #define asdErrorName1 "<asd>-1"


    Теперь можно использовать в программе коды ошибок и/или сокращённые имена ошибок, например:

    Include CustomErrors

    Class demo.test [ Abstract ]
    {

    ClassMethod test(A As %Integer) As %Status
    {
      
    if A=1 Quit $$$ERROR($$$asdErrorName1)
      
    if A=2 Quit $$$ERROR($$$asdErrorName2,"f","6")
      
    Quit $$$OK
    }

    }


    Результаты:
    USER>d $system.OBJ.DisplayError(##class(demo.test).test(1))
     
    ОШИБКА <asd>-1: Сообщение о некой ошибке 1
    USER>d $system.OBJ.DisplayError(##class(demo.test).test(2))
     
    ОШИБКА <asd>-2: Сообщение о некой ошибке 2 f 6
    
    USER>w $system.Status.GetErrorText(##class(demo.test).test(1),"en")
    ERROR <asd>-1: bla-bla-bla 1
    
    USER>w $system.Status.GetErrorText(##class(demo.test).test(2),"en")
    ERROR <asd>-2: bla-bla-bla 2 f 6
    Примечание: Сообщения для английского языка были созданы по аналогии.
    InterSystems
    76,00
    Вендор: СУБД Caché, OLAP DeepSee, шина Ensemble
    Поделиться публикацией

    Комментарии 5

      0
      Изображение — что надо!
        –1
        Ругают за картинку если честно — мол, миелофон прибор чтения мыслей, а не общения людей. Но ИМХО для решения задачи мультилингвального общения может использоваться.
        0
        Насколько это ресурсоёмко для самой базы.
        Как отражается на размере самой базы? В 2 раза, в 3, а то и больше?
          0
          Насколько это ресурсоёмко для самой базы.
          Не ресурсоёмко.
          Как отражается на размере самой базы? В 2 раза, в 3, а то и больше?
          Прямо пропорционально количеству языков.
          0
          Виталий, спасибо за описание работы с ошибками!

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое