blob: e53de02b891b85c4856eba13e51139c14e7b5150 [file] [log] [blame]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>i18n :: Apache Isis</title>
<link rel="canonical" href="https://isis.apache.org/userguide/2.0.0-M3/btb/i18n.html">
<meta name="generator" content="Antora 2.2.0">
<link rel="stylesheet" href="../../../_/css/site.css">
<link rel="stylesheet" href="../../../_/css/site-custom.css">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,700,700i|Raleway:300,400,500,700,800|Montserrat:300,400,700" rel="stylesheet">
<link rel="home" href="https://isis.apache.org" title="Apache Isis">
<link rel="next" href="headless-access.html" title="Headless Access">
<link rel="prev" href="about.html" title="Beyond the Basics">
</head>
<body class="article">
<header class="header">
<nav class="navbar">
<div class="navbar-brand">
<a class="navbar-item" href="https://isis.apache.org">
<span class="icon">
<img src="../../../_/img/isis-logo-48x48.png"></img>
</span>
<span>Apache Isis</span>
</a>
<button class="navbar-burger" data-target="topbar-nav">
<span></span>
<span></span>
<span></span>
</button>
</div>
<div id="topbar-nav" class="navbar-menu">
<a class="navbar-end">
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link" href="#">Quick Start</a>
<div class="navbar-dropdown">
<span class="navbar-item navbar-heading">Starter Apps</span>
<a class="navbar-item" href="../../../docs/latest/starters/helloworld.html">Hello World</a>
<a class="navbar-item" href="../../../docs/latest/starters/simpleapp.html">Simple App</a>
<hr class="navbar-divider"/>
<span class="navbar-item navbar-heading">Demos &amp; Tutorials</span>
<a class="navbar-item" href="../../../docs/latest/demo/about.html">Demo App</a>
<a class="navbar-item" href="https://danhaywood.gitlab.io/isis-petclinic-tutorial-docs/petclinic/1.16.2/intro.html">Petclinic (tutorial)</a>
<hr class="navbar-divider"/>
<span class="navbar-item navbar-heading">Resources</span>
<a class="navbar-item" href="../../../docs/latest/resources/cheatsheet.html">Cheatsheet</a>
<a class="navbar-item" href="../../../docs/latest/resources/icons.html">Icons</a>
</div>
</div>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link" href="#">Guides</a>
<div class="navbar-dropdown">
<span class="navbar-item navbar-heading">Development</span>
<a class="navbar-item" href="../../../setupguide/latest/about.html">Setup Guide</a>
<hr class="navbar-divider"/>
<span class="navbar-item navbar-heading">Core</span>
<a class="navbar-item" href="../../../userguide/latest/about.html">User Guide</a>
<a class="navbar-item" href="../../../refguide/latest/about.html">Reference Guide</a>
<a class="navbar-item" href="../../../testing/latest/about.html">Testing Guide</a>
</div>
</div>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link" href="#">Libraries</a>
<div class="navbar-dropdown">
<span class="navbar-item navbar-heading">For Use in Apps</span>
<a class="navbar-item" href="../../../subdomains/latest/about.html">Subdomain Libraries</a>
<a class="navbar-item" href="../../../valuetypes/latest/about.html">Value Types</a>
<hr class="navbar-divider"/>
<span class="navbar-item navbar-heading">Integrate between Apps</span>
<a class="navbar-item" href="../../../mappings/latest/about.html">Bounded Context Mapping Libraries</a>
<hr class="navbar-divider"/>
<span class="navbar-item navbar-heading">Other</span>
<a class="navbar-item" href="../../../incubator/latest/about.html">Incubator</a>
<a class="navbar-item" href="../../../legacy/latest/about.html">Legacy</a>
</div>
</div>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link" href="#">Components</a>
<div class="navbar-dropdown">
<span class="navbar-item navbar-heading">Viewers</span>
<a class="navbar-item" href="../../../vw/latest/about.html">Wicket UI</a>
<a class="navbar-item" href="../../../vro/latest/about.html">Restful Objects (REST)</a>
<hr class="navbar-divider"/>
<span class="navbar-item navbar-heading">Security</span>
<a class="navbar-item" href="../../../security/latest/about.html">Security Guide</a>
<hr class="navbar-divider"/>
<span class="navbar-item navbar-heading">Persistence</span>
<a class="navbar-item" href="../../../pjdo/latest/about.html">DataNucleus (JDO)</a>
<hr class="navbar-divider"/>
<span class="navbar-item navbar-heading">Extensions</span>
<a class="navbar-item" href="../../../extensions/latest/about.html">Extensions Catalog</a>
</div>
</div>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link" href="#">Support</a>
<div class="navbar-dropdown">
<span class="navbar-item navbar-heading">Contact</span>
<a class="navbar-item" href="../../../docs/latest/support/slack-channel.html">Slack</a>
<a class="navbar-item" href="../../../docs/latest/support/mailing-list.html">Mailing Lists</a>
<a class="navbar-item" href="https://issues.apache.org/jira/browse/ISIS">JIRA</a>
<a class="navbar-item" href="https://stackoverflow.com/questions/tagged/isis">Stack Overflow</a>
<hr class="navbar-divider"/>
<span class="navbar-item navbar-heading">Releases</span>
<a class="navbar-item" href="../../../docs/latest/downloads/how-to.html">Downloads</a>
<a class="navbar-item" href="../../../relnotes/latest/about.html">Release Notes</a>
<a class="navbar-item" href="../../../docs/latest/archive/1-x.html">Archive (1.x)</a>
<hr class="navbar-divider"/>
<span class="navbar-item navbar-heading">Framework</span>
<a class="navbar-item" href="../../../conguide/latest/about.html">Contributors' Guide</a>
<a class="navbar-item" href="../../../comguide/latest/about.html">Committers' Guide</a>
<a class="navbar-item" href="../../../core/latest/about.html">Core Design</a>
</div>
</div>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link" href="#">ASF</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="http://www.apache.org/">Apache Homepage</a>
<a class="navbar-item" href="https://www.apache.org/events/current-event">Events</a>
<a class="navbar-item" href="https://www.apache.org/licenses/">Licenses</a>
<a class="navbar-item" href="https://www.apache.org/security/">Security</a>
<a class="navbar-item" href="https://www.apache.org/foundation/sponsorship.html">Sponsorship</a>
<a class="navbar-item" href="https://www.apache.org/foundation/thanks.html">Thanks</a>
<hr class="navbar-divider"/>
<a class="navbar-item" href="https://whimsy.apache.org/board/minutes/Isis.html">PMC board minutes</a>
</div>
</div>
<a class="navbar-item" href="../../../docs/latest/about.html">
<span class="icon">
<img src="../../../_/img/home.png"></img>
</span>
</a>
</div>
</div>
</nav>
</header>
<div class="body ">
<div class="nav-container" data-component="userguide" data-version="2.0.0-M3">
<aside class="nav">
<div class="panels">
<div class="nav-panel-pagination">
<a class="page-previous" rel="prev" href="about.html" title="Beyond the Basics"><span></span></a>
<a class="page-next" rel="next"
href="headless-access.html" title="Headless Access"><span></span></a>
<!--
page.parent doesn't seem to be set...
<a class="page-parent" rel="prev" href="about.html" title="Beyond the Basics"><span></span></a>
-->
</div>
<div class="nav-panel-menu is-active" data-panel="menu">
<nav class="nav-menu">
<h3 class="title"><a href="../about.html">User Guide</a></h3>
<ul class="nav-list">
<li class="nav-item" data-depth="0">
<ul class="nav-list">
<li class="nav-item" data-depth="1">
<a class="nav-link" href="../fun/concepts-patterns.html">Concepts &amp; Patterns</a>
</li>
<li class="nav-item" data-depth="1">
<a class="nav-link" href="../fun/overview.html">Overview</a>
</li>
<li class="nav-item" data-depth="1">
<a class="nav-link" href="../fun/domain-entities-and-services.html">Domain Entities &amp; Services</a>
</li>
<li class="nav-item" data-depth="1">
<a class="nav-link" href="../fun/object-members.html">Object Members</a>
</li>
<li class="nav-item" data-depth="1">
<a class="nav-link" href="../fun/ui.html">UI Layout &amp; Hints</a>
</li>
<li class="nav-item" data-depth="1">
<a class="nav-link" href="../fun/business-rules.html">Business Rules</a>
</li>
<li class="nav-item" data-depth="1">
<a class="nav-link" href="../fun/drop-downs-and-defaults.html">Drop downs and Defaults</a>
</li>
<li class="nav-item" data-depth="1">
<a class="nav-link" href="../fun/meta-annotations.html">Meta-annotations</a>
</li>
<li class="nav-item" data-depth="1">
<a class="nav-link" href="../fun/view-models.html">View Models</a>
</li>
<li class="nav-item" data-depth="1">
<a class="nav-link" href="../fun/mixins.html">Mixins</a>
</li>
<li class="nav-item" data-depth="1">
<a class="nav-link" href="../fun/modules.html">Modules</a>
</li>
<li class="nav-item" data-depth="1">
<button class="nav-item-toggle"></button>
<a class="nav-link" href="about.html">Beyond the Basics</a>
<ul class="nav-list">
<li class="nav-item is-current-page" data-depth="2">
<a class="nav-link" href="i18n.html">i18n</a>
</li>
<li class="nav-item" data-depth="2">
<a class="nav-link" href="headless-access.html">Headless Access</a>
</li>
<li class="nav-item" data-depth="2">
<a class="nav-link" href="hints-and-tips.html">Hints-n-Tips</a>
</li>
<li class="nav-item" data-depth="2">
<a class="nav-link" href="programming-model.html">Programming Model</a>
</li>
</ul>
</li>
<li class="nav-item" data-depth="1">
<button class="nav-item-toggle"></button>
<span class="nav-text">Extensions</span>
<ul class="nav-list">
<li class="nav-item" data-depth="2">
<a class="nav-link" href="../flyway/about.html">Flyway</a>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</nav>
</div>
<div class="nav-panel-explore" data-panel="explore">
<div class="context">
<span class="title">User Guide</span>
<span class="version">2.0.0-M3</span>
</div>
<ul class="components">
<li class="component">
<span class="title"> </span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../docs/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">BC Mappings Catalog</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../mappings/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">Committers' Guide</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../comguide/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">Contributors' Guide</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../conguide/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">Design Docs</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../core/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">Extensions Catalog</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../extensions/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">Incubator Catalog</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../incubator/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">JDO/DataNucleus</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../pjdo/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">Legacy Catalog</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../legacy/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">Reference Guide</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../refguide/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">Release Notes</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../relnotes/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">Restful Objects Viewer</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../vro/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">Security Guide</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../security/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">Setup Guide</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../setupguide/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">Subdomains Catalog</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../subdomains/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">Testing Guide</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../testing/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component is-current">
<span class="title">User Guide</span>
<ul class="versions">
<li class="version is-current is-latest">
<a href="../about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">Value Types Catalog</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../valuetypes/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
<li class="component">
<span class="title">Wicket Viewer</span>
<ul class="versions">
<li class="version is-latest">
<a href="../../../vw/2.0.0-M3/about.html">2.0.0-M3</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</aside>
</div>
<main role="main">
<div class="toolbar" role="navigation">
<button class="nav-toggle"></button>
<a href="../../../docs/2.0.0-M3/about.html" class="home-link"></a>
<nav class="breadcrumbs" aria-label="breadcrumbs">
<ul>
<li><a href="../about.html">User Guide</a></li>
<li><a href="about.html">Beyond the Basics</a></li>
<li><a href="i18n.html">i18n</a></li>
</ul>
</nav>
<div class="edit-this-page"><a href="https://github.com/apache/isis/edit/2.0.0-M3/api/adoc/userguide/modules/btb/pages/i18n.adoc">Edit</a></div>
</div>
<article class="doc">
<a name="section-top"></a>
<h1 class="page">i18n</h1>
<div id="preamble">
<div class="sectionbody">
<div class="paragraph">
<p>Apache Isis' support for internationlization (i18n) allows every element of the domain model (the class names, property names, action names, parameter names and so forth) to be translated.</p>
</div>
<div class="paragraph">
<p>It also supports translations of messages raised imperatively, by which we mean as the result of a call to <code>title()</code> to obtain an object&#8217;s title, or messages resulting from any business rule violations (eg <a href="#refguide:applib-cm:methods.adoc#disable" class="page unresolved"><code>disable&#8230;&#8203;()</code></a> or <a href="#refguide:applib-cm:methods.adoc#validate" class="page unresolved"><code>validate&#8230;&#8203;()</code></a>, and so on.</p>
</div>
<div class="paragraph">
<p>The <a href="../../../vw/2.0.0-M3/about.html" class="page">Wicket viewer</a> (that is, its labels and messages) is also internationalized using the same mechanism.
If no translations are available, then the Wicket viewer falls back to using Wicket resource bundles.</p>
</div>
<div class="paragraph">
<p>Isis does not translate the values of your domain objects, though.
So, if you have a domain concept such as <code>Country</code> whose name is intended to be localized according to the current user, you will need to model this yourself.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="implementation-approach"><a class="anchor" href="#implementation-approach"></a>Implementation Approach</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Most Java frameworks tackle i18n by using Java&#8217;s own <code>ResourceBundle</code> API.
However, there are some serious drawbacks in this approach, including:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>if a string appears more than once (eg "name" or "description") then it must be translated everywhere it appears in every resource bundle file</p>
</li>
<li>
<p>there is no support for plural forms (see this <a href="http://stackoverflow.com/questions/14326653/java-internationalization-i18n-with-proper-plurals/14327683#14327683">SO answer</a>)</p>
</li>
<li>
<p>there is no tooling support for translators</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Apache Isis therefore takes a different approach, drawing inspiration from GNU&#8217;s <a href="https://www.gnu.org/software/gettext/manual/index.html">gettext</a> API and specifically its <code>.pot</code> and <code>.po</code> files.
These are intended to be used as follows:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>the <code>.pot</code> (portable object template) file holds the message text to be translated</p>
</li>
<li>
<p>this file is translated into multiple <code>.po</code> (portable object) files, one per supported locale</p>
</li>
<li>
<p>these <code>.po</code> files are renamed according to their locale, and placed into the 'appropriate' location to be picked up by the runtime.
The name of each <code>.po</code> resolved in a very similar way to resource bundles.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>The format of the <code>.pot</code> and <code>.po</code> files is identical; the only difference is that the <code>.po</code> file has translations for each of the message strings.
These message strings can also have singular and plural forms.</p>
</div>
<div class="admonitionblock important">
<table>
<tr>
<td class="icon">
<i class="fa icon-important" title="Important"></i>
</td>
<td class="content">
<div class="paragraph">
<p>Although Apache Isis' implementation is modelled after GNU&#8217;s API, it does <em>not</em> use any GNU software.
This is for two reasons: (a) to simplify the toolchain/developer experience, and (b) because GNU software is usually GPL, which would be incompatible with the Apache license.</p>
</div>
</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>This design tackles all the issues of <code>ResourceBundle</code>s:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>the <code>.po</code> message format is such that any given message text to translate need only be translated once, even if it appears in multiple places in the application (eg "Name")</p>
</li>
<li>
<p>the <code>.po</code> message format includes translations for (optional) plural form as well as singular form</p>
</li>
<li>
<p>there are lots of freely available editors <a href="https://www.google.co.uk/search?q=.po+file+editor">to be found</a>, many summarized on this <a href="https://www.drupal.org/node/11131">Drupal.org</a> webpage.<br>
+ In fact, there are also online communities/platforms of translators to assist with translating files.
One such is <a href="https://crowdin.com/">crowdin</a> (nb: this link does not imply endorsement).</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>In Apache Isis' implementation, if the translation is missing from the <code>.po</code> file then the original message text from the <code>.pot</code> file will be returned.
In fact, it isn&#8217;t even necessary for there to be any <code>.po</code> files; <code>.po</code> translations can be added piecemeal as the need arises.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="translationservice"><a class="anchor" href="#translationservice"></a><code>TranslationService</code></h2>
<div class="sectionbody">
<div class="paragraph">
<p>The cornerstone of Apache Isis' support for i18n is the <code>TranslationService</code> service.
This is defined in the applib with the following API:</p>
</div>
<div class="listingblock">
<div class="title">TranslationService.java</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">public interface TranslationService {
String translate( <i class="conum" data-value="1"></i><b>(1)</b>
final String context,
final String text);
String translate( <i class="conum" data-value="2"></i><b>(2)</b>
final String context,
final String singularText,
final String pluralText,
int num);
Mode getMode(); <i class="conum" data-value="3"></i><b>(3)</b>
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>is to translate the singular form of the text</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>is to translate the plural form of the text</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>indicates whether the translation service is in read or write mode.</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>where <code>Mode</code> is:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">enum Mode {
DISABLED
, READ
, WRITE
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>The <code>translate(&#8230;&#8203;)</code> methods are closely modelled on GNU&#8217;s gettext API.
The first version is used when no translation is required, the second is when both a singular and plural form will be required, with the <code>num</code> parameter being used to select which is returned.
In both cases the <code>context</code> parameter provides some contextual information for the translator; this generally corresponds to the class member.</p>
</div>
<div class="paragraph">
<p>The mode meanwhile determines the behaviour of the service.
More on this below.</p>
</div>
<div class="sect2">
<h3 id="translationservicepo"><a class="anchor" href="#translationservicepo"></a><code>TranslationServicePo</code></h3>
<div class="paragraph">
<p>Isis provides a default implementation of <code>TranslationService</code>, namely <code>TranslationServicePo</code>.</p>
</div>
<div class="paragraph">
<p>If the service is running in the normal read mode, then it simply provides translations for the locale of the current user.
This means locates the appropriate <code>.po</code> file (based on the requesting user&#8217;s locale), finds the translation and returns it.</p>
</div>
<div class="paragraph">
<p>If however the service is configured to run in write mode, then it instead records the fact that the message was requested to be translated (a little like a spy/mock in unit testing mock), and returns the original message.
The service can then be queried to discover which messages need to be translated.
All requested translations are written into the <code>.pot</code> file.</p>
</div>
<div class="paragraph">
<p>To make the service as convenient as possible to use, the service configures itself as follows:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>if running in prototype mode <a href="../../../refguide/2.0.0-M3/config/about.html#deployment-types" class="page">deployment type</a> or during integration tests, then the service runs in <strong>write</strong> mode, in which case it records all translations into the <code>.pot</code> file.
The <code>.pot</code> file is written out when the system is shutdown.</p>
</li>
<li>
<p>if running in server (production) mode a<a href="../../../refguide/2.0.0-M3/config/about.html#deployment-types" class="page">deployment type</a>, then the service runs in <strong>read</strong> mode.
It is also possible to set a configuration setting in <code>application.properties</code> to force read mode even if running in prototype mode (useful to manually test/demo the translations).</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>When running in write mode the original text is returned to the caller untranslated.
If in read mode, then the translated <code>.po</code> files are read and translations provided as required.</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="imperative-messages"><a class="anchor" href="#imperative-messages"></a>Imperative messages</h2>
<div class="sectionbody">
<div class="paragraph">
<p>The <code>TranslationService</code> is used internally by Apache Isis when building up the metamodel; the name and description of every class, property, collection, action and action parameter is automatically translated.
Thus the simple act of bootstrapping Apache Isis will cause most of the messages requiring translation (that is: those for the Apache Isis metamodel) to be captured by the <code>TranslationService</code>.</p>
</div>
<div class="paragraph">
<p>However, for an application to be fully internationalized, any validation messages (from either <code>disableXxx()</code> or <code>validateXxx()</code> supporting methods) and also possibly an object&#8217;s title (from the <code>title()</code> method) will also require translation.
Moreover, these messages must be captured in the <code>.pot</code> file such that they can be translated.</p>
</div>
<div class="sect2">
<h3 id="translatablestring"><a class="anchor" href="#translatablestring"></a><code>TranslatableString</code></h3>
<div class="paragraph">
<p>The first part of the puzzle is tackled by an extension to Apache Isis' programming model.
Whereas previously the <code>disableXxx()</code> / <code>validateXxx()</code> / <code>title()</code> methods could only return a <code>java.lang.String</code>, they may now optionally return a <code>TranslatableString</code> (defined in Isis applib) instead.</p>
</div>
<div class="paragraph">
<p>For example (based on similar code in the <a href="../../../docs/2.0.0-M3/starters/simpleapp.html" class="page">SimpleApp</a> starter app):</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">public TranslatableString validateUpdateName(final String name) {
return name.contains("!")
? TranslatableString.tr("Exclamation mark is not allowed")
: null;
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>This corresponds to the following entry in the <code>.pot</code> file:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-ini hljs" data-lang="ini">#: dom.simple.SimpleObject#updateName()
msgid "Exclamation mark is not allowed"
msgstr ""</code></pre>
</div>
</div>
<div class="paragraph">
<p>The full API of <code>TranslatableString</code> is modelled on the design of GNU gettext (in particular the <a href="https://code.google.com/p/gettext-commons/wiki/Tutorial">gettext-commons</a> library):</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">public final class TranslatableString {
public static TranslatableString tr( <i class="conum" data-value="1"></i><b>(1)</b>
final String pattern,
final Object... paramArgs) {
// ...
}
public static TranslatableString trn( <i class="conum" data-value="2"></i><b>(2)</b>
final String singularPattern,
final String pluralPattern,
final int number,
final Object... paramArgs) {
// ...
}
public String translate( <i class="conum" data-value="3"></i><b>(3)</b>
final TranslationService translationService,
final String context) {
// ...
}
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>returns a translatable string with a single pattern for both singular and plural forms.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>returns a translatable string with different patterns for singular and plural forms; the one to use is determined by the 'number' argument</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>translates the string using the provided <code>TranslationService</code>, using the appropriate singular/regular or plural form, and interpolating any arguments.</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>The interpolation uses the format <code>{xxx}</code>, where the placeholder can occur multiple times.</p>
</div>
<div class="paragraph">
<p>For example:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">final TranslatableString ts = TranslatableString.tr(
"My name is {lastName}, {firstName} {lastName}.",
"lastName", "Bond", "firstName", "James");</code></pre>
</div>
</div>
<div class="paragraph">
<p>would interpolate (for the English locale) as "My name is Bond, James Bond".</p>
</div>
<div class="paragraph">
<p>For a German user, on the other hand, if the translation in the corresponding <code>.po</code> file was:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-ini hljs" data-lang="ini">#: xxx.yyy.Whatever#context()
msgid "My name is {lastName}, {firstName} {lastName}."
msgstr "Ich heisse {firstName} {lastName}."</code></pre>
</div>
</div>
<div class="paragraph">
<p>then the translation would be: "Ich heisse James Bond".</p>
</div>
<div class="paragraph">
<p>The same class is used in <a href="../../../refguide/2.0.0-M3/applib-svc/MessageService.html" class="page"><code>MessageService</code></a> so that you can raise translatable info, warning and error messages; each of the relevant methods are overloaded.</p>
</div>
<div class="paragraph">
<p>For example:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">public interface MessageService {
void informUser(String message);
void informUser(
TranslatableMessage message,
final Class&lt;?&gt; contextClass, final String contextMethod); <i class="conum" data-value="1"></i><b>(1)</b>
...
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>concatenated together to form the context for the <code>.pot</code> file.</td>
</tr>
</table>
</div>
</div>
<div class="sect2">
<h3 id="translatableexception"><a class="anchor" href="#translatableexception"></a><code>TranslatableException</code></h3>
<div class="paragraph">
<p>Another mechanism by which messages can be rendered to the user are as the result of exception messages thrown and recognized by an <a href="../../../refguide/2.0.0-M3/applib-svc/ExceptionRecognizerService.html" class="page"><code>ExceptionRecognizer</code></a>.</p>
</div>
<div class="paragraph">
<p>In this case, if the exception implements <code>TranslatableException</code>, then the message will automatically be translated before being rendered.
The <code>TranslatableException</code> itself takes the form:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">public interface TranslatableException {
TranslatableString getTranslatableMessage(); <i class="conum" data-value="1"></i><b>(1)</b>
String getTranslationContext(); <i class="conum" data-value="2"></i><b>(2)</b>
}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>the message to translate.<br>
If returns <code>null</code>, then the <code>Exception#getMessage()</code> is used as a fallback</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>the context to use when translating the message</td>
</tr>
</table>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="wicket-viewer"><a class="anchor" href="#wicket-viewer"></a>Wicket Viewer</h2>
<div class="sectionbody">
<div class="paragraph">
<p>The <a href="../../../vw/2.0.0-M3/about.html" class="page">Wicket viewer</a> (its labels and messages) is also internationalized using the <code>TranslationService</code>.
This is done through an Isis-specific implementation of the Wicket framework&#8217;s <code>org.apache.wicket.Localizer</code> class, namely <code>LocalizerForIsis</code>.</p>
</div>
<div class="paragraph">
<p>The Wicket <code>Localizer</code> defines the following API:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">public String getString(
final String key, <i class="conum" data-value="1"></i><b>(1)</b>
final Component component, <i class="conum" data-value="2"></i><b>(2)</b>
final IModel&lt;?&gt; model,
final Locale locale,
final String style,
final String defaultValue)
throws MissingResourceException { /* ... */ }</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>The key to obtain the resource for</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>The component to get the resource for (if any)</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>For example, <code>key</code> might be a value such as "okLabel", while <code>component</code> an internal class of the Wicket viewer, such as <code>EntityPropertiesForm</code>.</p>
</div>
<div class="paragraph">
<p>The <code>LocalizerForIsis</code> implementation uses the <code>key</code> as the <code>msgId</code>, while the fully qualified class name of the <code>component</code> is used as a context.
There is one exception to this: if the component is the third-party select2 component (used for drop-downs), then that class name is used directly.</p>
</div>
<div class="paragraph">
<p>In the main, using Isis' i18n support means simply adding the appropriate translations to the <code>translation.po</code> file, for each locale that you require.
If the translations are missing then the original translations from the Wicket resource bundles will be used instead.</p>
</div>
<div class="sect2">
<h3 id="commonly-used"><a class="anchor" href="#commonly-used"></a>Commonly used</h3>
<div class="paragraph">
<p>Most of the translation requirements can be covered by adding in the following <code>msgId</code>s:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-properties hljs" data-lang="properties">#: org.apache.isis.viewer.wicket.ui.pages.entity.EntityPage
msgid "CollectionContentsAsAjaxTablePanelFactory.Table"
msgstr "Table"
#: org.apache.isis.viewer.wicket.ui.pages.entity.EntityPage
msgid "CollectionContentsAsUnresolvedPanel.Hide"
msgstr "Hide"
#: org.apache.isis.viewer.wicket.ui.pages.entity.EntityPage
msgid "aboutLabel"
msgstr "About"
#: org.apache.isis.viewer.wicket.ui.pages.entity.EntityPage
msgid "cancelLabel"
msgstr "Cancel"
#: org.apache.isis.viewer.wicket.ui.pages.entity.EntityPage
msgid "datatable.no-records-found"
msgstr "No Records Found"
#: org.apache.isis.viewer.wicket.ui.pages.entity.EntityPage
msgid "editLabel"
msgstr "Edit"
#: org.wicketstuff.select2.Select2Choice
msgid "inputTooShortPlural"
msgstr "Please enter {number} more characters"
#: org.wicketstuff.select2.Select2Choice
msgid "inputTooShortSingular"
msgstr "Please enter 1 more character"
#: org.wicketstuff.select2.Select2Choice
msgid "loadMore"
msgstr "Load more"
#: org.apache.isis.viewer.wicket.ui.pages.entity.EntityPage
msgid "logoutLabel"
msgstr "Logout"
#: org.wicketstuff.select2.Select2Choice
msgid "noMatches"
msgstr "No matches"
#: org.apache.isis.viewer.wicket.ui.pages.entity.EntityPage
msgid "okLabel"
msgstr "OK"
#: org.wicketstuff.select2.Select2Choice
msgid "searching"
msgstr "Searching..."
#: org.wicketstuff.select2.Select2Choice
msgid "selectionTooBigPlural"
msgstr "You can only select {limit} items"
#: org.wicketstuff.select2.Select2Choice
msgid "selectionTooBigSingular"
msgstr "You can only select 1 item"</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="loginself-sign-up"><a class="anchor" href="#loginself-sign-up"></a>Login/self-sign-up</h3>
<div class="paragraph">
<p>In addition, there are a reasonably large number of messages that are used for both login and the
<a href="../../../vw/2.0.0-M3/features.html#user-registration" class="page">user registration</a> (self sign-up) and password reset features.</p>
</div>
<div class="paragraph">
<p>These are:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-properties hljs" data-lang="properties">#: org.apache.isis.viewer.wicket.ui.pages.login.WicketSignInPage
msgid "AutoLabel.CSS.required"
msgstr "Required"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.signup.RegistrationFormPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.register.RegisterPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "confirmPasswordLabel"
msgstr "Confirm password"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.signup.RegistrationFormPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.register.RegisterPage
msgid "emailIsNotAvailable"
msgstr "The given email is already in use"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "emailPlaceholder"
msgstr "Enter your email"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.signup.RegistrationFormPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.register.RegisterPage
msgid "emailPlaceholder"
msgstr "Enter an email for the new account"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.signup.RegistrationFormPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.register.RegisterPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "emailLabel"
msgstr "Email"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.signup.RegistrationFormPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.register.RegisterPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "emailSentMessage"
msgstr "An email has been sent to '${email}' for verification."
#: org.apache.isis.viewer.wicket.ui.pages.login.WicketSignInPage
msgid "forgotPasswordLinkLabel"
msgstr "Forgot your password?"
#: org.apache.isis.viewer.wicket.ui.pages.login.WicketSignInPage
msgid "loginHeader"
msgstr "Login"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "noSuchUserByEmail"
msgstr "There is no account with this email"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "noUserForAnEmailValidToken"
msgstr "The account seems to be either already deleted or has changed its email address. Please try again."
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "passwordChangeSuccessful"
msgstr "The password has been changed successfully. You can &lt;a class=\"alert-success\" style=\"text-decoration:underline;\" href=\"${signInUrl}\"&gt;login&lt;/a&gt; now."
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "passwordChangeUnsuccessful"
msgstr "There was a problem while updating the password. Please try again."
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.signup.RegistrationFormPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.register.RegisterPage
#: org.apache.isis.viewer.wicket.ui.pages.login.WicketSignInPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "passwordLabel"
msgstr "Password"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.signup.RegistrationFormPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.register.RegisterPage
#: org.apache.isis.viewer.wicket.ui.pages.login.WicketSignInPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "passwordPlaceholder"
msgstr "Enter password"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "passwordResetExpiredOrInvalidToken"
msgstr "You are trying to reset the password for an expired or invalid token"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "passwordResetHeader"
msgstr "Forgot password"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "passwordResetSubmitLabel"
msgstr "Submit"
#: org.apache.isis.viewer.wicket.ui.pages.login.WicketSignInPage
msgid "registerButtonLabel"
msgstr "Register"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.register.RegisterPage
msgid "registerHeader"
msgstr "Register"
#: org.apache.isis.viewer.wicket.ui.pages.login.WicketSignInPage
msgid "rememberMeLabel"
msgstr "Remember Me"
#: org.apache.isis.viewer.wicket.ui.pages.login.WicketSignInPage
msgid "resetButtonLabel"
msgstr "Reset"
#: org.apache.isis.viewer.wicket.ui.pages.login.WicketSignInPage
msgid "signInButtonLabel"
msgstr "Sign in"
#: org.apache.isis.viewer.wicket.ui.pages.login.WicketSignInPage
msgid "signUpButtonLabel"
msgstr "Don't have an account? Sign up now."
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.signup.RegistrationFormPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.register.RegisterPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "signUpButtonLabel"
msgstr "Verify email"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.signup.RegistrationFormPage
msgid "signUpHeader"
msgstr "Sign Up"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.signup.RegistrationFormPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.register.RegisterPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "usernameIsNotAvailable"
msgstr "The provided username is already in use"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.signup.RegistrationFormPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.register.RegisterPage
#: org.apache.isis.viewer.wicket.ui.pages.login.WicketSignInPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "usernameLabel"
msgstr "Username"
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.signup.RegistrationFormPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.register.RegisterPage
#: org.apache.isis.viewer.wicket.ui.pages.login.WicketSignInPage
#: org.apache.isis.viewer.wicket.ui.pages.accmngt.password_reset.PasswordResetPage
msgid "usernamePlaceholder"
msgstr "Username"</code></pre>
</div>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="integration-testing"><a class="anchor" href="#integration-testing"></a>Integration Testing</h2>
<div class="sectionbody">
<div class="paragraph">
<p>So much for the API; but as noted, it is also necessary to ensure that the required translations are recorded (by the <code>TranslationService</code>) into the <code>.pot</code> file.</p>
</div>
<div class="paragraph">
<p>For this, we recommend that you ensure that all such methods are tested through an <a href="../../../testing/2.0.0-M3/integtestsupport/about.html" class="page">integration test</a> (not unit test).</p>
</div>
<div class="paragraph">
<p>For example, here&#8217;s the corresponding integration test for the "Exclamation mark" example from the simpleapp (above):</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">@BeforeEach
public void setUp() {
// given
simpleObject = fixtureScripts.runPersona(SimpleObject_persona.FOO);
}
@Test
public void fails_validation() {
// expect
InvalidException cause = assertThrows(InvalidException.class, ()-&gt;{
// when
wrap(simpleObject).updateName("new name!");
});
// then
assertThat(cause.getMessage(), containsString("Character '!' is not allowed"));
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>Running this test will result in the framework calling the <code>validateUpdateName(&#8230;&#8203;)</code> method, and thus to record that a translation is required in the <code>.pot</code> file.</p>
</div>
<div class="paragraph">
<p>When the integration tests are complete (that is, when Apache Isis is shutdown), the <code>TranslationServicePo</code> will write out all captured translations to its log (more on this below).
This will include all the translations captured from the Apache Isis metamodel, along with all translations as exercised by the integration tests.</p>
</div>
<div class="paragraph">
<p>To ensure your app is fully internationalized app, you must therefore:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>use <code>TranslatableString</code> rather than <code>String</code> for all validation/disable and title methods.</p>
</li>
<li>
<p>ensure that (at a minimum) all validation messages and title methods are integration tested.</p>
</li>
</ul>
</div>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
<div class="paragraph">
<p>We make no apologies for this requirement: one of the reasons that we decided to implement Apache Isis' i18n support in this way is because it encourages/requires the app to be properly tested.</p>
</div>
<div class="paragraph">
<p>Behind the scenes Apache Isis uses a JUnit 5 extension (<code>ExceptionRecognizerTranslate</code>) to intercept any exceptions that are thrown.
These are simply passed through to the <a href="../../../refguide/2.0.0-M3/applib-svc/ExceptionRecognizerService.html" class="page"><code>ExceptionRecognizerService</code></a>s so that any messages are recorded as requiring translation.</p>
</div>
</td>
</tr>
</table>
</div>
</div>
</div>
<div class="sect1">
<h2 id="escaped-strings"><a class="anchor" href="#escaped-strings"></a>Escaped strings</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Translated messages can be escaped if required, eg to include embedded markup.</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-ini hljs" data-lang="ini">#: com.mycompany.myapp.OrderItem#quantity
msgid "&lt;i&gt;Quantity&lt;/i&gt;"
msgstr "&lt;i&gt;Quantité&lt;/i&gt;"</code></pre>
</div>
</div>
<div class="paragraph">
<p>For this to work, the <code>namedEscaped</code> attribute must be specified using either the <a href="../fun/ui.html#object-layout" class="page">layout file</a>, or using an annotation such as <a href="../../../refguide/2.0.0-M3/applib-ant/PropertyLayout.html" class="page"><code>@PropertyLayout</code></a> or <a href="../../../refguide/2.0.0-M3/applib-ant/ParameterLayout.html" class="page"><code>@ParameterLayout</code></a>.</p>
</div>
<div class="paragraph">
<p>For example:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">import lombok.Getter;
import lombok.Setter;
@PropertyLayout(
named="&lt;i&gt;Quantity&lt;/i&gt;", <i class="conum" data-value="1"></i><b>(1)</b>
namedEscaped=false
)
@Getter @Setter
private Integer quantity;</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>required (even though it won&#8217;t be used when a translation is read; otherwise the escaped flag is ignored)</td>
</tr>
</table>
</div>
</div>
</div>
<div class="sect1">
<h2 id="configuration"><a class="anchor" href="#configuration"></a>Configuration</h2>
<div class="sectionbody">
<div class="paragraph">
<p>There are several different aspects of the translation service that can be configured.</p>
</div>
<div class="sect2">
<h3 id="logging"><a class="anchor" href="#logging"></a>Logging</h3>
<div class="paragraph">
<p>To configure the <code>TranslationServicePo</code> to write to out the <code>translations.pot</code> file, add the following to the <em>integtests</em> <code>log4j-test.xml</code> file:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-xml hljs" data-lang="xml">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;Configuration status="WARN"&gt;
&lt;!-- ... --&gt;
&lt;Appenders&gt;
&lt;Console name="Console" target="SYSTEM_OUT" follow="true"&gt;
&lt;PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" /&gt;
&lt;/Console&gt;
&lt;File name="TranslationsPoFile" fileName="translations.po"
append="false" immediateFlush="true"&gt;
&lt;PatternLayout&gt;
&lt;Pattern&gt;%m%n&lt;/Pattern&gt;
&lt;/PatternLayout&gt;
&lt;/File&gt;
&lt;/Appenders&gt;
&lt;Loggers&gt;
&lt;!-- ... --&gt;
&lt;logger
name="org.apache.isis.core.runtimeservices.i18n.po.PoWriter"
level="info"&gt;
&lt;AppenderRef ref="TranslationsPoFile"/&gt;
&lt;/logger&gt;
&lt;/Loggers&gt;
&lt;/Configuration&gt;</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="location-of-the-po-files"><a class="anchor" href="#location-of-the-po-files"></a>Location of the <code>.po</code> files</h3>
<div class="admonitionblock warning">
<table>
<tr>
<td class="icon">
<i class="fa icon-warning" title="Warning"></i>
</td>
<td class="content">
TODO - v2 - need to verify this.
</td>
</tr>
</table>
</div>
<div class="paragraph">
<p>The default location of the translated <code>.po</code> files is in the <code>WEB-INF</code> directory.
These are named and searched for similarly to regular Java resource bundles.</p>
</div>
<div class="paragraph">
<p>For example, assuming these translations:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-ini hljs" data-lang="ini">/WEB-INF/translations-en-US.po
/translations-en.po
/translations-fr-FR.po
/translations.po</code></pre>
</div>
</div>
<div class="paragraph">
<p>then:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>a user with <code>en-US</code> locale will use <code>translations-en-US.po</code></p>
</li>
<li>
<p>a user with <code>en-GB</code> locale will use <code>translations-en.po</code></p>
</li>
<li>
<p>a user with <code>fr-FR</code> locale will use <code>translations-fr-FR.po</code></p>
</li>
<li>
<p>a user with <code>fr-CA</code> locale will use <code>translations.po</code></p>
</li>
</ul>
</div>
<div class="paragraph">
<p>The basename for translation files is always <code>translations</code>; this cannot be altered.</p>
</div>
</div>
<div class="sect2">
<h3 id="externalized-translation-files"><a class="anchor" href="#externalized-translation-files"></a>Externalized translation files</h3>
<div class="admonitionblock warning">
<table>
<tr>
<td class="icon">
<i class="fa icon-warning" title="Warning"></i>
</td>
<td class="content">
TODO - v2 - need to figure out how to configure Spring Boot so that these resources can be read from an external location.
</td>
</tr>
</table>
</div>
</div>
<div class="sect2">
<h3 id="force-read-mode-or-disable"><a class="anchor" href="#force-read-mode-or-disable"></a>Force read mode, or disable</h3>
<div class="paragraph">
<p>As noted above, if running in prototype mode then <code>TranslationServicePo</code> will be in write mode, if in production mode then will be in read mode.
To force read mode (ie use translations) even if in prototype mode, add the <a href="../../../refguide/2.0.0-M3/config/sections/isis.core.runtime-services.html#isis.core.runtime-services.translation.po.mode" class="page"><code>isis.core.runtime-services.translation.po.mode</code></a> configuration property:</p>
</div>
<div class="listingblock">
<div class="title">application.properties</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-ini hljs" data-lang="ini">isis.core.runtime-services.translation.po.mode=read</code></pre>
</div>
</div>
<div class="paragraph">
<p>It&#8217;s also possible to disable the service completely.
This can sometimes be useful in integration tests.</p>
</div>
<div class="listingblock">
<div class="title">application.properties</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-ini hljs" data-lang="ini">isis.core.runtime-services.translation.po.mode=disabled</code></pre>
</div>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="supporting-services"><a class="anchor" href="#supporting-services"></a>Supporting services</h2>
<div class="sectionbody">
<div class="paragraph">
<p>The <code>TranslationServicePo</code> has a number of supporting/related services.</p>
</div>
<div class="sect2">
<h3 id="localeprovider"><a class="anchor" href="#localeprovider"></a><code>LocaleProvider</code></h3>
<div class="paragraph">
<p>The <a href="../../../refguide/2.0.0-M3/applib-svc/LocaleProvider.html" class="page"><code>LocaleProvider</code></a> API is used by the <code>TranslationServicePo</code> implementation to obtain the locale of the "current user".</p>
</div>
<div class="paragraph">
<p>An implementation is provided by the Wicket viewer.</p>
</div>
<div class="admonitionblock caution">
<table>
<tr>
<td class="icon">
<i class="fa icon-caution" title="Caution"></i>
</td>
<td class="content">
<div class="paragraph">
<p>There is no equivalent implementation of <code>LocaleProvider</code> for Restful Objects viewer; requests through Restful Objects are not translated.</p>
</div>
</td>
</tr>
</table>
</div>
</div>
<div class="sect2">
<h3 id="translationsresolver"><a class="anchor" href="#translationsresolver"></a><code>TranslationsResolver</code></h3>
<div class="paragraph">
<p>The <code>TranslationResolver</code> is used by the <code>TranslationService</code> implementation to lookup translations for a specified locale.
It is this service that reads from the <code>WEB-INF/</code> (or externalized directory).</p>
</div>
</div>
<div class="sect2">
<h3 id="translationservicepomenu"><a class="anchor" href="#translationservicepomenu"></a><code>TranslationServicePoMenu</code></h3>
<div class="paragraph">
<p>The <code>TranslationServicePoMenu</code> provides a couple of menu actions in the UI (prototype mode only) that interacts with the underlying <code>TranslationServicePo</code>:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>the <code>downloadTranslationsFile()</code> action - available only in write mode - allows the current <code>.pot</code> file to be downloaded.<br></p>
<div class="admonitionblock note">
<table>
<tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
<div class="paragraph">
<p>While this will contain all the translations from the metamodel, it will not necessarily contain all translations for all imperative methods returning <code>TranslatableString</code> instances; which are present and which are missing will depend on which imperative methods have been called (recorded by the service) prior to downloading.</p>
</div>
</td>
</tr>
</table>
</div>
</li>
<li>
<p>the <code>clearTranslationsCache()</code> action - available only in read mode - will clear the cache so that new translations can be loaded.<br>
+ This allows a translator to edit the appropriate <code>translations-xx-XX.po</code> file and check the translation is correct without having to restart the app.</p>
</li>
</ul>
</div>
</div>
</div>
</div>
</article>
<aside class="article-aside toc" role="navigation">
<p class="toc-title">On this page</p>
<div id="article-toc"></div>
</aside>
</main>
</div>
<footer class="footer">
<div class="content">
<div class="copyright">
<p>
Copyright © 2010~2020 The Apache Software Foundation, licensed under the Apache License, v2.0.
<br/>
Apache, the Apache feather logo, Apache Isis, and the Apache Isis project logo are all trademarks of The Apache Software Foundation.
</p>
</div>
<div class="revision">
<p>Revision: SNAPSHOT</p>
</div>
</div>
</footer>
<script src="../../../_/js/site.js"></script>
<script async src="../../../_/js/vendor/highlight.js"></script>
<script src="../../../_/js/vendor/jquery-3.4.1.min.js"></script>
<script src="../../../_/js/vendor/jquery-ui-1.12.1.custom.widget-only.min.js"></script>
<script src="../../../_/js/vendor/jquery.tocify.min.js"></script>
<script>
$(function() {
$("#article-toc").tocify( {
showEffect: "slideDown",
hashGenerator: "pretty",
hideEffect: "slideUp",
selectors: "h2, h3",
scrollTo: 120,
smoothScroll: true,
theme: "jqueryui",
highlightOnScroll: true
} );
});
</script>
</body>
</html>