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