Предположим, вы написали программу, выводящую «Hello, World!», например:
write "Hello, World!"
Приложение работает, всё хорошо.
Но проходит время, ваше приложение развивается, становится популярным и вот, вам нужно эту строку вывести уже на другом языке, причём количество и состав требуемых языков заранее неизвестен.
Под катом вы узнаете, как решается задача локализации в Caché.
Краткий обзор
В СУБД Caché предусмотрен готовый механизм, упрощающий локализацию строк в консольных программах, интерфейса в веб-приложениях, строк в файлах JavaScipt, сообщений об ошибках и т.д.
Примечание: Данная тема была рассмотрена вскользь в одной из предыдущих статей.
Допустим, имеется проект со множеством классов, программ, веб-страничек, js-скриптов и т.д.
Работает механизм локализации следующим образом:
- ещё на этапе компиляции проекта «выуживаются» все строки, подлежащие локализации, и сохраняются внутри базы в определённом формате.
- в сам откомпилированный код вместо самих строк подставляется определённый код, который уже на этапе выполнения будет в зависимости от текущего языка сессии выдавать из хранилища то или иное значение.
Весь процесс локализации полностью прозрачен для программиста.
Разработчик избавляется от необходимости ручного заполнения некоего хранилища строк (таблицы в БД или ресурсного файла), а также от написания кода по управлению всей этой инфраструктурой, как то: смена языка во время исполнения, экспорт/импорт данных в различные форматы для переводчика и т.д.
В итоге мы имеем:
- читаемый — незагромождённый лишним — исходный код;
- автоматически наполняемое хранилище локализуемых строк;
Примечание: При удалении строк из кода из хранилища они не удаляются. Для очистки хранилища от таких фантомов проще его очистить и заново перекомпилировать проект.
- смену текущего языка «на лету». Это касается как веб-приложений, так и обычных программ;
- возможность получить строку на заданном языке, из заданного домена (о доменах чуть ниже);
- готовые методы по экспорту/импорту хранилища в 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,!
}
Что касается примера выше, то следует обратить внимание на следующие вещи:
- строки в программе следует писать изначально на том языке, который прописан по умолчанию в СУБД Caché в текущей локали (настраивается в Портале Управления);
Примечание: При использовании идентификаторов строк вместо их хеша это уже не столь важно.
- для каждой строки макрос вычисляет её CRC32, и все данные — CRC32 или идентификатор строки, домен, текущий язык системы — сохраняются в глобал ^CacheMsg;
- вместо самой строки подставляется код, который учитывает значение в приватном глобале ^||%Language;
- если пользователь запросит строку на языке, для которого нет перевода (отсутствуют данные в хранилище), то вернётся исходная строка;
- механизм доменов позволяет логически разделять локализуемые строки, например разные переводы для одних и тех же строк и т.д.
Если по каким-то причинам Вас не устраивает текущий алгоритм работы макроса $$$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
}
}
Из новшеств следует отметить следующее:
- существует два варианта локализации сообщений на стороне клиента:
- с помощью метода $$$Text, который определён в файле "zenutils.js";
- с помощью комбинации метода zenText() на стороне клиента и серверного метода %OnGetJSResources()
Подробности можно узнать в документации: Localization for Client Side Text
- некоторые атрибуты ZEN-компонент уже изначально поддерживают локализацию, например: всевозможные заголовки, подсказки и т.д.
При необходимости создать свои собственные объектно-ориентированные компоненты — на основе, например jQuery или extJS или с нуля, — вы можете воспользоваться
специальным типом данных %ZEN.Datatype.caption: Localization for Zen Components - для смены языка можно воспользоваться свойством 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(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