/* * 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 }