/*
* 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 java.util.Set;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.isis.applib.services.i18n.LocaleProvider;
import org.apache.isis.applib.services.i18n.TranslationService;
import org.apache.isis.applib.services.i18n.TranslationsResolver;
class PoReader extends PoAbstract {
public static final String DASH = "-";
public static final String UNDERSCORE = "_";
public static Logger LOG = LoggerFactory.getLogger(PoReader.class);
private final Map<Locale, Map<ContextAndMsgId, String>> translationByKeyByLocale = Maps.newHashMap();
private final Map<Locale, Boolean> usesFallbackByLocale = 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.
* </p>
*
* <p>
* 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 final TranslationsResolver translationsResolver;
private final LocaleProvider localeProvider;
private List<String> fallback;
public PoReader(final TranslationServicePo translationServicePo) {
super(translationServicePo, TranslationService.Mode.READ);
translationsResolver = translationServicePo.getTranslationsResolver();
if(translationsResolver == null) {
LOG.warn("No translationsResolver available");
}
localeProvider = translationServicePo.getLocaleProvider();
}
//region > init, shutdown
/**
* Not API
*/
void init() {
fallback = readUrl(basename + ".po");
if(fallback == null) {
LOG.info("No fallback translations found; i18n is in effect disabled for this application");
fallback = Collections.emptyList();
}
}
@Override
void shutdown() {
}
//endregion
public String translate(final String context, final String msgId) {
if(translationsResolver == null) {
// already logged as WARN (in constructor) if null.
return 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);
}
void clearCache() {
translationByKeyByLocale.clear();
usesFallbackByLocale.clear();
init();
}
private String translate(
final String context, final String msgId, final ContextAndMsgId.Type type) {
final Locale targetLocale;
try {
targetLocale = localeProvider.getLocale();
if(targetLocale == null) {
// eg if request from RO viewer and the (default) LocaleProviderWicket is being used.
return msgId;
}
} catch(final RuntimeException ex){
logInfoIfNotPreviously("Failed to obtain locale, returning the original msgId");
return msgId;
}
final Map<ContextAndMsgId, String> translationsByKey = readAndCacheTranslationsIfRequired(targetLocale);
// search for translation with a context
final ContextAndMsgId key = new ContextAndMsgId(context, msgId, type);
final String translation = lookupTranslation(translationsByKey, key);
if (!Strings.isNullOrEmpty(translation)) {
return translation;
}
// else search for translation without a context
final ContextAndMsgId keyNoContext = new ContextAndMsgId("", msgId, type);
final String translationNoContext = lookupTranslation(translationsByKey, keyNoContext);
if (!Strings.isNullOrEmpty(translationNoContext)) {
return translationNoContext;
}
// to avoid chattiness in the log, we only log if there are ANY translations at all for the target locale.
// the algorithm for searching for translations looks for:
// 1. language_country
// 2. language
// 3. fallback
// so this message is only ever displayed if the locale isn't using fallback (ie a translation is genuinely missing)
final Boolean usesFallback = usesFallbackByLocale.get(targetLocale);
if(usesFallback == null || !usesFallback) {
logInfoIfNotPreviously("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) {
usesFallbackByLocale.put(locale, false);
return lines;
}
// this is only ever logged the first time that a user using this particular locale is encountered
logInfoIfNotPreviously("Could not locate translations for locale: " + locale + ", using fallback");
usesFallbackByLocale.put(locale, true);
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 + DASH + language + UNDERSCORE + country+ ".po");
candidates.add(basename + DASH + language + DASH + country+ ".po");
candidates.add(basename + UNDERSCORE + language + UNDERSCORE + country+ ".po");
candidates.add(basename + UNDERSCORE + language + DASH + country+ ".po");
}
candidates.add(basename + DASH + language + ".po");
candidates.add(basename + UNDERSCORE + 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) {
return translationsResolver.readLines(candidate);
}
// to avoid flooding the logs
private void logInfoIfNotPreviously(final String infoMessage) {
if(!loggedInfoMessages.contains(infoMessage)) {
LOG.info(infoMessage);
loggedInfoMessages.add(infoMessage);
}
}
private final Set<String> loggedInfoMessages = Sets.newConcurrentHashSet();
}