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

в 4:46, , рубрики: cache, csp, dbms, intersystems cache, nosql, web-разработка, zen, Блог компании InterSystems, локализация, ооп, субд Caché, метки: , , , , , , , , ,

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

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

Под катом вы узнаете, как решается задача локализации в 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

Примечание: Сообщения для английского языка были созданы по аналогии.

Автор: servitRM

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js