Merge commit '77d3a60869e7a780c6ae069e51530e1eacece5e2'
[fanfix.git] / src / be / nikiroo / utils / resources / TransBundle.java
1 package be.nikiroo.utils.resources;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.io.Writer;
6 import java.util.LinkedList;
7 import java.util.List;
8 import java.util.Locale;
9 import java.util.regex.Pattern;
10
11 /**
12 * This class manages a translation-dedicated Bundle.
13 * <p>
14 * Two special cases are handled for the used enum:
15 * <ul>
16 * <li>NULL will always will return an empty {@link String}</li>
17 * <li>DUMMY will return "[DUMMY]" (maybe with a suffix and/or "NOUTF")</li>
18 * </ul>
19 *
20 * @param <E>
21 * the enum to use to get values out of this class
22 *
23 * @author niki
24 */
25 public class TransBundle<E extends Enum<E>> extends Bundle<E> {
26 private boolean utf = true;
27 private Locale locale;
28 private boolean defaultLocale = false;
29
30 /**
31 * Create a translation service with the default language.
32 *
33 * @param type
34 * a runtime instance of the class of E
35 * @param name
36 * the name of the {@link Bundles}
37 */
38 public TransBundle(Class<E> type, Enum<?> name) {
39 this(type, name, (Locale) null);
40 }
41
42 /**
43 * Create a translation service for the given language (will fall back to
44 * the default one i not found).
45 *
46 * @param type
47 * a runtime instance of the class of E
48 * @param name
49 * the name of the {@link Bundles}
50 * @param language
51 * the language to use, can be NULL for default
52 */
53 public TransBundle(Class<E> type, Enum<?> name, String language) {
54 super(type, name, null);
55 setLocale(language);
56 }
57
58 /**
59 * Create a translation service for the given language (will fall back to
60 * the default one i not found).
61 *
62 * @param type
63 * a runtime instance of the class of E
64 * @param name
65 * the name of the {@link Bundles}
66 * @param language
67 * the language to use, can be NULL for default
68 */
69 public TransBundle(Class<E> type, Enum<?> name, Locale language) {
70 super(type, name, null);
71 setLocale(language);
72 }
73
74 /**
75 * Translate the given id into user text.
76 *
77 * @param stringId
78 * the ID to translate
79 * @param values
80 * the values to insert instead of the place holders in the
81 * translation
82 *
83 * @return the translated text with the given value where required or NULL
84 * if not found (not present in the resource file)
85 */
86 public String getString(E stringId, Object... values) {
87 return getStringX(stringId, "", values);
88 }
89
90 /**
91 * Translate the given id into user text.
92 *
93 * @param stringId
94 * the ID to translate
95 * @param values
96 * the values to insert instead of the place holders in the
97 * translation
98 *
99 * @return the translated text with the given value where required or NULL
100 * if not found (not present in the resource file)
101 */
102 public String getStringNOUTF(E stringId, Object... values) {
103 return getStringX(stringId, "NOUTF", values);
104 }
105
106 /**
107 * Translate the given id suffixed with the runtime value "_suffix" (that
108 * is, "_" and suffix) into user text.
109 *
110 * @param stringId
111 * the ID to translate
112 * @param values
113 * the values to insert instead of the place holders in the
114 * translation
115 * @param suffix
116 * the runtime suffix
117 *
118 * @return the translated text with the given value where required or NULL
119 * if not found (not present in the resource file)
120 */
121 public String getStringX(E stringId, String suffix, Object... values) {
122 E id = stringId;
123 String result = "";
124
125 String key = id.name()
126 + ((suffix == null || suffix.isEmpty()) ? "" : "_"
127 + suffix.toUpperCase());
128
129 if (!isUnicode()) {
130 if (containsKey(key + "_NOUTF")) {
131 key += "_NOUTF";
132 }
133 }
134
135 if ("NULL".equals(id.name().toUpperCase())) {
136 result = "";
137 } else if ("DUMMY".equals(id.name().toUpperCase())) {
138 result = "[" + key.toLowerCase() + "]";
139 } else if (containsKey(key)) {
140 result = getString(key, null);
141 if (result == null) {
142 result = getMetaDef(id.name());
143 }
144 } else {
145 result = null;
146 }
147
148 if (values != null && values.length > 0 && result != null) {
149 return String.format(locale, result, values);
150 }
151
152 return result;
153 }
154
155 /**
156 * Check if unicode characters should be used.
157 *
158 * @return TRUE to allow unicode
159 */
160 public boolean isUnicode() {
161 return utf;
162 }
163
164 /**
165 * Allow or disallow unicode characters in the program.
166 *
167 * @param utf
168 * TRUE to allow unuciode, FALSE to only allow ASCII characters
169 */
170 public void setUnicode(boolean utf) {
171 this.utf = utf;
172 }
173
174 /**
175 * Return all the languages known by the program for this bundle.
176 *
177 * @return the known language codes
178 */
179 public List<String> getKnownLanguages() {
180 return getKnownLanguages(keyType);
181 }
182
183 /**
184 * The current language (which can be the default one, but NOT NULL).
185 *
186 * @return the language, not NULL
187 */
188 public Locale getLocale() {
189 return locale;
190 }
191
192 /**
193 * The current language (which can be the default one, but NOT NULL).
194 *
195 * @return the language, not NULL, in a display format (fr-BE, en-GB, es,
196 * de...)
197 */
198 public String getLocaleString() {
199 String lang = locale.getLanguage();
200 String country = locale.getCountry();
201 if (country != null && !country.isEmpty()) {
202 return lang + "-" + country;
203 }
204 return lang;
205 }
206
207 /**
208 * Initialise the translation mappings for the given language.
209 *
210 * @param language
211 * the language to initialise, in the form "en-GB" or "fr" for
212 * instance
213 */
214 private void setLocale(String language) {
215 setLocale(getLocaleFor(language));
216 }
217
218 /**
219 * Initialise the translation mappings for the given language.
220 *
221 * @param language
222 * the language to initialise, or NULL for default
223 */
224 private void setLocale(Locale language) {
225 if (language != null) {
226 defaultLocale = false;
227 locale = language;
228 } else {
229 defaultLocale = true;
230 locale = Locale.getDefault();
231 }
232
233 setBundle(keyType, locale, false);
234 }
235
236 @Override
237 public void reload(boolean resetToDefault) {
238 setBundle(keyType, locale, resetToDefault);
239 }
240
241 @Override
242 public String getString(E id) {
243 return getString(id, (Object[]) null);
244 }
245
246 /**
247 * Create/update the .properties files for each supported language and for
248 * the default language.
249 * <p>
250 * Note: this method is <b>NOT</b> thread-safe.
251 *
252 * @param path
253 * the path where the .properties files are
254 *
255 * @throws IOException
256 * in case of IO errors
257 */
258 @Override
259 public void updateFile(String path) throws IOException {
260 String prev = locale.getLanguage();
261 Object status = takeSnapshot();
262
263 // default locale
264 setLocale((Locale) null);
265 if (prev.equals(Locale.getDefault().getLanguage())) {
266 // restore snapshot if default locale = current locale
267 restoreSnapshot(status);
268 }
269 super.updateFile(path);
270
271 for (String lang : getKnownLanguages()) {
272 setLocale(lang);
273 if (lang.equals(prev)) {
274 restoreSnapshot(status);
275 }
276 super.updateFile(path);
277 }
278
279 setLocale(prev);
280 restoreSnapshot(status);
281 }
282
283 @Override
284 protected File getUpdateFile(String path) {
285 String code = locale.toString();
286 File file = null;
287 if (!defaultLocale && code.length() > 0) {
288 file = new File(path, keyType.name() + "_" + code + ".properties");
289 } else {
290 // Default properties file:
291 file = new File(path, keyType.name() + ".properties");
292 }
293
294 return file;
295 }
296
297 @Override
298 protected void writeHeader(Writer writer) throws IOException {
299 String code = locale.toString();
300 String name = locale.getDisplayCountry(locale);
301
302 if (name.length() == 0) {
303 name = locale.getDisplayLanguage(locale);
304 }
305
306 if (name.length() == 0) {
307 name = "default";
308 }
309
310 if (code.length() > 0) {
311 name = name + " (" + code + ")";
312 }
313
314 name = (name + " " + getBundleDisplayName()).trim();
315
316 writer.write("# " + name + " translation file (UTF-8)\n");
317 writer.write("# \n");
318 writer.write("# Note that any key can be doubled with a _NOUTF suffix\n");
319 writer.write("# to use when the NOUTF env variable is set to 1\n");
320 writer.write("# \n");
321 writer.write("# Also, the comments always refer to the key below them.\n");
322 writer.write("# \n");
323 }
324
325 @Override
326 protected void writeValue(Writer writer, E id) throws IOException {
327 super.writeValue(writer, id);
328
329 String name = id.name() + "_NOUTF";
330 if (containsKey(name)) {
331 String value = getString(name, null);
332 if (value == null) {
333 value = getMetaDef(id.name());
334 }
335 boolean set = isSet(id, false);
336 writeValue(writer, name, value, set);
337 }
338 }
339
340 /**
341 * Return the {@link Locale} representing the given language.
342 *
343 * @param language
344 * the language to initialise, in the form "en-GB" or "fr" for
345 * instance
346 *
347 * @return the corresponding {@link Locale} or NULL if it is not known
348 */
349 static private Locale getLocaleFor(String language) {
350 Locale locale;
351
352 if (language == null || language.trim().isEmpty()) {
353 return null;
354 }
355
356 language = language.replaceAll("_", "-");
357 String lang = language;
358 String country = null;
359 if (language.contains("-")) {
360 lang = language.split("-")[0];
361 country = language.split("-")[1];
362 }
363
364 if (country != null)
365 locale = new Locale(lang, country);
366 else
367 locale = new Locale(lang);
368
369 return locale;
370 }
371
372 /**
373 * Return all the languages known by the program.
374 *
375 * @param name
376 * the enumeration on which we translate
377 *
378 * @return the known language codes
379 */
380 static protected List<String> getKnownLanguages(Enum<?> name) {
381 List<String> resources = new LinkedList<String>();
382
383 String regex = ".*" + name.name() + "[_a-zA-Za]*\\.properties$";
384
385 for (String res : TransBundle_ResourceList.getResources(Pattern
386 .compile(regex))) {
387 String resource = res;
388 int index = resource.lastIndexOf('/');
389 if (index >= 0 && index < (resource.length() - 1))
390 resource = resource.substring(index + 1);
391 if (resource.startsWith(name.name())) {
392 resource = resource.substring(0, resource.length()
393 - ".properties".length());
394 resource = resource.substring(name.name().length());
395 if (resource.startsWith("_")) {
396 resource = resource.substring(1);
397 resources.add(resource);
398 }
399 }
400 }
401
402 return resources;
403 }
404 }