I18n.java

/*
 * This file is part of Indicators.
 *
 * Indicators is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Indicators is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Indicators. If not, see <https://www.gnu.org/licenses/>.
 */
package fr.inrae.agroclim.indicators.resources;

import static java.lang.Math.abs;

import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Predicate;

/**
 * Localized messages with plural handling à la GWT.
 *
 * Last change $Date$
 *
 * @author $Author$
 * @version $Revision$
 */
public final class I18n {
    /**
     * Accepted operators, ordered.
     */
    public enum Operator implements BiFunction<Integer, Integer, Boolean> {
        /**
         * Equals.
         */
        AEQ("=", Objects::equals),
        /**
         * Inferior or equals.
         */
        BINFEQ("<=", (a, b) -> a <= b),
        /**
         * Strictly inferior.
         */
        CINF("<", (a, b) -> a < b),
        /**
         * Superior or equals.
         */
        DSUPEQ(">=", (a, b) -> a >= b),
        /**
         * Strictly superior.
         */
        ESUP(">", (a, b) -> a > b);
        /**
         * Guess the right operator in the string comparison.
         *
         * @param string string comparison (eg.: ">=10")
         * @return operator matching the string comparison
         */
        static Optional<Operator> extract(final String string) {
            for (final Operator op : values()) {
                if (string.startsWith(op.symbol)) {
                    return Optional.of(op);
                }
            }
            return Optional.empty();
        }
        /**
         * Comparison function of the operator.
         */
        private final BiPredicate<Integer, Integer> function;
        /**
         * String representation of the operator.
         */
        private final String symbol;

        /**
         * Constructor.
         *
         * @param string String representation of the operator.
         * @param func Comparison function of the operator.
         */
        Operator(final String string,
                final BiPredicate<Integer, Integer> func) {
            symbol = string;
            function = func;
        }

        @Override
        public Boolean apply(final Integer arg0, final Integer arg1) {
            return function.test(arg0, arg1);
        }

        /**
         * @return length of string representation
         */
        public int getLength() {
            return symbol.length();
        }
    }

    /**
     * Standard suffix for keys in .properties, à la GWT.
     *
     * http://www.gwtproject.org/doc/latest/DevGuideI18nPluralForms.html
     */
    @SuppressWarnings("checkstyle:MagicNumber")
    public enum PluralSuffix implements Predicate<Integer> {
        /**
         * The count is 0.
         */
        NONE(nb -> nb == 0),
        /**
         * The count is 1.
         */
        ONE(nb -> nb == 1),
        /**
         * The count is 2.
         */
        TWO(nb -> nb == 2),
        /**
         * The last two digits are from 03-10.
         */
        FEW(nb -> nb % 100 >= 3 && nb % 100 <= 10),
        /**
         * the last two digits are from 11-99.
         */
        MANY(nb -> nb % 100 >= 11 && nb % 100 <= 99);

        /**
         * Find the suffix according to the value.
         *
         * @param value value to find the prefix
         * @return found prefix or empty
         */
        static Optional<String> getSuffix(final Integer value) {
            for (final PluralSuffix suffix : values()) {
                if (suffix.test(value)) {
                    return Optional.of(suffix.name().toLowerCase());
                }
            }
            return Optional.empty();
        }

        /**
         * Predicate to check if the value match the plural suffix.
         */
        private final Predicate<Integer> predicate;

        /**
         * Constructor.
         *
         * @param pr predicate for the prefix
         */
        PluralSuffix(final Predicate<Integer> pr) {
            predicate = pr;
        }

        @Override
        public boolean test(final Integer value) {
            if (value != null) {
                return predicate.test(abs(value));
            }
            return false;
        }

    }

    /**
     * Check if the comparison string matches the value.
     *
     * @param comparison comparison string (eg.: ">10")
     * @param plural value
     * @return if the comparison string matches the value.
     */
    static boolean matches(final String comparison, final int plural) {
        final Optional<Operator> operator = Operator.extract(comparison);
        if (operator.isPresent()) {
            final Operator op = operator.get();
            String val = comparison.substring(op.getLength());
            if (val != null) {
                val = val.trim();
                if (!val.isEmpty()) {
                    final Integer value = Integer.valueOf(val);
                    return op.apply(plural, value);
                }
            }
        }
        return false;
    }

    /**
     * Resources from .properties file.
     */
    private final Resources resources;

    /**
     * Constructor.
     *
     * @param bundle Resources bundle build outside.
     */
    public I18n(final ResourceBundle bundle) {
        resources = new Resources(bundle);
    }

    /**
     * Constructor.
     *
     * @param bundleName Path of .property resource.
     * @param locale The locale for the bundle.
     */
    public I18n(final String bundleName, final Locale locale) {
        resources = new Resources(bundleName, locale);
    }

    /**
     * Return message with inlined arguments.
     *
     * @param plural value for plural form
     * @param key message key
     * @param messageArguments arguments for the message.
     * @return message with arguments or exclamation message
     */
    public String format(final int plural, final String key,
            final Object... messageArguments) {
        String keyWithSuffix;

        // the suffix for the value
        keyWithSuffix = key + "[=" + plural + "]";
        if (resources.getKeys().contains(keyWithSuffix)) {
            return resources.format(keyWithSuffix, messageArguments);
        }

        // find the right standard suffix
        final Optional<String> suffix = PluralSuffix.getSuffix(plural);
        if (suffix.isPresent()) {
            keyWithSuffix = key + "[" + suffix.get() + "]";
            if (resources.getKeys().contains(keyWithSuffix)) {
                return resources.format(keyWithSuffix, messageArguments);
            }
        }

        // with comparators <, <=, >, >=
        final List<String> suffixes = resources.getKeys().stream() //
                .filter(k -> k.startsWith(key + "[") && k.endsWith("]")) //
                .map(k -> k.substring(key.length() + 1, k.length() - 1)) //
                .toList();
        for (final String suf: suffixes) {
            if (matches(suf, plural)) {
                keyWithSuffix = key + "[" + suf + "]";
                return resources.format(keyWithSuffix, messageArguments);
            }
        }
        // if not defined, used default
        return resources.format(key, messageArguments);
    }

    /**
     * Return message with inlined arguments.
     *
     * @param key message key
     * @param messageArguments arguments for the message.
     * @return message with arguments or exclamation message
     */
    public String format(final String key,
            final Object... messageArguments) {
        return resources.format(key, messageArguments);
    }

    /**
     * Retrieve message from key.
     *
     * @param key message key
     * @return message value or exclamation message
     */
    public String get(final String key) {
        return resources.getString(key);
    }

    /**
     * @return Path of .property resource.
     */
    public String getBundleName() {
        return resources.getBundleName();
    }

    /**
     * @param locale Locale for the bundle.
     */
    public void setLocale(final Locale locale) {
        resources.setLocale(locale);
    }
}