Suppose you wrote a program that displays “Hello, World!”, For example:
write "Hello, World!"
The application works, everything is fine.
But time passes, your application develops, it becomes popular, and now, you need to output this line in a different language, and the number and composition of the required languages is not known in advance.
Under the cut, you will learn how the localization problem is solved in Caché.
Short review
Caché provides a ready-made mechanism that simplifies the localization of strings in console programs, the interface in web applications, strings in JavaScipt files, error messages, etc.
Note: This topic was considered in passing in a previous article .
Suppose there is a project with many classes, programs, web pages, js scripts, etc.
The localization mechanism works as follows:
- even at the compilation stage of the project, all lines that are to be localized are “fished out” and stored inside the database in a certain format.
- in the compiled code itself, instead of the lines themselves, a certain code is substituted, which, already at the execution stage , depending on the current session language, will output one or another value from the storage.
The entire localization process is completely transparent to the programmer.
The developer eliminates the need to manually fill in some kind of row storage (tables in the database or resource file), as well as writing code to manage all of this infrastructure, such as changing the language at runtime, exporting / importing data into various formats for the translator, etc. d.
As a result, we have:
- readable - uncluttered; - source code;
- automatically filled storage of localized strings;
Note: When deleting lines from code from the repository, they are not deleted. To clean up the repository from such phantoms, it is easier to clean it up and recompile the project.
- change the current language on the fly. This applies to both web applications and regular programs;
- the ability to get a string in a given language, from a given domain (about the domains below);
- ready-made methods for exporting / importing a repository to XML.
So let's take a closer look at how this works, as well as all sorts of examples on localization.
')
Introduction
Create a MAC program as follows:
#Include %occMessages
test () {
write "$$$DefaultLanguage=" , $$$DefaultLanguage ,!
write "$$$SessionLanguage=" , $$$SessionLanguage ,!
set msg1= $$$Text ( ", !" , "asd" )
set msg2= $$$Text ( "@my@, !" , "asd" )
write msg1,!,msg2,!
}
Result:
USER>d ^test $$$DefaultLanguage=ru $$$SessionLanguage=ru , ! , !
What did we get?
First, the global appeared in the database.
^CacheMsg("asd") = "ru" ^CacheMsg("asd","ru",2915927081) = ", !" ^CacheMsg("asd","ru","my") = ", !"
Secondly, if you hover the cursor on the
$$$ Text macro, you can see the code in which it is deployed.
For the example above, the intermediate (expanded) program code (INT code) will be as follows:
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,!
}
As for the example above , you should pay attention to the following things:
- lines in the program should be written initially in the language that is registered by default in the Caché DBMS in the current locale (configured in the Management Portal);
Note: When using string identifiers instead of their hash, this is not so important.
- for each row, the macro calculates its CRC32, and all data — CRC32 or row identifier, domain, current system language — are saved in the global ^ CacheMsg ;
- instead of the line itself, a code is inserted that takes into account the value in the private global ^ ||% Language ;
- if the user requests a string in a language for which there is no translation (there is no data in the repository), then the original string will be returned;
- the domain mechanism allows you to logically separate localized strings, for example, different translations for the same strings, etc.
If for some reason you are not satisfied with the current algorithm of the
$$$ Text macro, for example, you want to set the default language differently or store data in a different place or ..., you can create your own analog.
And the
## Expression and / or
## Function macros will help you with this.
Let's continue our example.
Let's add a new language. To do this, you need to unload the repository of strings into a file and give it to the translators, then download the translation back, but in a different language.
Data can be uploaded in various ways and in different formats.
We will use the standard methods of the class
% MessageDictionary :
Import (),
ImportDir (),
Export (),
ExportDomainList ():
do ##class ( %MessageDictionary ). Export ( "messages.xml" , "ru" )
In the catalog of our database, we get the file "
messages_ru.xml ". Rename it to "
messages_en.xml ", change its language to "
en " and translate the contents.
Next, import it back to our repository:
do ##class ( %MessageDictionary ). Import ( "messages_en.xml" )
Global will take the following form:
^CacheMsg("asd") = "ru" ^CacheMsg("asd","en",2915927081) = "Hello, World!" ^CacheMsg("asd","en","my") = "Hello, World!" ^CacheMsg("asd","ru",2915927081) = ", !" ^CacheMsg("asd","ru","my") = ", !"
Now we can change the language "on the fly", for example:
#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,!
}
Result:
USER>d ^test , ! , ! Hello, World! Hello, World! , ! , !
Pay attention to the last option.
Example of non- web application localization (normal class)
Localization of class methods:
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" ),!
}
}
}
Note: you can, of course, use the macros described above.
Result:
USER>d ##class(demo.test).Test() , ! Hello, World! , ! #822: ERROR #822: Access Denied ERRO #822: Acesso Negado
Pay attention to the following points:
- messages for exceptions have already been translated into several languages. Since these are system messages, the data for them is stored in the system global % qCacheMsg ;
- we set the domain name once, because the default $$$$ macro is designed for use in classes;
- Although the $$$ Text macro is intended for use in web applications, it is nevertheless quite suitable for offline environments.
Web application localization example
Consider the following example:
/// 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
}
}
Of the innovations, the following should be noted:
- There are two options for localizing messages on the client:
- using the $$$ Text method, which is defined in the " zenutils.js " file;
- using the client-side zenText () combination and the % OnGetJSResources () server method
Details can be found in the documentation: Localization for Client Side Text - Some attributes of the ZEN component already initially support localization, for example: all sorts of headers, hints, etc.
If you need to create your own object-oriented components — based on, for example, jQuery or extJS or from scratch — you can use
% ZEN.Datatype.caption special data type: Localization for Zen Components - To change the language, you can use the Language property of the % session and / or % response : Zen Special Variables objects.
Initially, the language specified in the browser is used for the session:
to increaseCreating your own error message directory
The means discussed above are enough to do this.
However, there is a built-in method that helps automate this process a bit.
So let's get started.
Create a file "
messages_ru.xml " with error messages, as follows:
<?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>
Import it into the database:
do ##class ( %MessageDictionary ). Import ( "messages_ru.xml" )
Two globals were created in the base:
- ^ 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"
Generate an Include file with the name “CustomErrors”:
USER>Do ##class(%MessageDictionary).GenerateInclude("CustomErrors",,"asd",1) Generating CustomErrors.INC ...
Note: For details, see the documentation for the GenerateInclude () method.
File "
CustomErrors.inc ":
#define asdErrorName2 "<asd>-2"
#define asdErrorName1 "<asd>-1"
Now you can use error codes and / or abbreviated names of errors in the program, for example:
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
}
}
Results:
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
Note: Messages for the English language were created by analogy.