OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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