| <!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 & 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 & 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 & 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 & 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’s title, or messages resulting from any business rule violations (eg <a href="#refguide:applib-cm:methods.adoc#disable" class="page unresolved"><code>disable…​()</code></a> or <a href="#refguide:applib-cm:methods.adoc#validate" class="page unresolved"><code>validate…​()</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’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’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’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’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(…​)</code> methods are closely modelled on GNU’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’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’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<?> 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’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<?> 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 <a class=\"alert-success\" style=\"text-decoration:underline;\" href=\"${signInUrl}\">login</a> 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’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, ()->{ |
| |
| // 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(…​)</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 "<i>Quantity</i>" |
| msgstr "<i>Quantité</i>"</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="<i>Quantity</i>", <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’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"><?xml version="1.0" encoding="UTF-8"?> |
| <Configuration status="WARN"> |
| <!-- ... --> |
| <Appenders> |
| <Console name="Console" target="SYSTEM_OUT" follow="true"> |
| <PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" /> |
| </Console> |
| <File name="TranslationsPoFile" fileName="translations.po" |
| append="false" immediateFlush="true"> |
| <PatternLayout> |
| <Pattern>%m%n</Pattern> |
| </PatternLayout> |
| </File> |
| </Appenders> |
| <Loggers> |
| <!-- ... --> |
| <logger |
| name="org.apache.isis.core.runtimeservices.i18n.po.PoWriter" |
| level="info"> |
| <AppenderRef ref="TranslationsPoFile"/> |
| </logger> |
| </Loggers> |
| </Configuration></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’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> |