/*
* 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.applib.services.i18n;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.common.collect.Lists;
import org.apache.isis.applib.annotation.Programmatic;
import org.apache.isis.applib.annotation.Value;
@Value
public final class TranslatableString {
//region > tr, trn (factory methods); constructor
/**
* A translatable string with a single pattern for both singular and plural forms.
*
* @param pattern - pattern for all (singular and plural) form, with <code>${xxx}</code> placeholders
* @param paramArgs - parameter/argument pairs (string and object, string and object, ...)
*/
public static TranslatableString tr(
final String pattern,
final Object... paramArgs) {
if(pattern == null) {
return null;
}
return new TranslatableString(Type.TR, pattern, null, 1, asMap(paramArgs));
}
/**
* A translatable string with different patterns for singular and plural forms, selected automatically by the number
*
* @param singularPattern - pattern for the singular form, with <code>${xxx}</code> placeholders
* @param pluralPattern - pattern for the singular form, with <code>${xxx}</code> placeholders
* @param number - whether to use singular or plural form when rendering
* @param paramArgs - parameter/argument pairs (string and object, string and object, ...)
*/
public static TranslatableString trn(
final String singularPattern,
final String pluralPattern,
final int number,
final Object... paramArgs) {
return new TranslatableString(Type.TRN, singularPattern, pluralPattern, number, asMap(paramArgs));
}
/**
* Converts a list of objects [a, 1, b, 2] into a map {a -> 1; b -> 2}
*/
private static Map<String, Object> asMap(final Object[] paramArgs) {
final HashMap<String, Object> map = new HashMap<String, Object>();
boolean param = true;
String paramStr = null;
for (final Object paramArg : paramArgs) {
if (param) {
if (paramArg instanceof String) {
paramStr = (String) paramArg;
} else {
throw new IllegalArgumentException("Parameter must be a string");
}
} else {
final Object arg = paramArg;
map.put(paramStr, arg);
paramStr = null;
}
param = !param;
}
if (paramStr != null) {
throw new IllegalArgumentException("Must have equal number of parameters and arguments");
}
return map;
}
private TranslatableString(
final Type type,
final String singularText,
final String pluralText,
final int number,
final Map<String, Object> argumentsByParameterName) {
this.type = type;
this.singularText = singularText;
this.pluralText = pluralText;
this.number = number;
this.argumentsByParameterName = argumentsByParameterName;
}
//endregion
//region > singularText, pluralText, pluralForm
private final String singularText;
private final String pluralText;
/**
* The text as provided in (either of the {@link #tr(String, Object...) factory} {@link #trn(String, String, int, Object...) method}s,
* with placeholders rather than substituted arguments; if {@link #isPluralForm()} is <code>true</code> then used only
* for the singular form.
*/
String getSingularText() {
return singularText;
}
/**
* The plural text as provided in the {@link #trn(String, String, int, Object...) factory method}, with placeholders
* rather than substituted arguments; but will be <code>null</code> if {@link #isPluralForm()} is <code>false</code>.
*/
String getPluralText() {
return pluralText;
}
private enum Type {
/**
* No plurals
*/
TR {
@Override
public String toString(final TranslatableString trString) {
return "tr: " + trString.singularText;
}
},
/**
* With plurals
*/
TRN {
@Override
public String toString(final TranslatableString trString) {
return "trn: " + trString.pluralText;
}
};
public abstract String toString(final TranslatableString trString);
}
private final Type type;
private final int number;
boolean isPluralForm() {
return type == Type.TRN;
}
//endregion
//region > argumentsByParameterName
private final Map<String, Object> argumentsByParameterName;
/**
* The arguments; excluded from {@link #equals(Object)} comparison.
*/
Map<String, Object> getArgumentsByParameterName() {
return argumentsByParameterName;
}
//endregion
//region > translate
/**
* Translates this string using the provided {@link org.apache.isis.applib.services.i18n.TranslationService}, selecting
* either the single or plural form as per {@link #getPattern()}.
* @param translationService
* @param context
* @return
*/
@Programmatic
public String translate(final TranslationService translationService, final String context) {
final String translatedText =
!isPluralForm()
? translationService.translate(context, getSingularText())
: translationService.translate(context, getSingularText(), getPluralText(), number);
return translated(translatedText);
}
/**
* The text to be translated; depends on whether {@link #isPluralForm()} and whether to be translated.
*
* <p>
* Any placeholders will <i>not</i> have been replaced.
* </p>
*
* <p>
* NB: this method is exposed only so that implementations of
* {@link org.apache.isis.applib.services.exceprecog.TranslatableException} can return a non-null
* {@link Exception#getMessage() message} when only a translatable message has been provided.
* </p>
*/
@Programmatic
public String getPattern() {
return !isPluralForm() || number == 1 ? getSingularText() : getPluralText();
}
String translated(final String translatedText) {
return format(translatedText, argumentsByParameterName);
}
static String format(String format, Map<String, Object> values)
{
StringBuilder formatter = new StringBuilder(format);
List<Object> valueList = Lists.newArrayList();
Matcher matcher = Pattern.compile("\\{(\\w+)}").matcher(format);
while (matcher.find())
{
String key = matcher.group(1);
String formatKey = String.format("{%s}", key);
int index = formatter.indexOf(formatKey);
if (index != -1)
{
formatter.replace(index, index + formatKey.length(), "%s");
valueList.add(values.get(key));
}
}
return String.format(formatter.toString(), valueList.toArray());
}
//endregion
//region > equals, hashCode, toString
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final TranslatableString that = (TranslatableString) o;
if (pluralText != null ? !pluralText.equals(that.pluralText) : that.pluralText != null)
return false;
if (singularText != null ? !singularText.equals(that.singularText) : that.singularText != null)
return false;
if (type != that.type) return false;
return true;
}
@Override
public int hashCode() {
int result = type != null ? type.hashCode() : 0;
result = 31 * result + (singularText != null ? singularText.hashCode() : 0);
result = 31 * result + (pluralText != null ? pluralText.hashCode() : 0);
return result;
}
@Override
public String toString() {
return type.toString(this);
}
//endregion
}