OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 156 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
73 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.utils.i18n;
15
 
16
import org.openconcerto.utils.CollectionUtils;
17
import org.openconcerto.utils.Log;
18
import org.openconcerto.utils.PropertiesUtils;
19
import org.openconcerto.utils.Tuple2;
20
 
90 ilm 21
import java.beans.Introspector;
73 ilm 22
import java.io.IOException;
23
import java.util.ArrayList;
156 ilm 24
import java.util.Arrays;
73 ilm 25
import java.util.Collections;
26
import java.util.HashMap;
27
import java.util.LinkedHashMap;
28
import java.util.List;
29
import java.util.Locale;
30
import java.util.Map;
31
import java.util.MissingResourceException;
156 ilm 32
import java.util.Objects;
73 ilm 33
import java.util.Properties;
34
import java.util.regex.Matcher;
35
import java.util.regex.Pattern;
36
 
37
import com.ibm.icu.text.MessageFormat;
38
import com.ibm.icu.text.MessagePattern;
39
import com.ibm.icu.text.MessagePattern.Part;
40
import com.ibm.icu.text.MessagePattern.Part.Type;
41
 
177 ilm 42
import net.jcip.annotations.ThreadSafe;
43
 
73 ilm 44
/**
45
 * Translation manager. The translations are provided by {@link Translator} instances, they are
46
 * created either from a class ending in a language tag that implements it, or by properties files
47
 * that must contain values that will be passed to {@link MessageFormat}. In the latter case,
90 ilm 48
 * messages can reference {@link #createValue(Map, Object[], String) virtual named arguments}.
73 ilm 49
 *
50
 * @author Sylvain
51
 * @see LocalizedInstances
52
 */
177 ilm 53
@ThreadSafe
73 ilm 54
public class TM {
55
 
80 ilm 56
    static public enum MissingMode {
156 ilm 57
        EXCEPTION {
58
            @Override
59
            protected String returnMissing(TM tm, String key) throws MissingResourceException {
60
                throw new MissingResourceException("Missing translation", tm.getBaseName(), key);
61
            }
62
        },
63
        NULL {
64
            @Override
65
            protected String returnMissing(TM tm, String key) {
66
                return null;
67
            }
68
        },
69
        STRING {
70
            @Override
71
            protected String returnMissing(TM tm, String key) {
72
                return '!' + key + '!';
73
            }
74
        };
75
 
76
        protected abstract String returnMissing(final TM tm, final String key) throws MissingResourceException;
77
 
78
        // method to avoid array allocation and Arrays.toString()
79
        protected final String returnResult(final TM tm, final String res, final String key) throws MissingResourceException {
80
            return res == null ? this.returnMissing(tm, key) : res;
81
        }
82
 
83
        protected final String returnResult(final TM tm, final String res, final String... keys) throws MissingResourceException {
84
            return res == null ? this.returnMissing(tm, Arrays.toString(keys)) : res;
85
        }
80 ilm 86
    }
87
 
90 ilm 88
    static public final String NOUN_CLASS_PROP = "nounClass";
89
    static {
90
        assert NOUN_CLASS_PROP.equals(Introspector.decapitalize(NounClass.class.getSimpleName()));
91
    }
92
 
80 ilm 93
    static private final MissingMode DEFAULT_MISSING_MODE = MissingMode.STRING;
94
 
177 ilm 95
    private static final TMPool<TM> POOL = new TMPool<TM>(TM::new);
73 ilm 96
    static private final Pattern splitPtrn = Pattern.compile("__", Pattern.LITERAL);
97
    static private boolean USE_DYNAMIC_MAP = true;
98
 
177 ilm 99
    public static synchronized void setUseDynamicMap(boolean b) {
73 ilm 100
        USE_DYNAMIC_MAP = b;
101
    }
102
 
103
    /**
104
     * Whether to use a {@link DynamicMap} or add all possible keys up front.
105
     * <code>DynamicMap</code> is the default since it's faster : only required keys are computed,
106
     * otherwise every key that may be used must be computed. So if your pattern has a lot of
107
     * plurals and choices it might make a difference. However <code>DynamicMap</code> is less
108
     * robust, since it twists a little the definition of {@link Map}.
109
     *
110
     * @return <code>true</code> if <code>DynamicMap</code> should be used.
111
     */
177 ilm 112
    public static synchronized boolean useDynamicMap() {
73 ilm 113
        return USE_DYNAMIC_MAP;
114
    }
115
 
177 ilm 116
    /**
117
     * The default locale for {@link #getInstance()}. Currently just {@link Locale#getDefault()}.
118
     *
119
     * @return the default locale.
120
     */
121
    public static final Locale getDefaultLocale() {
122
        return Locale.getDefault();
123
    }
124
 
73 ilm 125
    static public TM getInstance() {
177 ilm 126
        return getInstance(getDefaultLocale());
73 ilm 127
    }
128
 
177 ilm 129
    static public TM getInstance(final Locale l) {
130
        return POOL.get(l);
131
    }
132
 
133
    static public String tr(final Locale l, final String key, final Object... args) {
134
        return getInstance(l).translate(key, args);
135
    }
136
 
73 ilm 137
    static public String tr(final String key, final Object... args) {
138
        return getInstance().translate(key, args);
139
    }
140
 
141
    private Locale locale;
142
    private TranslatorChain translations;
143
    private Locale translationsLocale;
144
 
177 ilm 145
    protected TM(final Locale locale) {
146
        setLocale(locale);
73 ilm 147
    }
148
 
177 ilm 149
    private final void setLocale(final Locale locale) {
73 ilm 150
        final LocalizedInstances<Translator> localizedInstances = new LocalizedInstances<Translator>(Translator.class, TranslationManager.getControl()) {
151
            @Override
152
            protected Translator createInstance(final String bundleName, final Locale l, final Class<?> cl) throws IOException {
153
                final Properties props = PropertiesUtils.createFromResource(cl, '/' + this.getControl().toResourceName(bundleName, "properties"));
154
                if (props == null) {
155
                    return null;
156
                } else {
157
                    return new Translator() {
158
                        @Override
159
                        public String translate(final String key, final MessageArgs args) {
160
                            final String msg = props.getProperty(key);
161
                            if (msg == null)
162
                                return null;
163
                            // replaceMap() handles virtual keys (e.g.
164
                            // element__pluralDefiniteArticle)
165
                            return new MessageFormat(msg, l).format(replaceMap(args, msg).getAll());
166
                        }
167
                    };
168
                }
169
            };
170
        };
171
        final Tuple2<Locale, List<Translator>> createInstances = localizedInstances.createInstances(getBaseName(), locale);
177 ilm 172
        synchronized (this) {
173
            this.locale = locale;
174
            this.translationsLocale = createInstances.get0();
175
            this.translations = new TranslatorChain(createInstances.get1());
176
        }
73 ilm 177
    }
178
 
179
    /**
180
     * The requested locale.
181
     *
182
     * @return the requested locale.
183
     * @see #setLocale(Locale)
184
     */
177 ilm 185
    public final synchronized Locale getLocale() {
73 ilm 186
        return this.locale;
187
    }
188
 
189
    /**
190
     * The actual locale of the loaded translations.
191
     *
192
     * @return the actual locale.
193
     */
177 ilm 194
    public final synchronized Locale getTranslationsLocale() {
73 ilm 195
        return this.translationsLocale;
196
    }
197
 
177 ilm 198
    private final synchronized TranslatorChain getTranslations() {
199
        return this.translations;
200
    }
201
 
80 ilm 202
    protected String getBaseName() {
73 ilm 203
        return I18nUtils.getBaseName(this.getClass());
204
    }
205
 
206
    // translate array
207
    public final String trA(final String key, final Object... args) {
208
        return translate(key, args);
209
    }
210
 
211
    public final String translate(final String key, final Object... args) {
80 ilm 212
        return translate(DEFAULT_MISSING_MODE, key, args);
73 ilm 213
    }
214
 
215
    // translate map
216
    public final String trM(final String key, final String name1, final Object arg1) {
217
        return trM(key, Collections.singletonMap(name1, arg1));
218
    }
219
 
220
    public final String trM(final String key, final String name1, final Object arg1, final String name2, final Object arg2) {
221
        return trM(key, CollectionUtils.createMap(name1, arg1, name2, arg2));
222
    }
223
 
224
    public final String trM(final String key, final Map<String, ?> args) {
80 ilm 225
        return trM(DEFAULT_MISSING_MODE, key, args);
73 ilm 226
    }
227
 
80 ilm 228
    public final String trM(final MissingMode mode, final String key, Map<String, ?> map) throws MissingResourceException {
229
        return translate(mode, key, new MessageArgs(map));
73 ilm 230
    }
231
 
80 ilm 232
    public final String translate(final MissingMode mode, final String key, final Object... args) throws MissingResourceException {
156 ilm 233
        return translate(mode, key, args.length == 0 ? MessageArgs.getEmpty() : new MessageArgs(args));
73 ilm 234
    }
235
 
156 ilm 236
    public final String translateFirst(final MissingMode mode, final String... keys) throws MissingResourceException {
237
        return translateFirst(mode, MessageArgs.getEmpty(), keys);
238
    }
239
 
240
    /**
241
     * Return the first non-<code>null</code> result.
242
     *
243
     * @param mode what to do if all keys are <code>null</code>.
244
     * @param args the arguments.
245
     * @param keys the keys to search for.
246
     * @return the first non-<code>null</code> result.
247
     * @throws MissingResourceException if {@link MissingMode#EXCEPTION} and all keys are
248
     *         <code>null</code>.
249
     */
250
    public final String translateFirst(final MissingMode mode, final MessageArgs args, final String... keys) throws MissingResourceException {
251
        String res = null;
252
        for (int i = 0; i < keys.length && res == null; i++) {
253
            final String key = keys[i];
254
            if (key != null)
255
                res = this.translate(MissingMode.NULL, key, args);
256
        }
257
        return mode.returnResult(this, res, keys);
258
    }
259
 
80 ilm 260
    private final String translate(final MissingMode mode, final String key, MessageArgs args) throws MissingResourceException {
156 ilm 261
        Objects.requireNonNull(mode, "Null mode");
262
        Objects.requireNonNull(key, "Null key");
263
        Objects.requireNonNull(args, "Null arguments");
177 ilm 264
        final String res = this.getTranslations().translate(key, args);
156 ilm 265
        return mode.returnResult(this, res, key);
73 ilm 266
    }
267
 
268
    protected MessageArgs replaceMap(final MessageArgs args, final String msg) {
269
        final MessageArgs res;
80 ilm 270
        if (args.isMapPrimary()) {
73 ilm 271
            final Map<String, ?> map = args.getMap();
80 ilm 272
            final Map<String, Object> copy;
73 ilm 273
            if (MessageArgs.isOrdered(map)) {
80 ilm 274
                copy = new LinkedHashMap<String, Object>(map);
73 ilm 275
            } else {
80 ilm 276
                copy = new HashMap<String, Object>(map);
73 ilm 277
            }
80 ilm 278
            final Object[] array = args.getArray();
279
            final Map<String, Object> newMap;
73 ilm 280
            if (useDynamicMap()) {
80 ilm 281
                newMap = new DynamicMap<Object>(copy) {
73 ilm 282
                    @Override
283
                    protected Object createValueNonNull(String key) {
80 ilm 284
                        return TM.this.createValue(this, array, key);
73 ilm 285
                    }
286
                };
287
            } else {
80 ilm 288
                newMap = copy;
73 ilm 289
                final MessagePattern messagePattern = new MessagePattern(msg);
290
                if (messagePattern.hasNamedArguments()) {
291
                    final int countParts = messagePattern.countParts();
292
                    String argName;
293
                    for (int i = 0; i < countParts; i++) {
294
                        final Part part = messagePattern.getPart(i);
295
                        if (part.getType() == Type.ARG_NAME && !newMap.containsKey(argName = messagePattern.getSubstring(part))) {
80 ilm 296
                            final Object createValue = this.createValue(newMap, array, argName);
73 ilm 297
                            if (createValue != null)
298
                                newMap.put(argName, createValue);
299
                        }
300
                    }
301
                }
302
            }
303
            res = new MessageArgs(newMap);
304
        } else {
305
            res = args;
306
        }
307
        return res;
308
    }
309
 
310
    /**
311
     * Try to create a value for a missing key. The syntax of keys must be phraseName(__name)+ and
312
     * if you need to have __ in a name it must be doubled (i.e. ____). <code>phraseName</code>, as
90 ilm 313
     * its name implies, must reference an existing phrase in <code>map</code>. If this phrase is
314
     * suffixed by {@value #NOUN_CLASS_PROP} then the {@link NounClass#getName() name} of the noun
315
     * class of the phrase is returned. Else this phrase and the list of <code>name</code> are
316
     * passed to {@link Grammar#eval(Phrase, Number, List)}. The count is
317
     * <code>phraseNameCount</code> if it exists and is a {@link Number}, then <code>count</code>
318
     * else <code>null</code>.
73 ilm 319
     *
320
     * @param map the current map.
80 ilm 321
     * @param objects the original map as an array.
73 ilm 322
     * @param key the missing key.
323
     * @return its value, or <code>null</code> to leave the map unmodified.
324
     */
80 ilm 325
    protected Object createValue(final Map<String, Object> map, final Object[] objects, final String key) {
326
        if (key.length() == 1 && Character.isDigit(key.charAt(0))) {
327
            final int index = Integer.parseInt(key);
328
            if (index < objects.length) {
329
                return objects[index];
330
            } else {
331
                Log.get().warning("Only " + objects.length + " arguments : " + index);
332
                return null;
333
            }
334
        }
335
 
73 ilm 336
        final Matcher m = splitPtrn.matcher(key);
337
        final String pattern = splitPtrn.pattern();
338
        final int patternL = pattern.length();
339
        final StringBuffer sb = new StringBuffer(key.length());
340
        final List<String> l = new ArrayList<String>();
341
        int pos = 0;
342
        while (m.find(pos)) {
343
            // double to escape pattern
344
            if (key.length() >= m.end() + patternL && pattern.equals(key.substring(m.end(), m.end() + patternL))) {
345
                // go to the end to include one
346
                sb.append(key.substring(pos, m.end()));
347
                // and set pos after the second one
348
                pos = m.end() + patternL;
349
            } else {
350
                sb.append(key.substring(pos, m.start()));
351
                l.add(sb.toString());
352
                sb.setLength(0);
353
                pos = m.end();
354
            }
355
        }
356
        sb.append(key.substring(pos));
357
        l.add(sb.toString());
358
 
359
        final String first = CollectionUtils.getFirst(l);
360
        // at least the whole key
361
        assert first != null;
362
        final Object firstObj = handleGet(map, first);
363
        final Phrase phrase = firstObj instanceof Phrase ? (Phrase) firstObj : null;
90 ilm 364
        if (phrase != null && l.size() == 2 && NOUN_CLASS_PROP.equals(l.get(1))) {
365
            if (phrase.getNounClass() == null) {
366
                Log.get().warning("No noun class for " + phrase);
367
                return phrase.getBase();
368
            } else {
369
                return phrase.getNounClass().getName();
370
            }
371
        } else if (phrase != null && phrase.getGrammar() != null) {
73 ilm 372
            Object countObj = handleGet(map, first + "Count");
373
            if (!(countObj instanceof Number))
374
                countObj = handleGet(map, "count");
375
            final Number count = countObj instanceof Number ? (Number) countObj : null;
376
            return phrase.getGrammar().eval(phrase, count, l.subList(1, l.size()));
377
        } else if (phrase != null) {
378
            Log.get().warning("While splitting " + key + ", " + first + " is a Phrase without grammar : " + phrase);
379
            return phrase.getBase();
380
        } else {
381
            Log.get().warning("While splitting " + key + " : " + first + " isn't a Phrase");
382
            return null;
383
        }
384
    }
385
 
386
    private final Object handleGet(final Map<String, Object> map, final String key) {
387
        if (map instanceof DynamicMap)
388
            return ((DynamicMap<Object>) map).handleGet(key);
389
        else
390
            return map.get(key);
391
    }
392
}