Блог

    Caché (Кашэ́) — промышленная высокопроизводительная, объектная система управления базами данных, интегрированная с технологией разработки веб-приложений. Единая архитектура данных Caché позволяет разработчикам использовать одновременно объектный, реляционный (SQL) и прямой (NoSQL) доступ к одним и тем же данным, хранение которых обеспечивается ориентированным на транзакции многомерным ядром СУБД.

    http://www.intersystems.ru/cache/

Последние записи


Теги

Информация

Локализация в СУБД Caché классов, веб-приложений, исключений, консольных программ и т.д.

добавлено: 18 май 12
понравилось:0
просмотров: 2700
комментов: 0

теги:

Автор: servit

Этот же пост доступен и на хабре.

Предположим, Вы написали программу, выводящую "Hello, World!", например:
  write "Hello, World!"
Всё хорошо до поры до времени.
Потом обстоятельства меняются и Вам нужно эту же строку вывести уже на другом языке, причём количество и состав требуемых языков заранее неизвестен.

Как можно поступить проще, учитывая что:
  • в программе таких строчек может быть очень много;
  • пользователи в любой момент могут поменять язык сессии?

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

В СУБД 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 "http://www.intersystems.com/zen" ]
{
<
page xmlns="http://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,
  
BAs %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 %StringAs %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

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

Картинка с другого сайта.


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

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

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

Создадим файл "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(As %IntegerAs %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
Примечание
Сообщения для английского языка были созданы по аналогии.

Комментарии




Необходимо войти на сайт, чтобы оставлять комментарии