ISIS-903: allow translations to be read from externalized config location.

In addition, pick up value of deploymentType by reading 
IsisContext.getDeploymentType() rather than the hacky way of reading config; 
this should work in all situations (org.apache.isis.WebServer, integtetss, 
tomcat etc).  This necessitates moving the TranslatioServiePo implementation 
(and supporting classes) from core/metamodel to core/runtime.


Project: http://git-wip-us.apache.org/repos/asf/isis/repo
Commit: http://git-wip-us.apache.org/repos/asf/isis/commit/362a5bfc
Tree: http://git-wip-us.apache.org/repos/asf/isis/tree/362a5bfc
Diff: http://git-wip-us.apache.org/repos/asf/isis/diff/362a5bfc

Branch: refs/heads/master
Commit: 362a5bfcc7f5a8f45c15c9f5c242a3b4618ce3e5
Parents: 2cf70db
Author: Dan Haywood <[email protected]>
Authored: Wed Feb 18 11:32:17 2015 +0000
Committer: Dan Haywood <[email protected]>
Committed: Wed Feb 18 14:07:54 2015 +0000

----------------------------------------------------------------------
 .../services/TranslationsResolverWicket.java    |  28 ++-
 .../TranslationsResolverWicketTest.java         |  37 ++++
 .../all/i18n/TranslationFacetFactory.java       |   3 +-
 .../core/metamodel/services/i18n/po/Block.java  | 114 ----------
 .../services/i18n/po/ContextAndMsgId.java       |  90 --------
 .../metamodel/services/i18n/po/PoAbstract.java  |  43 ----
 .../metamodel/services/i18n/po/PoReader.java    | 190 -----------------
 .../metamodel/services/i18n/po/PoWriter.java    | 141 ------------
 .../services/i18n/po/TranslationServicePo.java  | 138 ------------
 .../i18n/po/TranslationServicePoMenu.java       |  94 --------
 .../services/i18n/po/PoReaderTest.java          | 212 -------------------
 .../core/runtime/services/i18n/po/Block.java    | 114 ++++++++++
 .../services/i18n/po/ContextAndMsgId.java       |  90 ++++++++
 .../runtime/services/i18n/po/PoAbstract.java    |  43 ++++
 .../core/runtime/services/i18n/po/PoReader.java | 190 +++++++++++++++++
 .../core/runtime/services/i18n/po/PoWriter.java | 141 ++++++++++++
 .../services/i18n/po/TranslationServicePo.java  | 149 +++++++++++++
 .../i18n/po/TranslationServicePoMenu.java       |  94 ++++++++
 .../core/webapp/IsisWebAppBootstrapper.java     |   6 +-
 .../runtime/services/i18n/po/PoReaderTest.java  | 212 +++++++++++++++++++
 20 files changed, 1100 insertions(+), 1029 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/component/viewer/wicket/impl/src/main/java/org/apache/isis/viewer/wicket/viewer/services/TranslationsResolverWicket.java
----------------------------------------------------------------------
diff --git 
a/component/viewer/wicket/impl/src/main/java/org/apache/isis/viewer/wicket/viewer/services/TranslationsResolverWicket.java
 
b/component/viewer/wicket/impl/src/main/java/org/apache/isis/viewer/wicket/viewer/services/TranslationsResolverWicket.java
index 0a104b8..110854a 100644
--- 
a/component/viewer/wicket/impl/src/main/java/org/apache/isis/viewer/wicket/viewer/services/TranslationsResolverWicket.java
+++ 
b/component/viewer/wicket/impl/src/main/java/org/apache/isis/viewer/wicket/viewer/services/TranslationsResolverWicket.java
@@ -18,18 +18,22 @@
  */
 package org.apache.isis.viewer.wicket.viewer.services;
 
+import java.io.File;
 import java.io.IOException;
 import java.net.URL;
+import java.nio.file.Path;
 import java.util.List;
 import javax.servlet.ServletContext;
 import com.google.common.base.Charsets;
 import com.google.common.io.CharSource;
+import com.google.common.io.Files;
 import com.google.common.io.Resources;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.apache.isis.applib.annotation.DomainService;
 import org.apache.isis.applib.annotation.Programmatic;
 import org.apache.isis.applib.services.i18n.TranslationsResolver;
+import org.apache.isis.core.webapp.WebAppConstants;
 import org.apache.isis.viewer.wicket.viewer.IsisWicketApplication;
 
 
@@ -44,15 +48,33 @@ public class TranslationsResolverWicket implements 
TranslationsResolver {
     @Override
     @Programmatic
     public List<String> readLines(final String file) {
+        final ServletContext servletContext = getServletContext();
+
+        final String configLocation = 
servletContext.getInitParameter(WebAppConstants.CONFIG_DIR_PARAM);
         try {
-            final ServletContext servletContext = 
getIsisWicketApplication().getServletContext();
-            final URL url = servletContext.getResource("/WEB-INF/" + file);
-            return readLines(url);
+            if(configLocation != null) {
+                LOG.info( "Reading translations relative to config override 
location: " + configLocation );
+                return Files.readLines(newFile(configLocation, file), 
Charsets.UTF_8);
+            } else {
+                final URL url = servletContext.getResource("/WEB-INF/" + file);
+                return readLines(url);
+            }
         } catch (final RuntimeException | IOException ignored) {
             return null;
         }
     }
 
+    static File newFile(final String dir, final String file) {
+        final File base = new File(dir);
+        final Path path = base.toPath();
+        final Path resolve = path.resolve(file);
+        return resolve.toFile();
+    }
+
+    protected ServletContext getServletContext() {
+        return getIsisWicketApplication().getServletContext();
+    }
+
     private static List<String> readLines(final URL url) throws IOException {
         if(url == null) {
             return null;

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/component/viewer/wicket/impl/src/test/java/org/apache/isis/viewer/wicket/viewer/services/TranslationsResolverWicketTest.java
----------------------------------------------------------------------
diff --git 
a/component/viewer/wicket/impl/src/test/java/org/apache/isis/viewer/wicket/viewer/services/TranslationsResolverWicketTest.java
 
b/component/viewer/wicket/impl/src/test/java/org/apache/isis/viewer/wicket/viewer/services/TranslationsResolverWicketTest.java
new file mode 100644
index 0000000..2190a62
--- /dev/null
+++ 
b/component/viewer/wicket/impl/src/test/java/org/apache/isis/viewer/wicket/viewer/services/TranslationsResolverWicketTest.java
@@ -0,0 +1,37 @@
+package org.apache.isis.viewer.wicket.viewer.services;
+
+import java.io.File;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assume.assumeThat;
+
+public class TranslationsResolverWicketTest {
+
+    public static class NewFile extends TranslationsResolverWicketTest {
+
+        @Before
+        public void setUp() throws Exception {
+            assumeThat(System.getProperty("os.name").startsWith("Windows"), 
is(true));
+        }
+
+        @Test
+        public void simple() throws Exception {
+            final File file = TranslationsResolverWicket.newFile("c:/foo", 
"bar");
+            final String absolutePath = file.getAbsolutePath();
+            assertThat(absolutePath, is("c:\\foo\\bar"));
+        }
+
+        @Test
+        public void nestedChild() throws Exception {
+            final File file = TranslationsResolverWicket.newFile("c:/foo", 
"bar/baz");
+            final String absolutePath = file.getAbsolutePath();
+            assertThat(absolutePath, is("c:\\foo\\bar\\baz"));
+        }
+
+    }
+
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/all/i18n/TranslationFacetFactory.java
----------------------------------------------------------------------
diff --git 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/all/i18n/TranslationFacetFactory.java
 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/all/i18n/TranslationFacetFactory.java
index 4200a95..bcadd2c 100644
--- 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/all/i18n/TranslationFacetFactory.java
+++ 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/all/i18n/TranslationFacetFactory.java
@@ -127,7 +127,8 @@ public class TranslationFacetFactory extends 
FacetFactoryAbstract implements Con
      * Looks up from {@link 
org.apache.isis.core.metamodel.runtimecontext.ServicesInjector}.
      *
      * <p>
-     *     There is guaranteed to be an instance because {@link 
org.apache.isis.core.metamodel.services.i18n.po.TranslationServicePo} is 
annotated as a {@link org.apache.isis.applib.annotation.DomainService 
&#64;DomainService}.
+     *     There is guaranteed to be an instance because 
<code>TranslationServicePo</code> (in runtime) is annotated
+     *     as a {@link org.apache.isis.applib.annotation.DomainService 
&#64;DomainService}.
      * </p>
      */
     TranslationService lookupTranslationService() {

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/Block.java
----------------------------------------------------------------------
diff --git 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/Block.java
 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/Block.java
deleted file mode 100644
index 43870d9..0000000
--- 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/Block.java
+++ /dev/null
@@ -1,114 +0,0 @@
-package org.apache.isis.core.metamodel.services.i18n.po;
-
-import java.util.List;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import com.google.common.collect.Lists;
-
-class Block {
-
-    private enum State {
-        CONTEXT("^#: (?<value>.+)$"),
-        MSGID("^msgid \"(?<value>.+)\"$"),
-        MSGID_PLURAL("^msgid_plural \"(?<value>.+)\"$"),
-        MSGSTR("^msgstr \"(?<value>.+)\"$"),
-        MSGSTR0("^msgstr\\[0\\] \"(?<value>.+)\"$"),
-        MSGSTR1("^msgstr\\[1\\] \"(?<value>.+)\"$");
-
-        private final Pattern pattern;
-
-        private State(final String regex) {
-            pattern = Pattern.compile(regex);
-        }
-    }
-
-    State state = State.CONTEXT;
-
-    List<String> contextList = Lists.newArrayList();
-    String msgid = null;
-    String msgid_plural = null;
-    String msgstr = null; // either from msgstr or msgstr[0] if there is a 
plural
-    String msgstr_plural = null; // from msgstr[1]
-
-    Block parseLine(final String line, final Map<ContextAndMsgId, String> 
translationsByKey) {
-        if (state == State.CONTEXT) {
-            final Matcher contextMatcher = state.pattern.matcher(line);
-            if (contextMatcher.matches()) {
-                final String context = contextMatcher.group("value");
-                contextList.add(context);
-                return this;
-            } else {
-                state = State.MSGID;
-                // fallthrough (there may not have been any more context)
-            }
-        }
-
-        if (state == State.MSGID) {
-            final Matcher msgidMatcher = state.pattern.matcher(line);
-            if (msgidMatcher.matches()) {
-                msgid = msgidMatcher.group("value");
-                state = State.MSGID_PLURAL; // found, next time look for 
plurals
-            } else {
-                return new Block();
-            }
-            return this;
-        }
-
-        if (state == State.MSGID_PLURAL) {
-            final Matcher msgIdPluralMatcher = state.pattern.matcher(line);
-            if (msgIdPluralMatcher.matches()) {
-                msgid_plural = msgIdPluralMatcher.group("value");
-                state = State.MSGSTR0; // next time look for msgstr[0]
-                return this;
-            } else {
-                state = State.MSGSTR; // fall through (there may not have been 
any plural form)
-            }
-        }
-
-        if (state == State.MSGSTR) {
-            final Matcher msgStrMatcher = state.pattern.matcher(line);
-            if (msgStrMatcher.matches()) {
-                msgstr = msgStrMatcher.group("value");
-            }
-            append(translationsByKey);
-            return new Block();
-        }
-
-        if (state == State.MSGSTR0) {
-            final Matcher msgStr0Matcher = state.pattern.matcher(line);
-            if (msgStr0Matcher.matches()) {
-                msgstr = msgStr0Matcher.group("value");
-                state = State.MSGSTR1; // next time, look for plural
-            } else {
-                append(translationsByKey);
-                return new Block();
-            }
-            return this;
-        }
-
-        if (state == State.MSGSTR1) {
-            final Matcher msgStr1Matcher = state.pattern.matcher(line);
-            if (msgStr1Matcher.matches()) {
-                msgstr_plural = msgStr1Matcher.group("value");
-            }
-            append(translationsByKey);
-            return new Block();
-        }
-        return this;
-    }
-
-    void append(final Map<ContextAndMsgId, String> translationsByKey) {
-        for (String context : contextList) {
-            if(msgid != null && msgstr != null) {
-                final ContextAndMsgId mc = new ContextAndMsgId(context, msgid, 
ContextAndMsgId.Type.REGULAR);
-                translationsByKey.put(mc, msgstr);
-            }
-            if(msgid_plural != null && msgstr_plural != null) {
-                final ContextAndMsgId mc = new ContextAndMsgId(context, 
msgid_plural, ContextAndMsgId.Type.PLURAL_ONLY);
-                translationsByKey.put(mc, msgstr_plural);
-            }
-        }
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/ContextAndMsgId.java
----------------------------------------------------------------------
diff --git 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/ContextAndMsgId.java
 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/ContextAndMsgId.java
deleted file mode 100644
index 2077c11..0000000
--- 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/ContextAndMsgId.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package org.apache.isis.core.metamodel.services.i18n.po;
-
-/**
- * The combination of a <tt>msgId</tt> and context (optionally null) that 
represents a key to a translatable resource.
- *
- * <p>
- *     For example, with this <i>.pot</i> file:
- * </p>
- * <pre>
- * #: 
org.isisaddons.module.sessionlogger.dom.SessionLoggingServiceMenu#activeSessions()
- msgid: "Active Sessions"
-
- #: org.isisaddons.module.audit.dom.AuditingServiceMenu
- #: org.isisaddons.module.command.dom.CommandServiceMenu
- #: org.isisaddons.module.publishing.dom.PublishingServiceMenu
- msgid: "Activity"
-
- * </pre>
- *
- * <p>
- *     the combination of 
<code>{org.isisaddons.module.sessionlogger.dom.SessionLoggingServiceMenu#activeSessions(),
 "Active Sessions"}</code> represents such a key, as does 
<code>{org.isisaddons.module.audit.dom.AuditingServiceMenu, "Activity"}</code>
- * </p>
- */
-public class ContextAndMsgId implements Comparable<ContextAndMsgId> {
-
-    public enum Type {
-        /**
-         * The text to use when there is no plural form, or the text to use 
for singular pattern when there is also a plural form.
-         */
-        REGULAR,
-        /**
-         * The text to use for plural form.
-         */
-        PLURAL_ONLY
-    }
-
-    private final String context;
-    private final String msgId;
-    private final Type type;
-
-    public ContextAndMsgId(final String context, final String msgId, final 
Type type) {
-        this.context = context == null? "": context;
-        this.msgId = msgId;
-        this.type = type;
-    }
-
-    public String getMsgId() {
-        return msgId;
-    }
-
-    public String getContext() {
-        return context;
-    }
-
-    /**
-     * Not part of equals/hashCode impl.
-     */
-    public Type getType() {
-        return type;
-    }
-
-    @Override
-    public boolean equals(final Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-
-        final ContextAndMsgId that = (ContextAndMsgId) o;
-
-        if (context != null ? !context.equals(that.context) : that.context != 
null) return false;
-        if (msgId != null ? !msgId.equals(that.msgId) : that.msgId != null) 
return false;
-
-        return true;
-    }
-
-    @Override
-    public int hashCode() {
-        int result = context != null ? context.hashCode() : 0;
-        result = 31 * result + (msgId != null ? msgId.hashCode() : 0);
-        return result;
-    }
-
-    @Override
-    public int compareTo(final ContextAndMsgId o) {
-        final int i = msgId.compareTo(o.msgId);
-        if(i != 0) {
-            return i;
-        }
-        return context.compareTo(o.context);
-    }
-}

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/PoAbstract.java
----------------------------------------------------------------------
diff --git 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/PoAbstract.java
 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/PoAbstract.java
deleted file mode 100644
index e732d46..0000000
--- 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/PoAbstract.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.isis.core.metamodel.services.i18n.po;
-
-import org.apache.isis.applib.services.i18n.TranslationService;
-
-abstract class PoAbstract {
-
-    protected final TranslationServicePo translationServicePo;
-    private final TranslationService.Mode mode;
-
-    PoAbstract(final TranslationServicePo translationServicePo, final 
TranslationService.Mode mode) {
-        this.translationServicePo = translationServicePo;
-        this.mode = mode;
-    }
-
-
-    abstract void shutdown();
-
-    abstract String translate(final String context, final String msgId);
-
-    abstract String translate(final String context, final String msgId, final 
String msgIdPlural, int num);
-
-    public TranslationService.Mode getMode() {
-        return mode;
-    }
-}

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/PoReader.java
----------------------------------------------------------------------
diff --git 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/PoReader.java
 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/PoReader.java
deleted file mode 100644
index e0bb2b7..0000000
--- 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/PoReader.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.isis.core.metamodel.services.i18n.po;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.apache.isis.applib.services.i18n.TranslationService;
-import org.apache.isis.applib.services.i18n.TranslationsResolver;
-
-class PoReader extends PoAbstract {
-
-    public static final String LOCATION_BASE_URL = 
"isis.services.translation.po.locationBaseUrl";
-    public static Logger LOG = LoggerFactory.getLogger(PoReader.class);
-
-    private final Map<Locale, Map<ContextAndMsgId, String>> 
translationByKeyByLocale = Maps.newHashMap();
-
-    /**
-     * The basename of the translations file, hard-coded to 
<tt>translations</tt>.
-     *
-     * <p>
-     *     This means that the reader will search for 
<tt>translations_en-US.po</tt>, <tt>translations_en.po</tt>,
-     *     <tt>translations.po</tt>, according to the location that the 
provided {@link org.apache.isis.applib.services.i18n.TranslationsResolver} 
searches.  For example, if using the Wicket implementation, then will search 
for these files
-     *     under <tt>/WEB-INF</tt> directory.
-     * </p>
-     */
-    private final String basename = "translations";
-
-    private List<String> fallback;
-
-    public PoReader(final TranslationServicePo translationServicePo) {
-        super(translationServicePo, TranslationService.Mode.READ);
-    }
-
-    //region > init, shutdown
-    void init(final Map<String,String> config) {
-        fallback = readUrl(basename + ".po");
-        if(fallback == null) {
-            LOG.warn("No fallback translations found");
-            fallback = Collections.emptyList();
-        }
-    }
-
-    @Override
-    void shutdown() {
-    }
-    //endregion
-
-    public String translate(final String context, final String msgId) {
-        return translate(context, msgId, ContextAndMsgId.Type.REGULAR);
-    }
-
-    @Override
-    String translate(final String context, final String msgId, final String 
msgIdPlural, final int num) {
-
-        final String msgIdToUse;
-        final ContextAndMsgId.Type type;
-        if (num == 1) {
-            msgIdToUse = msgId;
-            type = ContextAndMsgId.Type.REGULAR;
-        } else {
-            msgIdToUse = msgIdPlural;
-            type = ContextAndMsgId.Type.PLURAL_ONLY;
-        }
-
-        return translate(context, msgIdToUse, type);
-    }
-
-    private String translate(
-            final String context, final String msgId, final 
ContextAndMsgId.Type type) {
-
-        final Locale targetLocale;
-        try {
-            targetLocale = 
translationServicePo.getLocaleProvider().getLocale();
-        } catch(final RuntimeException ex){
-            LOG.warn("Failed to obtain locale, returning the original msgId");
-            return msgId;
-        }
-
-        final Map<ContextAndMsgId, String> translationsByKey = 
readAndCacheTranslationsIfRequired(targetLocale);
-
-        final ContextAndMsgId key = new ContextAndMsgId(context, msgId, type);
-        final String translation = lookupTranslation(translationsByKey, key);
-        if (!Strings.isNullOrEmpty(translation)) {
-            return translation;
-        }
-
-        final ContextAndMsgId keyNoContext = new ContextAndMsgId("", msgId, 
type);
-        final String translationNoContext = 
lookupTranslation(translationsByKey, keyNoContext);
-        if (!Strings.isNullOrEmpty(translationNoContext)) {
-            return translationNoContext;
-        }
-
-        LOG.warn("No translation found for: " + key);
-        return msgId;
-    }
-
-    private String lookupTranslation(final Map<ContextAndMsgId, String> 
translationsByKey, final ContextAndMsgId key) {
-        final String s = translationsByKey.get(key);
-        return s != null? s.trim(): null;
-    }
-
-    private Map<ContextAndMsgId, String> 
readAndCacheTranslationsIfRequired(final Locale locale) {
-        Map<ContextAndMsgId, String> translationsByKey = 
translationByKeyByLocale.get(locale);
-        if(translationsByKey != null) {
-            return translationsByKey;
-        }
-
-        translationsByKey = Maps.newHashMap();
-        read(locale, translationsByKey);
-        translationByKeyByLocale.put(locale, translationsByKey);
-
-        return translationsByKey;
-    }
-
-
-    /**
-     * @param locale - the .po file to load
-     * @param translationsByKey - the translations to be populated
-     */
-    private void read(final Locale locale, final Map<ContextAndMsgId, String> 
translationsByKey) {
-        final List<String> contents = readPo(locale);
-
-        Block block = new Block();
-        for (final String line : contents) {
-            block = block.parseLine(line, translationsByKey);
-        }
-    }
-
-    protected List<String> readPo(final Locale locale) {
-        final List<String> lines = readPoElseNull(locale);
-        if(lines != null) {
-            return lines;
-        }
-        LOG.warn("Could not locate translations for locale: " + locale + ", 
using fallback");
-        return fallback;
-    }
-
-    private List<String> readPoElseNull(final Locale locale) {
-        final String country = locale.getCountry().toUpperCase(Locale.ROOT);
-        final String language = locale.getLanguage().toLowerCase(Locale.ROOT);
-
-        final List<String> candidates = Lists.newArrayList();
-        if(!Strings.isNullOrEmpty(language)) {
-            if(!Strings.isNullOrEmpty(country)) {
-                candidates.add(basename + "_" + language + "-" + country+ 
".po");
-            }
-            candidates.add(basename + "_" + language + ".po");
-        }
-
-        for (final String candidate : candidates) {
-            final List<String> lines = readUrl(candidate);
-            if(lines != null) {
-                return lines;
-            }
-        }
-        return null;
-    }
-
-    private List<String> readUrl(final String candidate) {
-        final TranslationsResolver translationsResolver = 
translationServicePo.getTranslationsResolver();
-        if(translationsResolver == null) {
-            return null;
-        }
-        return translationsResolver.readLines(candidate);
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/PoWriter.java
----------------------------------------------------------------------
diff --git 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/PoWriter.java
 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/PoWriter.java
deleted file mode 100644
index 6ff8117..0000000
--- 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/PoWriter.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.isis.core.metamodel.services.i18n.po;
-
-import java.util.SortedMap;
-import java.util.SortedSet;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
-import org.joda.time.LocalDateTime;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.apache.isis.applib.services.i18n.TranslationService;
-
-class PoWriter extends PoAbstract {
-
-    public static Logger LOG = LoggerFactory.getLogger(PoWriter.class);
-
-    private static class Block {
-        private final String msgId;
-        private final SortedSet<String> contexts = Sets.newTreeSet();
-        private String msgIdPlural;
-
-        private Block(final String msgId) {
-            this.msgId = msgId;
-        }
-    }
-
-    private final SortedMap<String, Block> blocksByMsgId = Maps.newTreeMap();
-
-    public PoWriter(final TranslationServicePo translationServicePo) {
-        super(translationServicePo, TranslationService.Mode.WRITE);
-    }
-
-    //region > shutdown
-
-    @Override
-    void shutdown() {
-        final StringBuilder buf = new StringBuilder();
-        buf.append("\n");
-        
buf.append("\n##############################################################################");
-        buf.append("\n#");
-        buf.append("\n# .pot file");
-        buf.append("\n#");
-        buf.append("\n# generated at: 
").append(LocalDateTime.now().toString("yyyy-MM-dd HH:mm:ss"));
-        buf.append("\n# generated by: 
").append(TranslationServicePo.class.getSimpleName());
-        buf.append("\n#");
-        buf.append("\n# Translate this file to each required language and 
place in WEB-INF, eg:");
-        buf.append("\n#");
-        buf.append("\n#     /WEB-INF/translations_en-US.po");
-        buf.append("\n#     /WEB-INF/translations_en.po");
-        buf.append("\n#     /WEB-INF/translations_fr-FR.po");
-        buf.append("\n#     /WEB-INF/translations_fr.po");
-        buf.append("\n#     /WEB-INF/translations.po");
-        buf.append("\n#");
-        buf.append("\n# If the app uses TranslatableString (eg for 
internationalized validation");
-        buf.append("\n# messages), or if the app calls the TranslationService 
directly, then ensure");
-        buf.append("\n# that all text to be translated has been captured by 
running a full");
-        buf.append("\n# integration test suite that exercises all relevant 
behaviour");
-        buf.append("\n#");
-        
buf.append("\n##############################################################################");
-        buf.append("\n");
-        buf.append("\n");
-        buf.append(toPot());
-        buf.append("\n");
-        buf.append("\n");
-        
buf.append("\n##############################################################################");
-        buf.append("\n# end of .pot file");
-        
buf.append("\n##############################################################################");
-        buf.append("\n");
-        LOG.info(buf.toString());
-    }
-    //endregion
-
-
-    public String translate(final String context, final String msgId) {
-
-        final Block block = blockFor(msgId);
-        block.contexts.add(context);
-
-        return msgId;
-    }
-
-    @Override
-    String translate(final String context, final String msgId, final String 
msgIdPlural, final int num) {
-
-        final Block block = blockFor(msgId);
-        block.contexts.add(context);
-        block.msgIdPlural = msgIdPlural;
-
-        return null;
-    }
-
-    private Block blockFor(final String msgId) {
-        Block block = blocksByMsgId.get(msgId);
-        if(block == null) {
-            block = new Block(msgId);
-            blocksByMsgId.put(msgId, block);
-        }
-        return block;
-    }
-
-    /**
-     * Not API
-     */
-    String toPot() {
-        final StringBuilder buf = new StringBuilder();
-        for (final String msgId : blocksByMsgId.keySet()) {
-            final Block block = blocksByMsgId.get(msgId);
-            for (final String context : block.contexts) {
-                buf.append("#: ").append(context).append("\n");
-            }
-            buf.append("msgid \"").append(msgId).append("\"\n");
-            if(block.msgIdPlural == null) {
-                buf.append("msgstr \"\"\n");
-            } else {
-                buf.append("msgid_plural 
\"").append(block.msgIdPlural).append("\"\n");
-                buf.append("msgstr[0] \"\"\n");
-                buf.append("msgstr[1] \"\"\n");
-            }
-            buf.append("\n\n");
-        }
-        return buf.toString();
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/TranslationServicePo.java
----------------------------------------------------------------------
diff --git 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/TranslationServicePo.java
 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/TranslationServicePo.java
deleted file mode 100644
index 5dda93b..0000000
--- 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/TranslationServicePo.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.isis.core.metamodel.services.i18n.po;
-
-import java.util.Map;
-import javax.annotation.PostConstruct;
-import javax.annotation.PreDestroy;
-import javax.inject.Inject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.apache.isis.applib.annotation.DomainService;
-import org.apache.isis.applib.annotation.Programmatic;
-import org.apache.isis.applib.services.i18n.LocaleProvider;
-import org.apache.isis.applib.services.i18n.TranslationService;
-import org.apache.isis.applib.services.i18n.TranslationsResolver;
-
-@DomainService
-public class TranslationServicePo implements TranslationService {
-
-    public static Logger LOG = 
LoggerFactory.getLogger(TranslationServicePo.class);
-
-    public static final String KEY_DEPLOYMENT_TYPE = "isis.deploymentType";
-    public static final String KEY_PO_MODE = 
"isis.services.translation.po.mode";
-
-    private PoAbstract po;
-
-    /**
-     * Defaults to writer mode because the service won't have been init'd 
while the metamodel is bring instantiated,
-     * and we want to ensure that we capture all requests for translation.
-     */
-    public TranslationServicePo() {
-        po = new PoWriter(this);
-    }
-
-    //region > init, shutdown
-
-    @Programmatic
-    @PostConstruct
-    public void init(final Map<String,String> config) {
-
-        if(getLocaleProvider() == null || getTranslationsResolver() == null) {
-            // remain in write mode
-            return;
-        }
-
-        final String deploymentType = config.get(KEY_DEPLOYMENT_TYPE);
-        boolean prototypeOrTest = deploymentType==null ||
-                    deploymentType.toLowerCase().contains("prototype") ||
-                    deploymentType.toLowerCase().contains("test") ;
-
-        final String translationMode = config.get(KEY_PO_MODE);
-        final boolean forceRead =
-                translationMode != null &&
-                        ("read".equalsIgnoreCase(translationMode) ||
-                         "reader".equalsIgnoreCase(translationMode));
-
-        if(prototypeOrTest && !forceRead) {
-            // remain in write mode
-            return;
-        }
-
-        // switch to read mode
-        final PoReader poReader = new PoReader(this);
-        poReader.init(config);
-        po = poReader;
-    }
-
-    @Programmatic
-    @PreDestroy
-    public void shutdown() {
-        po.shutdown();
-    }
-    //endregion
-
-
-    @Override
-    @Programmatic
-    public String translate(final String context, final String text) {
-        return po.translate(context, text);
-    }
-
-    @Override
-    public String translate(final String context, final String singularText, 
final String pluralText, final int num) {
-        return po.translate(context, singularText, pluralText, num);
-    }
-
-    @Override
-    public Mode getMode() {
-        return po.getMode();
-    }
-
-    /**
-     * Not API
-     */
-    @Programmatic
-    public String toPo() {
-        if (!getMode().isWrite()) {
-            throw new IllegalStateException("Not in write mode");
-        }
-        return  ((PoWriter)po).toPot();
-    }
-
-    // //////////////////////////////////////
-
-    @Inject
-    private
-    TranslationsResolver translationsResolver;
-
-    @Programmatic
-    TranslationsResolver getTranslationsResolver() {
-        return translationsResolver;
-    }
-
-    @Inject
-    private
-    LocaleProvider localeProvider;
-
-    @Programmatic
-    LocaleProvider getLocaleProvider() {
-        return localeProvider;
-    }
-}

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/TranslationServicePoMenu.java
----------------------------------------------------------------------
diff --git 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/TranslationServicePoMenu.java
 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/TranslationServicePoMenu.java
deleted file mode 100644
index c39f98a..0000000
--- 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/services/i18n/po/TranslationServicePoMenu.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.isis.core.metamodel.services.i18n.po;
-
-import java.util.List;
-import javax.inject.Inject;
-import org.apache.isis.applib.Identifier;
-import org.apache.isis.applib.IsisApplibModule;
-import org.apache.isis.applib.annotation.Action;
-import org.apache.isis.applib.annotation.ActionLayout;
-import org.apache.isis.applib.annotation.DomainService;
-import org.apache.isis.applib.annotation.DomainServiceLayout;
-import org.apache.isis.applib.annotation.NatureOfService;
-import org.apache.isis.applib.annotation.ParameterLayout;
-import org.apache.isis.applib.annotation.RestrictTo;
-import org.apache.isis.applib.annotation.SemanticsOf;
-import org.apache.isis.applib.value.Clob;
-
-@DomainService(
-        nature = NatureOfService.VIEW_MENU_ONLY
-)
-@DomainServiceLayout(
-        menuBar = DomainServiceLayout.MenuBar.SECONDARY,
-        named = "Prototyping"
-)
-public class TranslationServicePoMenu {
-
-    public static abstract class ActionDomainEvent extends 
IsisApplibModule.ActionDomainEvent<TranslationServicePoMenu> {
-        public ActionDomainEvent(final TranslationServicePoMenu source, final 
Identifier identifier) {
-            super(source, identifier);
-        }
-
-        public ActionDomainEvent(final TranslationServicePoMenu source, final 
Identifier identifier, final Object... arguments) {
-            super(source, identifier, arguments);
-        }
-
-        public ActionDomainEvent(final TranslationServicePoMenu source, final 
Identifier identifier, final List<Object> arguments) {
-            super(source, identifier, arguments);
-        }
-    }
-
-    // //////////////////////////////////////
-
-    public static class DownloadPotFileDomainEvent extends ActionDomainEvent {
-        public DownloadPotFileDomainEvent(final TranslationServicePoMenu 
source, final Identifier identifier, final Object... arguments) {
-            super(source, identifier, arguments);
-        }
-    }
-
-    @Action(
-            domainEvent = DownloadPotFileDomainEvent.class,
-            semantics = SemanticsOf.SAFE,
-            restrictTo = RestrictTo.PROTOTYPING
-    )
-    @ActionLayout(
-            cssClassFa = "fa-download"
-    )
-    public Clob downloadPotFile(
-            @ParameterLayout(named = ".pot file name")
-            final String potFileName) {
-        final String chars = translationService.toPo();
-        return new Clob(potFileName, "text/plain", chars);
-    }
-
-    public String default0DownloadPotFile() {
-        return "translations.pot";
-    }
-    public boolean hideDownloadPotFile() {
-        return translationService.getMode().isRead();
-    }
-
-    // //////////////////////////////////////
-
-
-    @Inject
-    private TranslationServicePo translationService;
-
-}

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/i18n/po/PoReaderTest.java
----------------------------------------------------------------------
diff --git 
a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/i18n/po/PoReaderTest.java
 
b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/i18n/po/PoReaderTest.java
deleted file mode 100644
index 1889394..0000000
--- 
a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/services/i18n/po/PoReaderTest.java
+++ /dev/null
@@ -1,212 +0,0 @@
-package org.apache.isis.core.metamodel.services.i18n.po;
-
-import java.util.List;
-import java.util.Locale;
-import com.google.common.collect.Lists;
-import org.jmock.Expectations;
-import org.jmock.auto.Mock;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.apache.isis.applib.services.i18n.LocaleProvider;
-import org.apache.isis.core.unittestsupport.jmocking.JUnitRuleMockery2;
-
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.hamcrest.CoreMatchers.is;
-import static org.junit.Assert.assertThat;
-
-public class PoReaderTest {
-
-    @Rule
-    public JUnitRuleMockery2 context = 
JUnitRuleMockery2.createFor(JUnitRuleMockery2.Mode.INTERFACES_AND_CLASSES);
-
-    @Mock
-    TranslationServicePo mockTranslationServicePo;
-
-    @Mock
-    LocaleProvider mockLocaleProvider;
-
-    PoReader poReader;
-
-    @Before
-    public void setUp() throws Exception {
-        context.checking(new Expectations() {{
-            allowing(mockTranslationServicePo).getLocaleProvider();
-            will(returnValue(mockLocaleProvider));
-
-            allowing(mockLocaleProvider).getLocale();
-            will(returnValue(Locale.UK));
-        }});
-    }
-
-    public static class Translate extends PoReaderTest {
-
-        @Test
-        public void singleContext() throws Exception {
-
-            // given
-            final String context =
-                    
"org.apache.isis.applib.services.bookmark.BookmarkHolderAssociationContributions#object()";
-            final String msgId = "Work of art";
-            final String msgStr = "Objet d'art";
-
-            poReader = new PoReader(mockTranslationServicePo) {
-                @Override
-                protected List<String> readPo(final Locale locale) {
-                    final List<String> lines = Lists.newArrayList();
-                    lines.add(String.format("#: %s", context));
-                    lines.add(String.format("msgid \"%s\"", msgId));
-                    lines.add(String.format("msgstr \"%s\"", msgStr));
-                    return lines;
-                }
-            };
-
-            // when
-            final String translated = poReader.translate(context, msgId);
-
-            // then
-            assertThat(translated, is(equalTo(msgStr)));
-        }
-
-        @Test
-        public void multipleContext() throws Exception {
-
-            // given
-            final String context1 =
-                    
"fixture.simple.SimpleObjectsFixturesService#runFixtureScript(org.apache.isis.applib.fixturescripts.FixtureScript,java.lang.String)";
-            final String context2 =
-                    
"org.apache.isis.applib.fixturescripts.FixtureScripts#runFixtureScript(org.apache.isis.applib.fixturescripts.FixtureScript,java.lang.String)";
-            final String msgId = "Parameters";
-            final String msgStr = "Paramètres";
-
-            poReader = new PoReader(mockTranslationServicePo) {
-                @Override
-                protected List<String> readPo(final Locale locale) {
-                    final List<String> lines = Lists.newArrayList();
-                    lines.add(String.format("#: %s", context1));
-                    lines.add(String.format("#: %s", context2));
-                    lines.add(String.format("msgid \"%s\"", msgId));
-                    lines.add(String.format("msgstr \"%s\"", msgStr));
-                    return lines;
-                }
-            };
-            // when
-            final String translated = poReader.translate(context1, msgId);
-
-            // then
-            assertThat(translated, is(equalTo(msgStr)));
-
-            // when
-            final String translated2 = poReader.translate(context2, msgId);
-
-            // then
-            assertThat(translated2, is(equalTo(msgStr)));
-        }
-
-        @Test
-        public void multipleBlocks() throws Exception {
-
-            // given
-            final String context1 =
-                    
"org.apache.isis.applib.services.bookmark.BookmarkHolderAssociationContributions#object()";
-            final String msgid1 = "Work of art";
-            final String msgstr1 = "Objet d'art";
-
-            final String context2 =
-                    
"org.apache.isis.applib.services.bookmark.BookmarkHolderAssociationContributions#lookup()";
-            final String msgid2 = "Lookup";
-            final String msgstr2 = "Look up";
-
-            poReader = new PoReader(mockTranslationServicePo) {
-                @Override
-                protected List<String> readPo(final Locale locale) {
-                    final List<String> lines = Lists.newArrayList();
-                    lines.add(String.format("#: %s", context1));
-                    lines.add(String.format("msgid \"%s\"", msgid1));
-                    lines.add(String.format("msgstr \"%s\"", msgstr1));
-
-                    lines.add(String.format(""));
-                    lines.add(String.format("# "));
-
-                    lines.add(String.format("#: %s", context2));
-                    lines.add(String.format("msgid \"%s\"", msgid2));
-                    lines.add(String.format("msgstr \"%s\"", msgstr2));
-
-                    lines.add(String.format(""));
-                    return lines;
-                }
-            };
-
-            // when
-            final String translated1 = poReader.translate(context1, msgid1);
-
-            // then
-            assertThat(translated1, is(equalTo(msgstr1)));
-
-            // when
-            final String translated2 = poReader.translate(context2, msgid2);
-
-            // then
-            assertThat(translated2, is(equalTo(msgstr2)));
-        }
-
-        @Test
-        public void withPlurals() throws Exception {
-
-            // given
-            final String context =
-                    
"org.apache.isis.applib.services.bookmark.BookmarkHolderAssociationContributions#object()";
-            final String msgid = "Work of art";
-            final String msgid_plural = "Works of art";
-            final String msgstr$0 = "Œuvre d'art";
-            final String msgstr$1 = "Les œuvres d'art";
-
-            poReader = new PoReader(mockTranslationServicePo) {
-                @Override
-                protected List<String> readPo(final Locale locale) {
-                    final List<String> lines = Lists.newArrayList();
-                    lines.add(String.format("#: %s", context));
-                    lines.add(String.format("msgid \"%s\"", msgid));
-                    lines.add(String.format("msgid_plural \"%s\"", 
msgid_plural));
-                    lines.add(String.format("msgstr[0] \"%s\"", msgstr$0));
-                    lines.add(String.format("msgstr[1] \"%s\"", msgstr$1));
-                    return lines;
-                }
-            };
-
-            // when
-            final String translated1 = poReader.translate(context, msgid);
-
-            // then
-            assertThat(translated1, is(equalTo(msgstr$0)));
-
-            // when
-            final String translated2 = poReader.translate(context, 
msgid_plural);
-
-            // then
-            assertThat(translated2, is(equalTo(msgstr$1)));
-        }
-
-
-
-        @Test
-        public void noTranslation() throws Exception {
-
-            // given
-
-            poReader = new PoReader(mockTranslationServicePo) {
-                @Override
-                protected List<String> readPo(final Locale locale) {
-                    return Lists.newArrayList();
-                }
-            };
-
-            // when
-            final String translated = poReader.translate("someContext", 
"Something to translate");
-
-            // then
-            assertThat(translated, is(equalTo("Something to translate")));
-       }
-    }
-
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/Block.java
----------------------------------------------------------------------
diff --git 
a/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/Block.java
 
b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/Block.java
new file mode 100644
index 0000000..a39ca1a
--- /dev/null
+++ 
b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/Block.java
@@ -0,0 +1,114 @@
+package org.apache.isis.core.runtime.services.i18n.po;
+
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import com.google.common.collect.Lists;
+
+class Block {
+
+    private enum State {
+        CONTEXT("^#: (?<value>.+)$"),
+        MSGID("^msgid \"(?<value>.+)\"$"),
+        MSGID_PLURAL("^msgid_plural \"(?<value>.+)\"$"),
+        MSGSTR("^msgstr \"(?<value>.+)\"$"),
+        MSGSTR0("^msgstr\\[0\\] \"(?<value>.+)\"$"),
+        MSGSTR1("^msgstr\\[1\\] \"(?<value>.+)\"$");
+
+        private final Pattern pattern;
+
+        private State(final String regex) {
+            pattern = Pattern.compile(regex);
+        }
+    }
+
+    State state = State.CONTEXT;
+
+    List<String> contextList = Lists.newArrayList();
+    String msgid = null;
+    String msgid_plural = null;
+    String msgstr = null; // either from msgstr or msgstr[0] if there is a 
plural
+    String msgstr_plural = null; // from msgstr[1]
+
+    Block parseLine(final String line, final Map<ContextAndMsgId, String> 
translationsByKey) {
+        if (state == State.CONTEXT) {
+            final Matcher contextMatcher = state.pattern.matcher(line);
+            if (contextMatcher.matches()) {
+                final String context = contextMatcher.group("value");
+                contextList.add(context);
+                return this;
+            } else {
+                state = State.MSGID;
+                // fallthrough (there may not have been any more context)
+            }
+        }
+
+        if (state == State.MSGID) {
+            final Matcher msgidMatcher = state.pattern.matcher(line);
+            if (msgidMatcher.matches()) {
+                msgid = msgidMatcher.group("value");
+                state = State.MSGID_PLURAL; // found, next time look for 
plurals
+            } else {
+                return new Block();
+            }
+            return this;
+        }
+
+        if (state == State.MSGID_PLURAL) {
+            final Matcher msgIdPluralMatcher = state.pattern.matcher(line);
+            if (msgIdPluralMatcher.matches()) {
+                msgid_plural = msgIdPluralMatcher.group("value");
+                state = State.MSGSTR0; // next time look for msgstr[0]
+                return this;
+            } else {
+                state = State.MSGSTR; // fall through (there may not have been 
any plural form)
+            }
+        }
+
+        if (state == State.MSGSTR) {
+            final Matcher msgStrMatcher = state.pattern.matcher(line);
+            if (msgStrMatcher.matches()) {
+                msgstr = msgStrMatcher.group("value");
+            }
+            append(translationsByKey);
+            return new Block();
+        }
+
+        if (state == State.MSGSTR0) {
+            final Matcher msgStr0Matcher = state.pattern.matcher(line);
+            if (msgStr0Matcher.matches()) {
+                msgstr = msgStr0Matcher.group("value");
+                state = State.MSGSTR1; // next time, look for plural
+            } else {
+                append(translationsByKey);
+                return new Block();
+            }
+            return this;
+        }
+
+        if (state == State.MSGSTR1) {
+            final Matcher msgStr1Matcher = state.pattern.matcher(line);
+            if (msgStr1Matcher.matches()) {
+                msgstr_plural = msgStr1Matcher.group("value");
+            }
+            append(translationsByKey);
+            return new Block();
+        }
+        return this;
+    }
+
+    void append(final Map<ContextAndMsgId, String> translationsByKey) {
+        for (String context : contextList) {
+            if(msgid != null && msgstr != null) {
+                final ContextAndMsgId mc = new ContextAndMsgId(context, msgid, 
ContextAndMsgId.Type.REGULAR);
+                translationsByKey.put(mc, msgstr);
+            }
+            if(msgid_plural != null && msgstr_plural != null) {
+                final ContextAndMsgId mc = new ContextAndMsgId(context, 
msgid_plural, ContextAndMsgId.Type.PLURAL_ONLY);
+                translationsByKey.put(mc, msgstr_plural);
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/ContextAndMsgId.java
----------------------------------------------------------------------
diff --git 
a/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/ContextAndMsgId.java
 
b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/ContextAndMsgId.java
new file mode 100644
index 0000000..d00d08a
--- /dev/null
+++ 
b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/ContextAndMsgId.java
@@ -0,0 +1,90 @@
+package org.apache.isis.core.runtime.services.i18n.po;
+
+/**
+ * The combination of a <tt>msgId</tt> and context (optionally null) that 
represents a key to a translatable resource.
+ *
+ * <p>
+ *     For example, with this <i>.pot</i> file:
+ * </p>
+ * <pre>
+ * #: 
org.isisaddons.module.sessionlogger.dom.SessionLoggingServiceMenu#activeSessions()
+ msgid: "Active Sessions"
+
+ #: org.isisaddons.module.audit.dom.AuditingServiceMenu
+ #: org.isisaddons.module.command.dom.CommandServiceMenu
+ #: org.isisaddons.module.publishing.dom.PublishingServiceMenu
+ msgid: "Activity"
+
+ * </pre>
+ *
+ * <p>
+ *     the combination of 
<code>{org.isisaddons.module.sessionlogger.dom.SessionLoggingServiceMenu#activeSessions(),
 "Active Sessions"}</code> represents such a key, as does 
<code>{org.isisaddons.module.audit.dom.AuditingServiceMenu, "Activity"}</code>
+ * </p>
+ */
+public class ContextAndMsgId implements Comparable<ContextAndMsgId> {
+
+    public enum Type {
+        /**
+         * The text to use when there is no plural form, or the text to use 
for singular pattern when there is also a plural form.
+         */
+        REGULAR,
+        /**
+         * The text to use for plural form.
+         */
+        PLURAL_ONLY
+    }
+
+    private final String context;
+    private final String msgId;
+    private final Type type;
+
+    public ContextAndMsgId(final String context, final String msgId, final 
Type type) {
+        this.context = context == null? "": context;
+        this.msgId = msgId;
+        this.type = type;
+    }
+
+    public String getMsgId() {
+        return msgId;
+    }
+
+    public String getContext() {
+        return context;
+    }
+
+    /**
+     * Not part of equals/hashCode impl.
+     */
+    public Type getType() {
+        return type;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        final ContextAndMsgId that = (ContextAndMsgId) o;
+
+        if (context != null ? !context.equals(that.context) : that.context != 
null) return false;
+        if (msgId != null ? !msgId.equals(that.msgId) : that.msgId != null) 
return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = context != null ? context.hashCode() : 0;
+        result = 31 * result + (msgId != null ? msgId.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public int compareTo(final ContextAndMsgId o) {
+        final int i = msgId.compareTo(o.msgId);
+        if(i != 0) {
+            return i;
+        }
+        return context.compareTo(o.context);
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/PoAbstract.java
----------------------------------------------------------------------
diff --git 
a/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/PoAbstract.java
 
b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/PoAbstract.java
new file mode 100644
index 0000000..15777cc
--- /dev/null
+++ 
b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/PoAbstract.java
@@ -0,0 +1,43 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.core.runtime.services.i18n.po;
+
+import org.apache.isis.applib.services.i18n.TranslationService;
+
+abstract class PoAbstract {
+
+    protected final TranslationServicePo translationServicePo;
+    private final TranslationService.Mode mode;
+
+    PoAbstract(final TranslationServicePo translationServicePo, final 
TranslationService.Mode mode) {
+        this.translationServicePo = translationServicePo;
+        this.mode = mode;
+    }
+
+
+    abstract void shutdown();
+
+    abstract String translate(final String context, final String msgId);
+
+    abstract String translate(final String context, final String msgId, final 
String msgIdPlural, int num);
+
+    public TranslationService.Mode getMode() {
+        return mode;
+    }
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/PoReader.java
----------------------------------------------------------------------
diff --git 
a/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/PoReader.java
 
b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/PoReader.java
new file mode 100644
index 0000000..03cb9ed
--- /dev/null
+++ 
b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/PoReader.java
@@ -0,0 +1,190 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.core.runtime.services.i18n.po;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.isis.applib.services.i18n.TranslationService;
+import org.apache.isis.applib.services.i18n.TranslationsResolver;
+
+class PoReader extends PoAbstract {
+
+    public static final String LOCATION_BASE_URL = 
"isis.services.translation.po.locationBaseUrl";
+    public static Logger LOG = LoggerFactory.getLogger(PoReader.class);
+
+    private final Map<Locale, Map<ContextAndMsgId, String>> 
translationByKeyByLocale = Maps.newHashMap();
+
+    /**
+     * The basename of the translations file, hard-coded to 
<tt>translations</tt>.
+     *
+     * <p>
+     *     This means that the reader will search for 
<tt>translations_en-US.po</tt>, <tt>translations_en.po</tt>,
+     *     <tt>translations.po</tt>, according to the location that the 
provided {@link org.apache.isis.applib.services.i18n.TranslationsResolver} 
searches.  For example, if using the Wicket implementation, then will search 
for these files
+     *     under <tt>/WEB-INF</tt> directory.
+     * </p>
+     */
+    private final String basename = "translations";
+
+    private List<String> fallback;
+
+    public PoReader(final TranslationServicePo translationServicePo) {
+        super(translationServicePo, TranslationService.Mode.READ);
+    }
+
+    //region > init, shutdown
+    void init(final Map<String,String> config) {
+        fallback = readUrl(basename + ".po");
+        if(fallback == null) {
+            LOG.warn("No fallback translations found");
+            fallback = Collections.emptyList();
+        }
+    }
+
+    @Override
+    void shutdown() {
+    }
+    //endregion
+
+    public String translate(final String context, final String msgId) {
+        return translate(context, msgId, ContextAndMsgId.Type.REGULAR);
+    }
+
+    @Override
+    String translate(final String context, final String msgId, final String 
msgIdPlural, final int num) {
+
+        final String msgIdToUse;
+        final ContextAndMsgId.Type type;
+        if (num == 1) {
+            msgIdToUse = msgId;
+            type = ContextAndMsgId.Type.REGULAR;
+        } else {
+            msgIdToUse = msgIdPlural;
+            type = ContextAndMsgId.Type.PLURAL_ONLY;
+        }
+
+        return translate(context, msgIdToUse, type);
+    }
+
+    private String translate(
+            final String context, final String msgId, final 
ContextAndMsgId.Type type) {
+
+        final Locale targetLocale;
+        try {
+            targetLocale = 
translationServicePo.getLocaleProvider().getLocale();
+        } catch(final RuntimeException ex){
+            LOG.warn("Failed to obtain locale, returning the original msgId");
+            return msgId;
+        }
+
+        final Map<ContextAndMsgId, String> translationsByKey = 
readAndCacheTranslationsIfRequired(targetLocale);
+
+        final ContextAndMsgId key = new ContextAndMsgId(context, msgId, type);
+        final String translation = lookupTranslation(translationsByKey, key);
+        if (!Strings.isNullOrEmpty(translation)) {
+            return translation;
+        }
+
+        final ContextAndMsgId keyNoContext = new ContextAndMsgId("", msgId, 
type);
+        final String translationNoContext = 
lookupTranslation(translationsByKey, keyNoContext);
+        if (!Strings.isNullOrEmpty(translationNoContext)) {
+            return translationNoContext;
+        }
+
+        LOG.warn("No translation found for: " + key);
+        return msgId;
+    }
+
+    private String lookupTranslation(final Map<ContextAndMsgId, String> 
translationsByKey, final ContextAndMsgId key) {
+        final String s = translationsByKey.get(key);
+        return s != null? s.trim(): null;
+    }
+
+    private Map<ContextAndMsgId, String> 
readAndCacheTranslationsIfRequired(final Locale locale) {
+        Map<ContextAndMsgId, String> translationsByKey = 
translationByKeyByLocale.get(locale);
+        if(translationsByKey != null) {
+            return translationsByKey;
+        }
+
+        translationsByKey = Maps.newHashMap();
+        read(locale, translationsByKey);
+        translationByKeyByLocale.put(locale, translationsByKey);
+
+        return translationsByKey;
+    }
+
+
+    /**
+     * @param locale - the .po file to load
+     * @param translationsByKey - the translations to be populated
+     */
+    private void read(final Locale locale, final Map<ContextAndMsgId, String> 
translationsByKey) {
+        final List<String> contents = readPo(locale);
+
+        Block block = new Block();
+        for (final String line : contents) {
+            block = block.parseLine(line, translationsByKey);
+        }
+    }
+
+    protected List<String> readPo(final Locale locale) {
+        final List<String> lines = readPoElseNull(locale);
+        if(lines != null) {
+            return lines;
+        }
+        LOG.warn("Could not locate translations for locale: " + locale + ", 
using fallback");
+        return fallback;
+    }
+
+    private List<String> readPoElseNull(final Locale locale) {
+        final String country = locale.getCountry().toUpperCase(Locale.ROOT);
+        final String language = locale.getLanguage().toLowerCase(Locale.ROOT);
+
+        final List<String> candidates = Lists.newArrayList();
+        if(!Strings.isNullOrEmpty(language)) {
+            if(!Strings.isNullOrEmpty(country)) {
+                candidates.add(basename + "_" + language + "-" + country+ 
".po");
+            }
+            candidates.add(basename + "_" + language + ".po");
+        }
+
+        for (final String candidate : candidates) {
+            final List<String> lines = readUrl(candidate);
+            if(lines != null) {
+                return lines;
+            }
+        }
+        return null;
+    }
+
+    private List<String> readUrl(final String candidate) {
+        final TranslationsResolver translationsResolver = 
translationServicePo.getTranslationsResolver();
+        if(translationsResolver == null) {
+            return null;
+        }
+        return translationsResolver.readLines(candidate);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/PoWriter.java
----------------------------------------------------------------------
diff --git 
a/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/PoWriter.java
 
b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/PoWriter.java
new file mode 100644
index 0000000..5b850d6
--- /dev/null
+++ 
b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/PoWriter.java
@@ -0,0 +1,141 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.core.runtime.services.i18n.po;
+
+import java.util.SortedMap;
+import java.util.SortedSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.joda.time.LocalDateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.isis.applib.services.i18n.TranslationService;
+
+class PoWriter extends PoAbstract {
+
+    public static Logger LOG = LoggerFactory.getLogger(PoWriter.class);
+
+    private static class Block {
+        private final String msgId;
+        private final SortedSet<String> contexts = Sets.newTreeSet();
+        private String msgIdPlural;
+
+        private Block(final String msgId) {
+            this.msgId = msgId;
+        }
+    }
+
+    private final SortedMap<String, Block> blocksByMsgId = Maps.newTreeMap();
+
+    public PoWriter(final TranslationServicePo translationServicePo) {
+        super(translationServicePo, TranslationService.Mode.WRITE);
+    }
+
+    //region > shutdown
+
+    @Override
+    void shutdown() {
+        final StringBuilder buf = new StringBuilder();
+        buf.append("\n");
+        
buf.append("\n##############################################################################");
+        buf.append("\n#");
+        buf.append("\n# .pot file");
+        buf.append("\n#");
+        buf.append("\n# generated at: 
").append(LocalDateTime.now().toString("yyyy-MM-dd HH:mm:ss"));
+        buf.append("\n# generated by: 
").append(TranslationServicePo.class.getSimpleName());
+        buf.append("\n#");
+        buf.append("\n# Translate this file to each required language and 
place in WEB-INF, eg:");
+        buf.append("\n#");
+        buf.append("\n#     /WEB-INF/translations_en-US.po");
+        buf.append("\n#     /WEB-INF/translations_en.po");
+        buf.append("\n#     /WEB-INF/translations_fr-FR.po");
+        buf.append("\n#     /WEB-INF/translations_fr.po");
+        buf.append("\n#     /WEB-INF/translations.po");
+        buf.append("\n#");
+        buf.append("\n# If the app uses TranslatableString (eg for 
internationalized validation");
+        buf.append("\n# messages), or if the app calls the TranslationService 
directly, then ensure");
+        buf.append("\n# that all text to be translated has been captured by 
running a full");
+        buf.append("\n# integration test suite that exercises all relevant 
behaviour");
+        buf.append("\n#");
+        
buf.append("\n##############################################################################");
+        buf.append("\n");
+        buf.append("\n");
+        buf.append(toPot());
+        buf.append("\n");
+        buf.append("\n");
+        
buf.append("\n##############################################################################");
+        buf.append("\n# end of .pot file");
+        
buf.append("\n##############################################################################");
+        buf.append("\n");
+        LOG.info(buf.toString());
+    }
+    //endregion
+
+
+    public String translate(final String context, final String msgId) {
+
+        final Block block = blockFor(msgId);
+        block.contexts.add(context);
+
+        return msgId;
+    }
+
+    @Override
+    String translate(final String context, final String msgId, final String 
msgIdPlural, final int num) {
+
+        final Block block = blockFor(msgId);
+        block.contexts.add(context);
+        block.msgIdPlural = msgIdPlural;
+
+        return null;
+    }
+
+    private Block blockFor(final String msgId) {
+        Block block = blocksByMsgId.get(msgId);
+        if(block == null) {
+            block = new Block(msgId);
+            blocksByMsgId.put(msgId, block);
+        }
+        return block;
+    }
+
+    /**
+     * Not API
+     */
+    String toPot() {
+        final StringBuilder buf = new StringBuilder();
+        for (final String msgId : blocksByMsgId.keySet()) {
+            final Block block = blocksByMsgId.get(msgId);
+            for (final String context : block.contexts) {
+                buf.append("#: ").append(context).append("\n");
+            }
+            buf.append("msgid \"").append(msgId).append("\"\n");
+            if(block.msgIdPlural == null) {
+                buf.append("msgstr \"\"\n");
+            } else {
+                buf.append("msgid_plural 
\"").append(block.msgIdPlural).append("\"\n");
+                buf.append("msgstr[0] \"\"\n");
+                buf.append("msgstr[1] \"\"\n");
+            }
+            buf.append("\n\n");
+        }
+        return buf.toString();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/TranslationServicePo.java
----------------------------------------------------------------------
diff --git 
a/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/TranslationServicePo.java
 
b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/TranslationServicePo.java
new file mode 100644
index 0000000..7bba5b2
--- /dev/null
+++ 
b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/TranslationServicePo.java
@@ -0,0 +1,149 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.core.runtime.services.i18n.po;
+
+import java.util.Map;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.isis.applib.annotation.DomainService;
+import org.apache.isis.applib.annotation.Programmatic;
+import org.apache.isis.applib.services.i18n.LocaleProvider;
+import org.apache.isis.applib.services.i18n.TranslationService;
+import org.apache.isis.applib.services.i18n.TranslationsResolver;
+import org.apache.isis.core.runtime.system.DeploymentType;
+import org.apache.isis.core.runtime.system.context.IsisContext;
+
+@DomainService
+public class TranslationServicePo implements TranslationService {
+
+    public static Logger LOG = 
LoggerFactory.getLogger(TranslationServicePo.class);
+
+    public static final String KEY_DEPLOYMENT_TYPE = "isis.deploymentType";
+    public static final String KEY_PO_MODE = 
"isis.services.translation.po.mode";
+
+    private PoAbstract po;
+
+    /**
+     * Defaults to writer mode because the service won't have been init'd 
while the metamodel is bring instantiated,
+     * and we want to ensure that we capture all requests for translation.
+     */
+    public TranslationServicePo() {
+        po = new PoWriter(this);
+    }
+
+    //region > init, shutdown
+
+    @Programmatic
+    @PostConstruct
+    public void init(final Map<String,String> config) {
+
+        if(getLocaleProvider() == null || getTranslationsResolver() == null) {
+            // remain in write mode
+            return;
+        }
+
+        final boolean prototypeOrTest = isPrototypeOrTest();
+
+        final String translationMode = config.get(KEY_PO_MODE);
+        final boolean forceRead =
+                translationMode != null &&
+                        ("read".equalsIgnoreCase(translationMode) ||
+                         "reader".equalsIgnoreCase(translationMode));
+
+        if(prototypeOrTest && !forceRead) {
+            // remain in write mode
+            return;
+        }
+
+        // switch to read mode
+        final PoReader poReader = new PoReader(this);
+        poReader.init(config);
+        po = poReader;
+    }
+
+    protected boolean isPrototypeOrTest() {
+        final DeploymentType deploymentType = getDeploymentType();
+        return !deploymentType.isProduction();
+    }
+
+    @Programmatic
+    @PreDestroy
+    public void shutdown() {
+        po.shutdown();
+    }
+    //endregion
+
+
+    @Override
+    @Programmatic
+    public String translate(final String context, final String text) {
+        return po.translate(context, text);
+    }
+
+    @Override
+    public String translate(final String context, final String singularText, 
final String pluralText, final int num) {
+        return po.translate(context, singularText, pluralText, num);
+    }
+
+    @Override
+    public Mode getMode() {
+        return po.getMode();
+    }
+
+    /**
+     * Not API
+     */
+    @Programmatic
+    public String toPo() {
+        if (!getMode().isWrite()) {
+            throw new IllegalStateException("Not in write mode");
+        }
+        return  ((PoWriter)po).toPot();
+    }
+
+    // //////////////////////////////////////
+
+    DeploymentType getDeploymentType() {
+        return IsisContext.getDeploymentType();
+    }
+
+
+    @Inject
+    private
+    TranslationsResolver translationsResolver;
+
+    @Programmatic
+    TranslationsResolver getTranslationsResolver() {
+        return translationsResolver;
+    }
+
+    @Inject
+    private
+    LocaleProvider localeProvider;
+
+    @Programmatic
+    LocaleProvider getLocaleProvider() {
+        return localeProvider;
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/TranslationServicePoMenu.java
----------------------------------------------------------------------
diff --git 
a/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/TranslationServicePoMenu.java
 
b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/TranslationServicePoMenu.java
new file mode 100644
index 0000000..e349668
--- /dev/null
+++ 
b/core/runtime/src/main/java/org/apache/isis/core/runtime/services/i18n/po/TranslationServicePoMenu.java
@@ -0,0 +1,94 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.core.runtime.services.i18n.po;
+
+import java.util.List;
+import javax.inject.Inject;
+import org.apache.isis.applib.Identifier;
+import org.apache.isis.applib.IsisApplibModule;
+import org.apache.isis.applib.annotation.Action;
+import org.apache.isis.applib.annotation.ActionLayout;
+import org.apache.isis.applib.annotation.DomainService;
+import org.apache.isis.applib.annotation.DomainServiceLayout;
+import org.apache.isis.applib.annotation.NatureOfService;
+import org.apache.isis.applib.annotation.ParameterLayout;
+import org.apache.isis.applib.annotation.RestrictTo;
+import org.apache.isis.applib.annotation.SemanticsOf;
+import org.apache.isis.applib.value.Clob;
+
+@DomainService(
+        nature = NatureOfService.VIEW_MENU_ONLY
+)
+@DomainServiceLayout(
+        menuBar = DomainServiceLayout.MenuBar.SECONDARY,
+        named = "Prototyping"
+)
+public class TranslationServicePoMenu {
+
+    public static abstract class ActionDomainEvent extends 
IsisApplibModule.ActionDomainEvent<TranslationServicePoMenu> {
+        public ActionDomainEvent(final TranslationServicePoMenu source, final 
Identifier identifier) {
+            super(source, identifier);
+        }
+
+        public ActionDomainEvent(final TranslationServicePoMenu source, final 
Identifier identifier, final Object... arguments) {
+            super(source, identifier, arguments);
+        }
+
+        public ActionDomainEvent(final TranslationServicePoMenu source, final 
Identifier identifier, final List<Object> arguments) {
+            super(source, identifier, arguments);
+        }
+    }
+
+    // //////////////////////////////////////
+
+    public static class DownloadPotFileDomainEvent extends ActionDomainEvent {
+        public DownloadPotFileDomainEvent(final TranslationServicePoMenu 
source, final Identifier identifier, final Object... arguments) {
+            super(source, identifier, arguments);
+        }
+    }
+
+    @Action(
+            domainEvent = DownloadPotFileDomainEvent.class,
+            semantics = SemanticsOf.SAFE,
+            restrictTo = RestrictTo.PROTOTYPING
+    )
+    @ActionLayout(
+            cssClassFa = "fa-download"
+    )
+    public Clob downloadPotFile(
+            @ParameterLayout(named = ".pot file name")
+            final String potFileName) {
+        final String chars = translationService.toPo();
+        return new Clob(potFileName, "text/plain", chars);
+    }
+
+    public String default0DownloadPotFile() {
+        return "translations.pot";
+    }
+    public boolean hideDownloadPotFile() {
+        return translationService.getMode().isRead();
+    }
+
+    // //////////////////////////////////////
+
+
+    @Inject
+    private TranslationServicePo translationService;
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/362a5bfc/core/runtime/src/main/java/org/apache/isis/core/webapp/IsisWebAppBootstrapper.java
----------------------------------------------------------------------
diff --git 
a/core/runtime/src/main/java/org/apache/isis/core/webapp/IsisWebAppBootstrapper.java
 
b/core/runtime/src/main/java/org/apache/isis/core/webapp/IsisWebAppBootstrapper.java
index 7fae462..f16068b 100644
--- 
a/core/runtime/src/main/java/org/apache/isis/core/webapp/IsisWebAppBootstrapper.java
+++ 
b/core/runtime/src/main/java/org/apache/isis/core/webapp/IsisWebAppBootstrapper.java
@@ -87,10 +87,10 @@ public class IsisWebAppBootstrapper implements 
ServletContextListener {
                     new ResourceStreamSourceForWebInf(servletContext)) ;
 
             if ( configLocation != null ) {
-              LOG.info( "Config override location: " + configLocation );
-              
compositeSource.addResourceStreamSource(ResourceStreamSourceFileSystem.create(configLocation));
+                LOG.info( "Config override location: " + configLocation );
+                
compositeSource.addResourceStreamSource(ResourceStreamSourceFileSystem.create(configLocation));
             } else {
-              LOG.info( "Config override location: No override location 
configured" );
+                LOG.info( "Config override location: No override location 
configured" );
             }
             
             // will load either from WEB-INF, from the classpath or from 
config directory.

Reply via email to