1 /*
2 * This file is part of Indicators.
3 *
4 * Indicators is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * Indicators is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with Indicators. If not, see <https://www.gnu.org/licenses/>.
16 */
17 package fr.inrae.agroclim.indicators.resources;
18
19 import static java.lang.Math.abs;
20
21 import java.util.List;
22 import java.util.Locale;
23 import java.util.Objects;
24 import java.util.Optional;
25 import java.util.ResourceBundle;
26 import java.util.function.BiFunction;
27 import java.util.function.BiPredicate;
28 import java.util.function.Predicate;
29
30 /**
31 * Localized messages with plural handling à la GWT.
32 *
33 * Last change $Date$
34 *
35 * @author $Author$
36 * @version $Revision$
37 */
38 public final class I18n {
39 /**
40 * Accepted operators, ordered.
41 */
42 public enum Operator implements BiFunction<Integer, Integer, Boolean> {
43 /**
44 * Equals.
45 */
46 AEQ("=", Objects::equals),
47 /**
48 * Inferior or equals.
49 */
50 BINFEQ("<=", (a, b) -> a <= b),
51 /**
52 * Strictly inferior.
53 */
54 CINF("<", (a, b) -> a < b),
55 /**
56 * Superior or equals.
57 */
58 DSUPEQ(">=", (a, b) -> a >= b),
59 /**
60 * Strictly superior.
61 */
62 ESUP(">", (a, b) -> a > b);
63 /**
64 * Guess the right operator in the string comparison.
65 *
66 * @param string string comparison (eg.: ">=10")
67 * @return operator matching the string comparison
68 */
69 static Optional<Operator> extract(final String string) {
70 for (final Operator op : values()) {
71 if (string.startsWith(op.symbol)) {
72 return Optional.of(op);
73 }
74 }
75 return Optional.empty();
76 }
77 /**
78 * Comparison function of the operator.
79 */
80 private final BiPredicate<Integer, Integer> function;
81 /**
82 * String representation of the operator.
83 */
84 private final String symbol;
85
86 /**
87 * Constructor.
88 *
89 * @param string String representation of the operator.
90 * @param func Comparison function of the operator.
91 */
92 Operator(final String string,
93 final BiPredicate<Integer, Integer> func) {
94 symbol = string;
95 function = func;
96 }
97
98 @Override
99 public Boolean apply(final Integer arg0, final Integer arg1) {
100 return function.test(arg0, arg1);
101 }
102
103 /**
104 * @return length of string representation
105 */
106 public int getLength() {
107 return symbol.length();
108 }
109 }
110
111 /**
112 * Standard suffix for keys in .properties, à la GWT.
113 *
114 * http://www.gwtproject.org/doc/latest/DevGuideI18nPluralForms.html
115 */
116 @SuppressWarnings("checkstyle:MagicNumber")
117 public enum PluralSuffix implements Predicate<Integer> {
118 /**
119 * The count is 0.
120 */
121 NONE(nb -> nb == 0),
122 /**
123 * The count is 1.
124 */
125 ONE(nb -> nb == 1),
126 /**
127 * The count is 2.
128 */
129 TWO(nb -> nb == 2),
130 /**
131 * The last two digits are from 03-10.
132 */
133 FEW(nb -> nb % 100 >= 3 && nb % 100 <= 10),
134 /**
135 * the last two digits are from 11-99.
136 */
137 MANY(nb -> nb % 100 >= 11 && nb % 100 <= 99);
138
139 /**
140 * Find the suffix according to the value.
141 *
142 * @param value value to find the prefix
143 * @return found prefix or empty
144 */
145 static Optional<String> getSuffix(final Integer value) {
146 for (final PluralSuffix suffix : values()) {
147 if (suffix.test(value)) {
148 return Optional.of(suffix.name().toLowerCase());
149 }
150 }
151 return Optional.empty();
152 }
153
154 /**
155 * Predicate to check if the value match the plural suffix.
156 */
157 private final Predicate<Integer> predicate;
158
159 /**
160 * Constructor.
161 *
162 * @param pr predicate for the prefix
163 */
164 PluralSuffix(final Predicate<Integer> pr) {
165 predicate = pr;
166 }
167
168 @Override
169 public boolean test(final Integer value) {
170 if (value != null) {
171 return predicate.test(abs(value));
172 }
173 return false;
174 }
175
176 }
177
178 /**
179 * Check if the comparison string matches the value.
180 *
181 * @param comparison comparison string (eg.: ">10")
182 * @param plural value
183 * @return if the comparison string matches the value.
184 */
185 static boolean matches(final String comparison, final int plural) {
186 final Optional<Operator> operator = Operator.extract(comparison);
187 if (operator.isPresent()) {
188 final Operator op = operator.get();
189 String val = comparison.substring(op.getLength());
190 if (val != null) {
191 val = val.trim();
192 if (!val.isEmpty()) {
193 final Integer value = Integer.valueOf(val);
194 return op.apply(plural, value);
195 }
196 }
197 }
198 return false;
199 }
200
201 /**
202 * Resources from .properties file.
203 */
204 private final Resources resources;
205
206 /**
207 * Constructor.
208 *
209 * @param bundle Resources bundle build outside.
210 */
211 public I18n(final ResourceBundle bundle) {
212 resources = new Resources(bundle);
213 }
214
215 /**
216 * Constructor.
217 *
218 * @param bundleName Path of .property resource.
219 * @param locale The locale for the bundle.
220 */
221 public I18n(final String bundleName, final Locale locale) {
222 resources = new Resources(bundleName, locale);
223 }
224
225 /**
226 * Return message with inlined arguments.
227 *
228 * @param plural value for plural form
229 * @param key message key
230 * @param messageArguments arguments for the message.
231 * @return message with arguments or exclamation message
232 */
233 public String format(final int plural, final String key,
234 final Object... messageArguments) {
235 String keyWithSuffix;
236
237 // the suffix for the value
238 keyWithSuffix = key + "[=" + plural + "]";
239 if (resources.getKeys().contains(keyWithSuffix)) {
240 return resources.format(keyWithSuffix, messageArguments);
241 }
242
243 // find the right standard suffix
244 final Optional<String> suffix = PluralSuffix.getSuffix(plural);
245 if (suffix.isPresent()) {
246 keyWithSuffix = key + "[" + suffix.get() + "]";
247 if (resources.getKeys().contains(keyWithSuffix)) {
248 return resources.format(keyWithSuffix, messageArguments);
249 }
250 }
251
252 // with comparators <, <=, >, >=
253 final List<String> suffixes = resources.getKeys().stream() //
254 .filter(k -> k.startsWith(key + "[") && k.endsWith("]")) //
255 .map(k -> k.substring(key.length() + 1, k.length() - 1)) //
256 .toList();
257 for (final String suf: suffixes) {
258 if (matches(suf, plural)) {
259 keyWithSuffix = key + "[" + suf + "]";
260 return resources.format(keyWithSuffix, messageArguments);
261 }
262 }
263 // if not defined, used default
264 return resources.format(key, messageArguments);
265 }
266
267 /**
268 * Return message with inlined arguments.
269 *
270 * @param key message key
271 * @param messageArguments arguments for the message.
272 * @return message with arguments or exclamation message
273 */
274 public String format(final String key,
275 final Object... messageArguments) {
276 return resources.format(key, messageArguments);
277 }
278
279 /**
280 * Retrieve message from key.
281 *
282 * @param key message key
283 * @return message value or exclamation message
284 */
285 public String get(final String key) {
286 return resources.getString(key);
287 }
288
289 /**
290 * @return Path of .property resource.
291 */
292 public String getBundleName() {
293 return resources.getBundleName();
294 }
295
296 /**
297 * @param locale Locale for the bundle.
298 */
299 public void setLocale(final Locale locale) {
300 resources.setLocale(locale);
301 }
302 }