< Localization
Revision as of 20:35, 29 June 2011 by Neverendingo (Talk | contribs) (Text replace - "<code javascript>" to "<syntaxhighlight lang="javascript">")

Jump to: navigation, search
Translation Scripting with Transcript
On Localization   Concepts
Prerequisites   The PO Format
Related Articles   n/a
External Reading   JavaScript Guide and Reference

Translation Scripting?

The current state of affairs in the localization of user-visible strings (messages) in application interfaces is such that the translator is sometimes forced to supply an inadequate translation. This problem typically occurs when a message contains a placeholder to be substituted at runtime, or when two unrelated strings become related by placement in the interface. In either case, the modest requirements of the English language on the congruence of words in a sentence allow the original string to remain grammatically correct, while in many other languages this is not the case.

Translators and programmers sometimes try to work out a change in the code which would provide a more workable alternative. However, this process is difficult, and worse yet, the outcome is still language-dependent: solving the problem for a few languages does not necessarily solve it for all of them.

One way to overcome these problems in a more general and compartmentalized manner is to provide translators with a way to modify translated strings at runtime, depending on the context (eg. particular placeholder substitutes). In other words, to script translation. The translator should be able to operate on any interface string he wishes, while the programmer should not bear any extra burden (or know about translation scripting at all).

The Transcript Engine

KDE4 comes ready with a translation scripting system, called Transcript. Several strategic choices were made in its design:

  • programmers are unaware of scripting, which means that translator can script any message without outside coordination
  • unless the translator wants to script a message, he is faced with the familiar standard Gettext PO environment (i.e. translators, too, can ignore scripting)
  • scripting is a low-level bolt-on to Gettext environment, to keep existing PO tools in the game
  • in order to be powerful enough for unforeseen needs, a general-purpose scripting language is provided: JavaScript (with extensions for interfacing with Transcript)
For comparison, another translation scripting approach taking some different decisions (new translation environment down to the file format, scripting facilities more specialized, etc.) is brewing at http://wiki.mozilla.org/L20n

To script a particular message, the translator writes short scripting calls into msgstr in the PO file which expand into parts of msgstr (interpolations), and JavaScript code which defines these calls into an accompanying Transcript module file (eg. foo.po can be augmented with foo.js).

In case where the application draws translations from several PO files, scripting calls defined in one of the Transcript modules are available in all used PO files. Since every KDE app uses kdelibs.po, calls defined in kdelibs.js are available everywhere.

The scripting process is illustrated by several examples. More detailed explanations of the elements are given in following sections.

A Useless Example

In Nevernessian it is impolite to speak a greeting with the same tone of voice throughout; instead, the name of the person must be shouted out. Hence, the translator wants to capitalize the placeholder substitute in the following login greeting in neverness.po:

  1. neverness_login.cpp:10

msgid "Hello, %1!" msgstr "Heelyy, %1!"

So the translator adds a scripted msgstr, with an interpolation:

  1. neverness_login.cpp:10

msgid "Hello, %1!" msgstr "Heelyy, %1!" "|/|" "Heelyy, $[shout %1]!"

The first thing to note is that, while a bit longer, msgstr is still a proper PO msgstr, which means that it can be edited and processed by the usual PO tools.

The first part of msgstr is same as before, and called the fallback in this context: if the scripted part happens to fail in some way, the fallback translation is used. The fallback is followed by the fence |/|, which separates the fallback and scripted translation (and also indicates that this message is scripted).

Finally, there is the scripted translation after the fence. It is different from the fallback in that it contains the interpolation $[shout %1], which is supposed to evaluate to a capitalized version of the placeholder substitute. It is composed of the call name, shout, and one argument to it, the %1 placeholder which will be replaced by its substitute. The syntax and expansion rules for interpolations are similar to Unix shell.

The call shout itself is defined in the Transcript module neverness.js, which contains only these lines:

function capitalize (str) {
    return str.toUpperCase();

Ts.setcall("shout", capitalize);</code>

Here the function <tt>capitalize</tt> is an ordinary JavaScript function which takes a string argument and returns all-caps version of it.

The link with the PO file is established by the call to <tt>Ts.setcall()</tt> -- the Transcript interface is represented by the property functions of the <tt>Ts</tt> object. In this variant, the <tt>Ts.setcall()</tt> takes the name of the call for the interpolations in the PO messages (a string), and the JavaScript function which will actually be invoked (''bound'' to the call).

That's it: now the fair Nevernesse folks are greeted properly.

=== Basic Case Resolution ===

One problem frequently encountered is use of the wrong noun case when the placeholder is substituted in the msgstr. For example, in many languages every KDE app has such a problem in the Help menu, with one or both of "About %1..." and "%1 &Handbook". This can be scripted in {{path|kdelibs.po}} like this:

<code po>
msgid "&About %1"
msgstr "&O %1"
"&O $[get-case dative %1]"</code>

The <tt>get-case</tt> interpolation is supposed to get the <tt>dative</tt> case of whatever app name the <tt>%1</tt> happens to be. The Transcript module {{path|kdelibs.js}} contains the definition of <tt>get-case</tt>, as well as the dictionary of cases:

<syntaxhighlight lang="javascript">
function getProperty (prop, key) {
    return _dict_[key][prop];
Ts.setcall("get-case", getProperty);

_dict_ = {};
function addDictCases (key, gen, dat, acc, ins) {
    if (!_dict_[key])
        _dict_[key] = {};
    _dict_[key]["genitive"]     = gen;
    _dict_[key]["dative"]       = dat;
    _dict_[key]["accusative"]   = acc;
    _dict_[key]["instrumental"] = ins;

// dictionary entries follow:
addDictCases("KWrite", "KWritea", "KWriteu", "KWrite", "KWriteom");
addDictCases("Konsole", "Konsole", "Konsoli", "Konsolu", "Konsolom");

Function <tt>getProperty</tt>, bound to <tt>get-case</tt> call, simply returns the entry from the dictionary of forms. Function <tt>addDictCases</tt> is responsible for adding the static entries (name and its cases) into the dictionary, which is done in the final few lines for all apps of interest.

This completes the example, but for better modularization, it is also possible split out the dictionary insertion in a separate file, eg. {{path|appdict.js}}:

<syntaxhighlight lang="javascript">
// appdict.js
addDictCases("KWrite", "KWritea", "KWriteu", "KWrite", "KWriteom");
addDictCases("Konsole", "Konsole", "Konsoli", "Konsolu", "Konsolom");

and use Transcript interface to load this file in the {{path|kdelibs.js}}:

<syntaxhighlight lang="javascript">
// kdelibs.js

Note that <tt>Ts.load()</tt> takes filename without extension, and assumes its location is relative to the folder of the parent file (ie. in this case {{path|kdelibs.js}} and {{path|appdict.js}} should be in the same folder).

=== Dynamic Case Setting ===

The previous scripted example solves the original problem, but introduces the burden of maintaining the dictionary insertion file. There is no way around this when the placeholder substitutes are "dead" strings from outside (eg. from {{path|.desktop}} files), but when they are coming from KDE's PO files at runtime, this burden can be removed.

The app name in KDE's Help menu indeed comes from the app PO file, and it is of course encountered at runtime before the menu strings come into focus. This allows setting the cases of app name in the PO msgstr which contains it. For example, {{path|katepart.po}} contains the "KWrite" string, and the forms could be set at that point:

<code po>
msgid "KWrite"
msgstr "KWrite"
"$[set-cases KWritea KWriteu KWrite KWriteom]"</code>

The <tt>set-cases</tt> is a ''side-effect'' interpolation: it should set the dictionary entries, but this particular message should in any case use the ordinary translation. Assuming all the definitions from previous example are still in effect, here is how <tt>set-cases</tt> could be defined in {{path|kdelibs.js}}:

<syntaxhighlight lang="javascript">
function dynamicSetCases (gen, dat, acc, ins) {
    addDictCases(Ts.msgstrf(), gen, dat, acc, ins);
Ts.setcall("set-cases", dynamicSetCases);</code>

In other words, this is little more than a wrapper to "static" <tt>addDictCases</tt> from previous example, but two new elements of Transcript interface appear. First is the <tt>Ts.msgstrf()</tt> function, which returns the finalized ordinary translation (placeholders substituted), and which is needed in this case as the dictionary key. Second is the <tt>Ts.fallback()</tt> function, which signalizes the Transcript engine to disregard the result of the scripted part of msgstr and use the ordinary translation.

Admittedly, the use of <tt>Ts.fallback()</tt> in this case is not necessary, but given for introductory purpose; <tt>dynamicSetCases</tt> might as well ''return'' the ordinary translation via <tt>Ts.msgstrf()</tt>.

== The PO Shell ==

This section gives the details of how the interpolations in the PO msgstr are expanded before evaluation.

The interpolations are parts of msgstr between <tt>$[...]</tt>, and are parsed into a number of strings. The first string is the name of the call registered in the scripting module via <tt>Ts.setcall()</tt>, and the rest are the arguments to bound JavaScript function. The arguments are typically passed as JavaScript type <tt>String</tt>, except in a special case detailed below.

The special characters in the interpolation are whitespace, single quote (') and backslash (\). Whitespace separates arguments, whereas single quote can be used for text which contains whitespaces. The backslash is used as escape; it can escape whitespace in non-quoted text, or single quotes in quoted text. This is pretty much like a typical Unix shell.

Double quotes are not special. Single quotes are used instead of double quotes because it makes it easier to edit interpolations in PO files, where double quotes would have to be escaped. This also means that when escape is needed in the interpolation, it must be escaped once itself for the PO msgstr.

The biggest difference from the shell expansion is that unlike with shell variables, the placeholders are expanded such that no characters within them are treated as special. Otherwise, many tricky problems could arise with whitespace or single quotes contained inside.

The call name bound to a JavaScript function using <tt>Ts.setcall()</tt> does not have to be a proper JavaScript identifier, but any Unicode string not containing the interpolation-special characters. This means that more "natural" call names can be used inside the msgstr, like those with dashes or non-Latin1 characters. E.g. call names can be in the language of the PO file, for an aesthetic impression.

Sub-interpolations, <tt>$[... $[...] ...]</tt>, are also possible. If put inside inside single quotes, they will be treated as ordinary text. Thus, if a literal closing square bracket is needed as an argument inside the interpolation, it can be given within single quotes.

In case that the argument is given as <tt>^''number''</tt>, it will evaluate to the value supplied for the corresponding placeholder -- unlike the <tt>%''number''</tt> form, which evaluates to the placeholder substitution string. Also, the argument type as seen by JavaScript need no longer be <tt>String</tt>, but will correspond to the value substituted (e.g. <tt>Number</tt>). This distinction may be important in some situations. Even if the value is originally a string, the substitution string may have extra formatting (padding, tags...), which may not be desirable. In particular, the number substitutes may come tagged or locale-formatted, such that they cannot be easily parsed back into <tt>Number</tt>. In these cases, <tt>^''number''</tt> form can be used to get the raw values.

== The Transcript Interface ==

Transcript provides extensions to the JavaScript, which interface with the PO file and the Transcript environment. They are all function properties of the <tt>Ts</tt> object, accessible as <tt>Ts.''func''(''args'')</tt>.

; <tt>setcall (''name'', ''func'')</tt>
; <tt>setcall (''name'', ''func'', ''obj'')</tt>
: Binds the call name to the JavaScript function, for use in the interpolations inside the PO file.
:: <tt>''name''</tt> name of the call. Can be any Unicode string, for ease of use in the msgstr
:: <tt>''func''</tt> function object
:: <tt>''obj''</tt> object to act as <tt>this</tt> inside the function; if omitted, <tt>this</tt> refers to global object
: Returns <tt>Undefined</tt>.

; <tt>hascall (''name'')</tt>
: Check if the call for use in PO file has been set.
:: <tt>''name''</tt> name of the call
: Returns <tt>true</tt> if set, <tt>false</tt> otherwise.

; <tt>acall (''name'', ''arg''*)</tt>
: Applies previously set PO call to a list of arguments, within the scripting module itself. Indirect calls can be made this way, e.g. by passing a call name as a string within PO file interpolation.
:: <tt>''name''</tt> name of the call
:: <tt>''arg''*</tt> arguments to the call
: Returns what the indirect call would return.

; <tt>load (''file''*)</tt>
: Evaluates the code in the specified files, in the left to right order. File paths are expected to be relative to current module's folder.
:: <tt>''file''</tt> file name without extension
: Returns <tt>Undefined</tt>.

; <tt>fallback ()</tt>
: Forces Transcript to use ordinary translation, regardless of whether the interpolation evaluates successfully or fails. The evaluation of the script is not aborted, any other interpolations will evaluate too.
: &nbsp;
: Returns <tt>Undefined</tt>.

; <tt>msgid ()</tt>
: Returns msgid of the last message, with placeholders intact.

; <tt>msgstrf ()</tt>
: Returns finalized ordinary translation of the last message, with placeholders substituted.

; <tt>msgctxt ()</tt>
: Returns msgctxt of the last message, with placeholders intact.

; <tt>msgkey ()</tt>
: Returns a <tt>String</tt> which is implementation-dependent combination of msgctxt and msgid with placeholders intact. Used to uniquely identify the message within the PO file, usefull as a dictionary key.

; <tt>nsubs ()</tt>
: Returns the number of substitutes provided for placeholders in the last message. It is equal to the highest-numbered placeholder for a proper i18n call in the application code, but i18n calls do not have to be quite proper.

; <tt>subs (''index'')</tt>
: Used to access values of placeholder substitutes provided to the last message. Numbering is zero-based.
:: <tt>''index''</tt> index of placeholder substitute
: Returns <tt>String</tt>, regardless of the value type substituted in the application code.

; <tt>vals (''index'')</tt>
: Used to access values of placeholder values provided to the last message. Numbering is zero-based.
:: <tt>''index''</tt> index of placeholder value
: Returns the matching JavaScript type to the value type used in the application code. E.g. strings will be <tt>String</tt>, integers and doubles <tt>Number</tt>. May also return <tt>Undefined</tt>, in case the value cannot be represented as a reasonable JavaScript type.

; <tt>dynctxt (''key'')</tt>
: Programmers may append ''dynamic'' context to the message, which is a pair of strings: a context key and its value. Use this function to retrieve such contexts.
:: <tt>''key''</tt> context key string
: Returns <tt>String</tt>, or <tt>Undefined</tt> if dynamic context with the given key does not exist.

; <tt>dbgputs (''msg'')</tt>
: Outputs a debug message in the shell when KDE has been compiled with debug option.
:: <tt>''msg''</tt> message string
: Returns <tt>Undefined</tt>.

; <tt>setcallForall (''name'', ''func'', ''obj'')</tt>
; <tt>setcallForall (''name'', ''func'')</tt>
: Similar to <tt>setcall()</tt>, but the function is executed on every message after it has been finalized (if the message was explicitly scripted, the function is executed after the script has been evaluated). The function receives no arguments, and its return value is ignored, i.e. it cannot change the finalized message; the call is used for side-effects. The <tt>''name''</tt> parameter is used only for reporting errors. When several <tt>setcallForall()</tt> have been issued, the functions are invoked in the order of issue.
: &nbsp;
: More precisely, these calls are made on every message ''after'' the Transcript engine has been initialized, which happens on first explicitly scripted message. Thus, if earlier application of the calls is necessary, Transcript can be jump-started by scripting one early encountered message with only a single empty interpolation: <tt>msgstr "...|/|$[]"</tt>; this will do nothing for that message, but it will start the engine.
: &nbsp;
: Returns <tt>Undefined</tt>.

; <tt>toUpperFirst (''text'')</tt>
: Converts the first letter in the string into upper case, even if the first character in the string is not a letter. Unicode specification is used to determine what a letter is (i.e. works for non-ASCII letters).
:: <tt>''text''</tt> string to process
: Returns <tt>String</tt>.

; <tt>toLowerFirst (''text'')</tt>
: Converts the first letter in the string into lower case, under same assumptions as for <tt>toUpperFirst</tt>.
:: <tt>''text''</tt> string to process
: Returns <tt>String</tt>.

; <tt>loadProps (''file''*)</tt>
: Loads [[#Property Maps|property maps]] from the specified files. File paths are expected to be relative to current module's folder.
:: <tt>''file''</tt> file name without extension
: Returns <tt>Undefined</tt>.

; <tt>getProp (''phrase'', ''prop'')</tt>
: Fetches the property value of the phrase, as defined by [[#Property Maps|property map]].
:: <tt>''phrase''</tt> text for which the property is requested
:: <tt>''prop''</tt> property key string
: Returns <tt>String</tt>, or <tt>Undefined</tt> if the given text has no such property.

; <tt>setProp (''phrase'', ''prop'', ''value'')</tt>
: Sets the property value of the phrase. The phrase and property key will be automatically normalized, as they would have been when loaded from a [[#Property Maps|property map]] file.
:: <tt>''phrase''</tt> text for which the property is set
:: <tt>''prop''</tt> property key string
:: <tt>''value''</tt> value of the property (string)
: Returns <tt>Undefined</tt>.

; <tt>normKey (''phrase'')</tt>
: Normalizes the text by removing all whitespace, removing the accelerator, and lowercasing it, e.g. for use in near-match lookups.
:: <tt>''phrase''</tt> text to be normalized
: Returns <tt>String</tt>.

; <tt>getConfString (''key'')</tt>
; <tt>getConfString (''key'', ''value'')</tt>
; <tt>getConfBool (''key'')</tt>
; <tt>getConfBool (''key'', ''value'')</tt>
; <tt>getConfNumber (''key'')</tt>
; <tt>getConfNumber (''key'', ''value'')</tt>
: Fetches a value of the requested type from the [[#User Configuration|user-configuration]] file. A default value of the same type can be stated too (or else it defaults to <tt>undefined</tt>).
:: <tt>''key''</tt> the name of the field in the configuration file
:: <tt>''value''</tt> the default value, in case the key is not found
: Returns the configuration value if the key is found, or the default value or <tt>undefined</tt> otherwise. If the configuration string is not a valid representation of the requested type, again the default or <tt>Undefined</tt> is returned instead.

=== Property Maps ===

There is a frequent need to have different properties attached to the particular pieces of text. For example, case, gender, etc. may be considered as properties of a noun, or phrase with a subject/object role in a sentence. Sometimes it may be convenient (or necessary) to define and maintain the phrases and their properties in an external file. ''Property map'' is Transcript's built-in (i.e. efficient) way to handle such definitions.

Property map files are text files in a simple dictionary format for writing down phrases with their properties (filenames must end in <tt>.pmap</tt>). The format is best shown on the example:
<code text>
# cities.pmap
Each entry starts with two characters, <tt>=:</tt> in this example, which may be chosen arbitrarily (but must be non-letters, and non-#), and can change from entry to entry. The first character defines the separator in the property key-value pair, and the second character is the separator of pairs. E.g. the <tt>gen=Atine</tt> segment above is defining the genitive case of the localized form of Athens. The "pairs" which actually lack the property key (first two in the entries above) are considered phrase identifiers. The completely empty pair (<tt>::</tt> in the example) terminates the entry.

The property map is loaded inside the scripting module using the <tt>Ts.loadProps()</tt> call. The map file should normally reside in the same folder as the scripting module that uses it; if so, the map from the above example is loaded simply with <tt>Ts.loadProps("cities")</tt>. The property values are obtained in the scripts using <tt>Ts.getProp()</tt> call. E.g. the genitive form of Athens would be obtained either by <tt>Ts.getProp("Athens", "gen")</tt> or <tt>Ts.getProp("Atina", "gen")</tt> -- because the map defined both as phrase identifiers for Athens. The phrase/key lookup is performed using normalized values; this normalization can be checked or used for own purposes by <tt>Ts.normKey()</tt> call.

The comments are a bit of an issue in property map files. Since the phrases and property values can be anything (hence the possibility to choose separators), fixing any single character as your typical to-the-end-of-line comment would introduce limitations. Instead, the #-comments are allowed only ''between'' the entries, span to the end of line, and # cannot be a separator character.

The whitespace in property values is mostly preserved (for the keys it does not matter, as it gets stripped for lookups). However, if the leading sequence of whitespace contains a newline, all whitespace to the first newline and including it is stripped; symmetrically, in a trailing whitespace sequence, the last newline (if any) and the whitespace following it are removed.

=== User Configuration ===

Using a configuration file located at {{path|$HOME/.transcriptrc}}, the user can be allowed to configure certain aspects of Transcript operation. The configuration file is in ini-style, where group names are language codes to which the selections apply. For example:
<code text>
# Settings for German.
serve-sausages = no
wine-instead-of-beer = yes

# Settings for Italian.
with-mozzarella = no
Within scripting modules, the configuration values can be fetched using <tt>Ts.getConf*()</tt> series of calls. These calls will automatically look for the configuration fields under the proper language group, as the scripting modules are language-dependent too.

For boolean-valued keys, <tt>0</tt>, <tt>no</tt>, and <tt>false</tt> can be used to indicate negative (false) values, and everything else is treated as positive (true). Also, the boolean values are not case sensitive.

== Repository Organization ==

For the PO file in the KDE repository {{path|LL/messages/kdemodule/foo.po}}, the corresponding Transcript module should be located at {{path|LL/scripts/kdemodule/foo/foo.js}}. Note the extra subfolder in the module path, named like the basename of the PO/JS file. This subfolder is introduced because the module's main JS file might need other supporting files, which can then be located conveniently in the same folder.

The {{path|autogen.sh}} script, used for some time already to generate build system support for PO files, will also generate build support for Transcript modules. In particular, it must be rerun whenever a new module subfolder is added (e.g. the {{path|LL/scripts/kdemodule/foo}}), but not when particular files within it are added or removed. Every file in the module subfolder will be installed automatically, so put there only what is needed at runtime.

== Real-Life Examples ==

=== CJK Accelerator Keys ===
In CJK environment, accelerator keys are wrapped in the parenthesis. For example, translation of "&File" in Korean is "파일(&F)", and in Japanese, "ファイル(&F)". When it comes to the names of action, it is shared among toolbar icon names, menu bar, and so on.

However, in toolbar icon name, "Configure shortcut" dialog, etc., those access keys are remaining in the name, makes the name little bit awkward. In this situation, we can use transcript to get rid of those accelerator keys. Currently,  the code is in Japanese and Korean kdelibs4.js file.

=== Korean Postpositions ===
In Korean language, postpositions are widely used. Among those, 8 postpositions are in pair and they change forms according to the word. Because PO file itself doesn't know what the substituted word(like %1) is, proper postpositions couldn't be added to the string. 

Fortunately, the rule is programmable, and proper postpositions could be added via transcript. Right now a little part of postposition rules is programmed into Korean version of kdelibs4.js.

=== Automatic Property Calls ===
Leveraging [[#Property Maps|property maps]] interface, we can make a generalization of the [[#Dynamic Case Setting|dynamic case setting]] example. We want to set named properties by key-value pairs for a given message, and then, to use calls named as property keys to retrieve the values when that message is used as placeholder replacement in another message.

For example, Nevernessians need to match the noun case of "baloon" when it is inserted into sentence "I see a %1", and additionally the adjective form of "red" to baloon in "I see a red %1", so they do:
<code po>
msgid "baloon"
msgstr ""
"$[properties see-it beeluuno see-red raado]"

msgid "I see a %1"
msgstr ""
"O sii e %1"
"O sii e $[see-it %1]"

msgid "I see a red %1"
msgstr ""
"O sii e raad %1"
"O sii e $[see-red %1] $[see-it %1]"

The <tt>properties</tt> call, which does all the work -- setting properties, setting new calls by property keys -- looks like this:
<syntaxhighlight lang="javascript">
// Set properties of the phrase given by the finalized msgstr in the PO file.
// The arguments to the call are consecutive pairs of keys and values,
// as many as needed (i.e. total number of arguments must be even).
// The property keys are registered as PO calls taking single argument,
// which can be used to retrive the property values for this msgstr
// when it is later used as placeholder replacement in another message.
// Always signals fallback.
function setMsgstrProps (/*KEY1, VALUE1, ...*/)
    if (arguments.length % 2 != 0)
        throw Error("Property setter given odd number of arguments.");

    // Collect finalized msgstr.
    phrase = Ts.msgstrf()

    // Go through all key-value pairs.
    for (var i = 0; i < arguments.length; i += 2) {
        var pkey = arguments[i];
        var pval = arguments[i + 1];

        // Set the value of the property for this phrase.
        Ts.setProp(phrase, pkey, pval);

        // Set the PO call for getting this property, if not already set.
        if (!Ts.hascall(pkey)) {
                       function (phr) { return Ts.getProp(phr, this.pkey) },
                       {"pkey" : pkey});

    throw Ts.fallback();
Ts.setcall("properties", setMsgstrProps);
// NOTE: You can replace "properties" in the line above with any UTF-8 string,
// e.g. one in your language so that it blends nicely inside POs.

=== Labels Dependent on Dynamic Values ===
Sometimes the text of a standalone label should grammatically conform to the a changing value elsewhere in the interface. The typical example of this is when a sentence-like row of different widgets is used to specify an action:
<code text>
Search in files modified:
( ) ...
(*) during the last {day|week|month|year}
( ) ...
<tt>( )</tt> stand for radio buttons, and <tt>{...|...|...}</tt> for a list box with several choices. The label <tt>"during the last"</tt> contains the adjective "last", which in some languages needs to have different forms according to the noun it describes (one of the choices from list box). Whenever the list box value changes, the label should change to match it. But this label is a standalone message in the PO file, so based on what can its translation be scripted?

The solution is to ask the programmer to make the label refresh on every change of the list box value, and to add a ''dynamic context'' to the label's message (using <tt>inContext</tt> method of <tt>KLocalizedString</tt>). The dynamic context is a key-value string pair which indicates the current list box value; the key and possible values should be documented in the message's context. If the programmer did this properly, the translator should now see in the PO file:
<code po>
msgctxt ""
"Part of logical sentence 'Search in files modified >during the last< "
"day|week|...'; provides dynamic context 'last-what': 'd' for day, "
"'w' for week, 'm' for month, 'y' for year."
msgid "during the last"
The translation can now be scripted based on the <tt>last-what</tt> dynamic context:
<code po>
msgstr ""
"duureeng thee leestee"
"duureeng $[by-context last-what 'd|m' 'thee leestee' 'w|y' 'thaa leestaa']"
The <tt>by-context</tt> call takes the context key, followed by pairs of context value matches (regular expressions) and corresponding phrases; in this example, one form is selected for context values <tt>d</tt> and <tt>m</tt>, and another form for <tt>w</tt> and <tt>y</tt>. The definition of this call is:
<syntaxhighlight lang="javascript">
// Select a phrase according to dynamic context.
// The first argument is the context keyword,
// followed by arbitrary numer of pairs of context values and
// corresponding phrases, optionally followed by default phrase.
// Context values are actually regular experessions, so that if one phrase
// corresponds to several context values, it does not have to be repeated
// but its context value can be given e.g. as 'foo|bar|...'.
// If the context was not matched, default phrase is returned if
// it was given; otherwise fallback is signaled.
function selectByContext (/* ctxt_key,
                             valrx_1, phrase_1, ..., valrx_N, phrase_N
                             [, default_phrase] */)
    if (arguments.length < 1) {
        throw Error("Selector by context needs at least one argument.");

    // Collect context value for the given key.
    var ctxtkey = arguments[0];
    var ctxtval = Ts.dynctxt(ctxtkey);

    // Match the context and select the corresponding phrase.
    var phrase;
    for (var i = 1; i < arguments.length; i += 2) {
        if (ctxtval.match(RegExp(arguments[i]))) {
            phrase = arguments[i + 1];

    // If context was not matched, select default phrase or signal fallback.
    if (phrase == undefined) {
        if (arguments.length % 2 == 0) {
            phrase = arguments[arguments.length - 1];
        } else {
            throw Ts.fallback();

    return phrase;
Ts.setcall("by-context", selectByContext);
// NOTE: You can replace "by-context" in the line above with any UTF-8 string,
// e.g. one in your language so that it blends nicely inside POs.

Content is available under Creative Commons License SA 4.0 unless otherwise noted.