Fix FN when empty (with a configurable option) + some i18n
[jvcard.git] / src / be / nikiroo / jvcard / resources / Trans.java
1 package be.nikiroo.jvcard.resources;
2
3 import java.io.BufferedWriter;
4 import java.io.File;
5 import java.io.FileOutputStream;
6 import java.io.IOException;
7 import java.io.OutputStreamWriter;
8 import java.lang.reflect.Field;
9 import java.util.Locale;
10 import java.util.ResourceBundle;
11
12 /**
13 * This class manages the translation of {@link Trans.StringId}s into
14 * user-understandable text.
15 *
16 * @author niki
17 *
18 */
19 public class Trans {
20 private ResourceBundle map;
21 private boolean utf = true;
22 private Locale locale;
23
24 /**
25 * Create a translation service with the default language.
26 */
27 public Trans() {
28 setLanguage(null);
29 }
30
31 /**
32 * Create a translation service for the given language. (Will fall back to
33 * the default one i not found.)
34 *
35 * @param language
36 * the language to use
37 */
38 public Trans(String language) {
39 setLanguage(language);
40 }
41
42 /**
43 * Translate the given {@link StringId} into user text.
44 *
45 * @param stringId
46 * the ID to translate
47 * @param values
48 * the values to insert instead of the place holders in the
49 * translation
50 *
51 * @return the translated text with the given value where required
52 */
53 public String trans(StringId stringId, String... values) {
54 StringId id = stringId;
55 String result = null;
56
57 if (!isUnicode()) {
58 try {
59 id = StringId.valueOf(stringId.name() + "_NOUTF");
60 } catch (IllegalArgumentException iae) {
61 // no special _NOUTF version found
62 }
63 }
64
65 if (id == StringId.NULL) {
66 result = "";
67 } else if (id == StringId.DUMMY) {
68 result = "[dummy]";
69 } else if (map.containsKey(id.name())) {
70 result = map.getString(id.name());
71 } else {
72 result = id.toString();
73 }
74
75 if (values != null && values.length > 0)
76 return String.format(locale, result, (Object[]) values);
77 else
78 return result;
79 }
80
81 /**
82 * Check if unicode characters should be used.
83 *
84 * @return TRUE to allow unicode
85 */
86 public boolean isUnicode() {
87 return utf;
88 }
89
90 /**
91 * Allow or disallow unicode characters in the program.
92 *
93 * @param utf
94 * TRUE to allow unuciode, FALSE to only allow ASCII characters
95 */
96 public void setUnicode(boolean utf) {
97 this.utf = utf;
98 }
99
100 /**
101 * Initialise the translation mappings for the given language.
102 *
103 * @param language
104 * the language to initialise, in the form "en-GB" or "fr" for
105 * instance
106 */
107 private void setLanguage(String language) {
108 locale = getLocaleFor(language);
109 map = Bundles.getBundle("resources", locale);
110 }
111
112 /**
113 * Create/update the translation .properties files. Will use the most likely
114 * candidate as base if the file does not already exists (for instance,
115 * "en_US" will use "en" as a base).
116 *
117 * @param path
118 * the path where the .properties files are
119 *
120 * @param language
121 * the language code to create/update (e.g.: <tt>fr-BE</tt>)
122 *
123 * @throws IOException
124 * in case of IO errors
125 */
126 static public void generateTranslationFile(String path, String language)
127 throws IOException {
128
129 Locale locale = getLocaleFor(language);
130 String code = locale.toString();
131 Trans trans = new Trans(code);
132
133 File file = null;
134 if (code.length() > 0) {
135 file = new File(path + "resources_" + code + ".properties");
136 } else {
137 // Default properties file:
138 file = new File(path + "resources.properties");
139 }
140
141 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
142 new FileOutputStream(file), "UTF-8"));
143
144 String name = locale.getDisplayCountry(locale);
145 if (name.length() == 0)
146 name = locale.getDisplayLanguage(locale);
147 if (name.length() == 0)
148 name = "default";
149
150 if (code.length() > 0) {
151 name = name + " (" + code + ")";
152 }
153
154 writer.append("# " + name + " translation file (UTF-8)\n");
155 writer.append("# \n");
156 writer.append("# Note that any key can be doubled with a _NOUTF suffix\n");
157 writer.append("# to use when the flag --noutf is passed\n");
158 writer.append("# \n");
159 writer.append("# Also, the comments always refer to the key below them.\n");
160 writer.append("# \n");
161 writer.append("\n");
162
163 for (Field field : StringId.class.getDeclaredFields()) {
164 Meta meta = field.getAnnotation(Meta.class);
165 if (meta != null) {
166 StringId id = StringId.valueOf(field.getName());
167 String info = getMetaInfo(meta);
168 if (info != null) {
169 writer.append(info);
170 writer.append("\n");
171 }
172
173 writer.append(id.name());
174 writer.append(" = ");
175 if (!trans.trans(id).equals(id.name()))
176 writer.append(trans.trans(id));
177 writer.append("\n");
178 }
179 }
180
181 writer.close();
182 }
183
184 /**
185 * Return the {@link Locale} representing the given language.
186 *
187 * @param language
188 * the language to initialise, in the form "en-GB" or "fr" for
189 * instance
190 *
191 * @return the corresponding {@link Locale} or the default {@link Locale} if
192 * it is not known
193 */
194 static private Locale getLocaleFor(String language) {
195 Locale locale;
196
197 if (language == null) {
198 locale = Locale.getDefault();
199 } else {
200 language = language.replaceAll("_", "-");
201 String lang = language;
202 String country = null;
203 if (language.contains("-")) {
204 lang = language.split("-")[0];
205 country = language.split("-")[1];
206 }
207
208 if (country != null)
209 locale = new Locale(lang, country);
210 else
211 locale = new Locale(lang);
212 }
213
214 return locale;
215 }
216
217 /**
218 * Return formated, display-able information from the {@link Meta} field
219 * given. Each line will always starts with a "#" character.
220 *
221 * @param meta
222 * the {@link Meta} field
223 *
224 * @return the information to display or NULL if none
225 */
226 static private String getMetaInfo(Meta meta) {
227 String what = meta.what();
228 String where = meta.where();
229 String format = meta.format();
230 String info = meta.info();
231
232 int opt = what.length() + where.length() + format.length();
233 if (opt + info.length() == 0)
234 return null;
235
236 StringBuilder builder = new StringBuilder();
237 builder.append("# ");
238
239 if (opt > 0) {
240 builder.append("(");
241 if (what.length() > 0) {
242 builder.append("WHAT: " + what);
243 if (where.length() + format.length() > 0)
244 builder.append(", ");
245 }
246
247 if (where.length() > 0) {
248 builder.append("WHERE: " + where);
249 if (format.length() > 0)
250 builder.append(", ");
251 }
252
253 if (format.length() > 0) {
254 builder.append("FORMAT: " + format);
255 }
256
257 builder.append(")");
258 if (info.length() > 0) {
259 builder.append("\n# ");
260 }
261 }
262
263 builder.append(info);
264
265 return builder.toString();
266 }
267
268 /**
269 * The enum representing textual information to be translated to the user as
270 * a key.
271 *
272 * Note that each key that should be translated MUST be annotated with a
273 * {@link Meta} annotation.
274 *
275 * @author niki
276 *
277 */
278 public enum StringId {
279 DUMMY, // <-- TODO : remove
280 NULL, // Special usage, no annotations so it is not visible in
281 // .properties files
282 @Meta(what = "a key to press", where = "action keys", format = "MUST BE 3 chars long", info = "Tab key")
283 KEY_TAB, // keys
284 @Meta(what = "a key to press", where = "action keys", format = "MUST BE 3 chars long", info = "Enter key")
285 KEY_ENTER, //
286 @Meta(what = "Action key", where = "All screens except the first (KEY_ACTION_QUIT)", format = "", info = "Go back to previous screen")
287 KEY_ACTION_BACK, //
288 @Meta(what = "Action key", where = "MainWindow", format = "", info = "Get help text")
289 KEY_ACTION_HELP, //
290 @Meta(what = "Action key", where = "FileList", format = "", info = "View the selected card")
291 KEY_ACTION_VIEW_CARD, //
292 @Meta(what = "Action key", where = "ContactList", format = "", info = "View the selected contact")
293 KEY_ACTION_VIEW_CONTACT, //
294 @Meta(what = "Action key", where = "ContactDetails", format = "", info = "Edit the contact")
295 KEY_ACTION_EDIT_CONTACT, //
296 @Meta(what = "Action key", where = "ContactDetails", format = "", info = "Edit the contact in RAW mode")
297 KEY_ACTION_EDIT_CONTACT_RAW, //
298 @Meta(what = "Action key", where = "ContactList", format = "", info = "Save the whole card")
299 KEY_ACTION_SAVE_CARD, //
300 @Meta(what = "", where = "ContactList", format = "", info = "Delete the selected contact")
301 KEY_ACTION_DELETE_CONTACT, //
302 @Meta(what = "Action key", where = "ContactList", format = "", info = "Filter the displayed contacts")
303 KEY_ACTION_SEARCH, //
304 @Meta(what = "", where = "", format = "we could use: ' ', ┃, │...", info = "Field separator")
305 DEAULT_FIELD_SEPARATOR, // MainContentList
306 @Meta(what = "Action key", where = "ContactDetails", format = "", info = "Invert the photo's colours")
307 KEY_ACTION_INVERT, //
308 @Meta(what = "Action key", where = "ContactDetails", format = "", info = "Show the photo in 'fullscreen'")
309 KEY_ACTION_FULLSCREEN, //
310 @Meta(what = "Action key", where = "ContactList, ContactDetails, ContactDetailsRaw", format = "", info = "Switch between the available display formats")
311 KEY_ACTION_SWITCH_FORMAT, // multi-usage
312 @Meta(what = "Action key", where = "Contact list, Edit Contact", format = "", info = "Add a new contact/field")
313 KEY_ACTION_ADD, //
314 @Meta(what = "User question: TEXT", where = "Contact list", format = "", info = "New contact")
315 ASK_USER_CONTACT_NAME, //
316 @Meta(what = "User question: [Y|N]", where = "Contact list", format = "%s = contact name", info = "Delete contact")
317 CONFIRM_USER_DELETE_CONTACT, //
318 @Meta(what = "Error", where = "Contact list", format = "%s = contact name", info = "cannot delete a contact")
319 ERR_CANNOT_DELETE_CONTACT, //
320 };
321 }