1 package be
.nikiroo
.utils
.resources
;
3 import java
.io
.BufferedWriter
;
5 import java
.io
.FileInputStream
;
6 import java
.io
.FileOutputStream
;
7 import java
.io
.IOException
;
8 import java
.io
.InputStreamReader
;
9 import java
.io
.OutputStreamWriter
;
10 import java
.io
.Reader
;
11 import java
.io
.Writer
;
12 import java
.lang
.reflect
.Field
;
13 import java
.util
.ArrayList
;
14 import java
.util
.HashMap
;
15 import java
.util
.List
;
16 import java
.util
.Locale
;
18 import java
.util
.MissingResourceException
;
19 import java
.util
.PropertyResourceBundle
;
20 import java
.util
.ResourceBundle
;
22 import be
.nikiroo
.utils
.resources
.Meta
.Format
;
25 * This class encapsulate a {@link ResourceBundle} in UTF-8. It allows to
26 * retrieve values associated to an enumeration, and allows some additional
29 * It also sports a writable change map, and you can save back the
30 * {@link Bundle} to file with {@link Bundle#updateFile(String)}.
33 * the enum to use to get values out of this class
38 public class Bundle
<E
extends Enum
<E
>> {
40 protected Class
<E
> type
;
42 * The {@link Enum} associated to this {@link Bundle} (all the keys used in
43 * this {@link Bundle} will be of this type).
45 protected Enum
<?
> keyType
;
47 private TransBundle
<E
> descriptionBundle
;
50 private Map
<String
, String
> map
;
52 private Map
<String
, String
> changeMap
;
55 * Create a new {@link Bundles} of the given name.
58 * a runtime instance of the class of E
60 * the name of the {@link Bundles}
61 * @param descriptionBundle
62 * the description {@link TransBundle}, that is, a
63 * {@link TransBundle} dedicated to the description of the values
64 * of the given {@link Bundle} (can be NULL)
66 protected Bundle(Class
<E
> type
, Enum
<?
> name
,
67 TransBundle
<E
> descriptionBundle
) {
70 this.descriptionBundle
= descriptionBundle
;
72 this.map
= new HashMap
<String
, String
>();
73 this.changeMap
= new HashMap
<String
, String
>();
74 setBundle(name
, Locale
.getDefault(), false);
78 * Check if the setting is set into this {@link Bundle}.
81 * the id of the setting to check
82 * @param includeDefaultValue
83 * TRUE to only return false when the setting is not set AND
84 * there is no default value
86 * @return TRUE if the setting is set
88 public boolean isSet(E id
, boolean includeDefaultValue
) {
89 return isSet(id
.name(), includeDefaultValue
);
93 * Check if the setting is set into this {@link Bundle}.
96 * the id of the setting to check
97 * @param includeDefaultValue
98 * TRUE to only return false when the setting is not set AND
99 * there is no default value
101 * @return TRUE if the setting is set
103 protected boolean isSet(String name
, boolean includeDefaultValue
) {
104 if (getString(name
, null) == null) {
105 if (!includeDefaultValue
|| getString(name
, "") == null) {
114 * Return the value associated to the given id as a {@link String}.
117 * the id of the value to get
119 * @return the associated value, or NULL if not found (not present in the
122 public String
getString(E id
) {
123 return getString(id
, null);
127 * Return the value associated to the given id as a {@link String}.
129 * If no value is associated, take the default one if any.
132 * the id of the value to get
134 * the default value when it is not present in the config file
136 * @return the associated value, or NULL if not found (not present in the
139 public String
getString(E id
, String def
) {
140 return getString(id
, def
, -1);
144 * Return the value associated to the given id as a {@link String}.
146 * If no value is associated, take the default one if any.
149 * the id of the value to get
151 * the default value when it is not present in the config file
153 * the item number to get for an array of values, or -1 for
156 * @return the associated value, or NULL if not found (not present in the
159 public String
getString(E id
, String def
, int item
) {
160 String rep
= getString(id
.name(), null);
163 Meta meta
= type
.getDeclaredField(id
.name()).getAnnotation(
166 } catch (NoSuchFieldException e
) {
167 } catch (SecurityException e
) {
171 //TODO: is it ok? need to jDoc?
172 if (rep
== null || rep
.isEmpty()) {
177 List
<String
> values
= BundleHelper
.parseList(rep
, item
);
178 if (values
!= null && item
< values
.size()) {
179 return values
.get(item
);
189 * Set the value associated to the given id as a {@link String}.
192 * the id of the value to set
197 public void setString(E id
, String value
) {
198 setString(id
.name(), value
);
202 * Set the value associated to the given id as a {@link String}.
205 * the id of the value to set
209 * the item number to get for an array of values, or -1 for
213 public void setString(E id
, String value
, int item
) {
215 setString(id
.name(), value
);
217 List
<String
> values
= getList(id
);
218 for (int i
= values
.size(); i
< item
; i
++) {
221 values
.set(item
, value
);
222 setString(id
.name(), BundleHelper
.fromList(values
));
227 * Return the value associated to the given id as a {@link String} suffixed
228 * with the runtime value "_suffix" (that is, "_" and suffix).
230 * Will only accept suffixes that form an existing id.
232 * If no value is associated, take the default one if any.
235 * the id of the value to get
239 * @return the associated value, or NULL if not found (not present in the
242 public String
getStringX(E id
, String suffix
) {
243 return getStringX(id
, suffix
, null, -1);
247 * Return the value associated to the given id as a {@link String} suffixed
248 * with the runtime value "_suffix" (that is, "_" and suffix).
250 * Will only accept suffixes that form an existing id.
252 * If no value is associated, take the default one if any.
255 * the id of the value to get
259 * the default value when it is not present in the config file
261 * @return the associated value, or NULL if not found (not present in the
264 public String
getStringX(E id
, String suffix
, String def
) {
265 return getStringX(id
, suffix
, def
, -1);
269 * Return the value associated to the given id as a {@link String} suffixed
270 * with the runtime value "_suffix" (that is, "_" and suffix).
272 * Will only accept suffixes that form an existing id.
274 * If no value is associated, take the default one if any.
277 * the id of the value to get
281 * the item number to get for an array of values, or -1 for
284 * the default value when it is not present in the config file
286 * the item number to get for an array of values, or -1 for
289 * @return the associated value, or NULL if not found (not present in the
292 public String
getStringX(E id
, String suffix
, String def
, int item
) {
293 String key
= id
.name()
294 + (suffix
== null ?
"" : "_" + suffix
.toUpperCase());
297 id
= Enum
.valueOf(type
, key
);
298 return getString(id
, def
, item
);
299 } catch (IllegalArgumentException e
) {
306 * Set the value associated to the given id as a {@link String} suffixed
307 * with the runtime value "_suffix" (that is, "_" and suffix).
309 * Will only accept suffixes that form an existing id.
312 * the id of the value to set
318 public void setStringX(E id
, String suffix
, String value
) {
319 setStringX(id
, suffix
, value
, -1);
323 * Set the value associated to the given id as a {@link String} suffixed
324 * with the runtime value "_suffix" (that is, "_" and suffix).
326 * Will only accept suffixes that form an existing id.
329 * the id of the value to set
335 * the item number to get for an array of values, or -1 for
338 public void setStringX(E id
, String suffix
, String value
, int item
) {
339 String key
= id
.name()
340 + (suffix
== null ?
"" : "_" + suffix
.toUpperCase());
343 id
= Enum
.valueOf(type
, key
);
344 setString(id
, value
, item
);
345 } catch (IllegalArgumentException e
) {
350 * Return the value associated to the given id as a {@link Boolean}.
352 * If no value is associated, take the default one if any.
355 * the id of the value to get
357 * @return the associated value
359 public Boolean
getBoolean(E id
) {
360 return BundleHelper
.parseBoolean(getString(id
), -1);
364 * Return the value associated to the given id as a {@link Boolean}.
366 * If no value is associated, take the default one if any.
369 * the id of the value to get
371 * the default value when it is not present in the config file or
372 * if it is not a boolean value
374 * @return the associated value
376 public boolean getBoolean(E id
, boolean def
) {
377 Boolean value
= getBoolean(id
);
386 * Return the value associated to the given id as a {@link Boolean}.
388 * If no value is associated, take the default one if any.
391 * the id of the value to get
393 * the default value when it is not present in the config file or
394 * if it is not a boolean value
396 * the item number to get for an array of values, or -1 for
399 * @return the associated value
401 public Boolean
getBoolean(E id
, boolean def
, int item
) {
402 String value
= getString(id
);
404 return BundleHelper
.parseBoolean(value
, item
);
411 * Set the value associated to the given id as a {@link Boolean}.
414 * the id of the value to set
419 public void setBoolean(E id
, boolean value
) {
420 setBoolean(id
, value
, -1);
424 * Set the value associated to the given id as a {@link Boolean}.
427 * the id of the value to set
431 * the item number to get for an array of values, or -1 for
435 public void setBoolean(E id
, boolean value
, int item
) {
436 setString(id
, BundleHelper
.fromBoolean(value
), item
);
440 * Return the value associated to the given id as an {@link Integer}.
442 * If no value is associated, take the default one if any.
445 * the id of the value to get
447 * @return the associated value
449 public Integer
getInteger(E id
) {
450 String value
= getString(id
);
452 return BundleHelper
.parseInteger(value
, -1);
459 * Return the value associated to the given id as an int.
461 * If no value is associated, take the default one if any.
464 * the id of the value to get
466 * the default value when it is not present in the config file or
467 * if it is not a int value
469 * @return the associated value
471 public int getInteger(E id
, int def
) {
472 Integer value
= getInteger(id
);
481 * Return the value associated to the given id as an int.
483 * If no value is associated, take the default one if any.
486 * the id of the value to get
488 * the default value when it is not present in the config file or
489 * if it is not a int value
491 * the item number to get for an array of values, or -1 for
494 * @return the associated value
496 public Integer
getInteger(E id
, int def
, int item
) {
497 String value
= getString(id
);
499 return BundleHelper
.parseInteger(value
, item
);
506 * Set the value associated to the given id as a {@link Integer}.
509 * the id of the value to set
514 public void setInteger(E id
, int value
) {
515 setInteger(id
, value
, -1);
519 * Set the value associated to the given id as a {@link Integer}.
522 * the id of the value to set
526 * the item number to get for an array of values, or -1 for
530 public void setInteger(E id
, int value
, int item
) {
531 setString(id
, BundleHelper
.fromInteger(value
), item
);
535 * Return the value associated to the given id as a {@link Character}.
537 * If no value is associated, take the default one if any.
540 * the id of the value to get
542 * @return the associated value
544 public Character
getCharacter(E id
) {
545 return BundleHelper
.parseCharacter(getString(id
), -1);
549 * Return the value associated to the given id as a {@link Character}.
551 * If no value is associated, take the default one if any.
554 * the id of the value to get
556 * the default value when it is not present in the config file or
557 * if it is not a char value
559 * @return the associated value
561 public char getCharacter(E id
, char def
) {
562 Character value
= getCharacter(id
);
571 * Return the value associated to the given id as a {@link Character}.
573 * If no value is associated, take the default one if any.
576 * the id of the value to get
578 * the default value when it is not present in the config file or
579 * if it is not a char value
581 * @return the associated value
583 public Character
getCharacter(E id
, char def
, int item
) {
584 String value
= getString(id
);
586 return BundleHelper
.parseCharacter(value
, item
);
593 * Set the value associated to the given id as a {@link Character}.
596 * the id of the value to set
601 public void setCharacter(E id
, char value
) {
602 setCharacter(id
, value
, -1);
606 * Set the value associated to the given id as a {@link Character}.
609 * the id of the value to set
613 * the item number to get for an array of values, or -1 for
617 public void setCharacter(E id
, char value
, int item
) {
618 setString(id
, BundleHelper
.fromCharacter(value
), item
);
622 * Return the value associated to the given id as a colour if it is found
625 * The returned value is an ARGB value.
627 * If no value is associated, take the default one if any.
630 * the id of the value to get
632 * @return the associated value
634 public Integer
getColor(E id
) {
635 return BundleHelper
.parseColor(getString(id
), -1);
639 * Return the value associated to the given id as a colour if it is found
642 * The returned value is an ARGB value.
644 * If no value is associated, take the default one if any.
647 * the id of the value to get
649 * @return the associated value
651 public int getColor(E id
, int def
) {
652 Integer value
= getColor(id
);
661 * Return the value associated to the given id as a colour if it is found
664 * The returned value is an ARGB value.
666 * If no value is associated, take the default one if any.
669 * the id of the value to get
671 * @return the associated value
673 public Integer
getColor(E id
, int def
, int item
) {
674 String value
= getString(id
);
676 return BundleHelper
.parseColor(value
, item
);
683 * Set the value associated to the given id as a colour.
685 * The value is a BGRA value.
688 * the id of the value to set
692 public void setColor(E id
, Integer color
) {
693 setColor(id
, color
, -1);
697 * Set the value associated to the given id as a Color.
700 * the id of the value to set
704 * the item number to get for an array of values, or -1 for
708 public void setColor(E id
, int value
, int item
) {
709 setString(id
, BundleHelper
.fromColor(value
), item
);
713 * Return the value associated to the given id as a list of values if it is
714 * found and can be parsed.
716 * If no value is associated, take the default one if any.
719 * the id of the value to get
721 * @return the associated list, empty if the value is empty, NULL if it is
722 * not found or cannot be parsed as a list
724 public List
<String
> getList(E id
) {
725 return BundleHelper
.parseList(getString(id
), -1);
729 * Return the value associated to the given id as a list of values if it is
730 * found and can be parsed.
732 * If no value is associated, take the default one if any.
735 * the id of the value to get
737 * @return the associated list, empty if the value is empty, NULL if it is
738 * not found or cannot be parsed as a list
740 public List
<String
> getList(E id
, List
<String
> def
) {
741 List
<String
> value
= getList(id
);
750 * Return the value associated to the given id as a list of values if it is
751 * found and can be parsed.
753 * If no value is associated, take the default one if any.
756 * the id of the value to get
758 * @return the associated list, empty if the value is empty, NULL if it is
759 * not found or cannot be parsed as a list
761 public List
<String
> getList(E id
, List
<String
> def
, int item
) {
762 String value
= getString(id
);
764 return BundleHelper
.parseList(value
, item
);
771 * Set the value associated to the given id as a list of values.
774 * the id of the value to set
776 * the new list of values
778 public void setList(E id
, List
<String
> list
) {
779 setList(id
, list
, -1);
783 * Set the value associated to the given id as a {@link List}.
786 * the id of the value to set
790 * the item number to get for an array of values, or -1 for
794 public void setList(E id
, List
<String
> value
, int item
) {
795 setString(id
, BundleHelper
.fromList(value
), item
);
799 * Create/update the .properties file.
801 * Will use the most likely candidate as base if the file does not already
802 * exists and this resource is translatable (for instance, "en_US" will use
803 * "en" as a base if the resource is a translation file).
805 * Will update the files in {@link Bundles#getDirectory()}; it <b>MUST</b>
808 * @throws IOException
809 * in case of IO errors
811 public void updateFile() throws IOException
{
812 updateFile(Bundles
.getDirectory());
816 * Create/update the .properties file.
818 * Will use the most likely candidate as base if the file does not already
819 * exists and this resource is translatable (for instance, "en_US" will use
820 * "en" as a base if the resource is a translation file).
823 * the path where the .properties files are, <b>MUST NOT</b> be
826 * @throws IOException
827 * in case of IO errors
829 public void updateFile(String path
) throws IOException
{
830 File file
= getUpdateFile(path
);
832 BufferedWriter writer
= new BufferedWriter(new OutputStreamWriter(
833 new FileOutputStream(file
), "UTF-8"));
839 for (Field field
: type
.getDeclaredFields()) {
840 Meta meta
= field
.getAnnotation(Meta
.class);
842 E id
= Enum
.valueOf(type
, field
.getName());
843 String info
= getMetaInfo(meta
);
850 writeValue(writer
, id
);
858 * Delete the .properties file.
860 * Will use the most likely candidate as base if the file does not already
861 * exists and this resource is translatable (for instance, "en_US" will use
862 * "en" as a base if the resource is a translation file).
864 * Will delete the files in {@link Bundles#getDirectory()}; it <b>MUST</b>
867 * @return TRUE if the file was deleted
869 public boolean deleteFile() {
870 return deleteFile(Bundles
.getDirectory());
874 * Delete the .properties file.
876 * Will use the most likely candidate as base if the file does not already
877 * exists and this resource is translatable (for instance, "en_US" will use
878 * "en" as a base if the resource is a translation file).
881 * the path where the .properties files are, <b>MUST NOT</b> be
884 * @return TRUE if the file was deleted
886 public boolean deleteFile(String path
) {
887 File file
= getUpdateFile(path
);
888 return file
.delete();
892 * The description {@link TransBundle}, that is, a {@link TransBundle}
893 * dedicated to the description of the values of the given {@link Bundle}
896 * @return the description {@link TransBundle}
898 public TransBundle
<E
> getDescriptionBundle() {
899 return descriptionBundle
;
903 * Reload the {@link Bundle} data files.
905 * @param resetToDefault
906 * reset to the default configuration (do not look into the
907 * possible user configuration files, only take the original
910 public void reload(boolean resetToDefault
) {
911 setBundle(keyType
, Locale
.getDefault(), resetToDefault
);
915 * Check if the internal map contains the given key.
918 * the key to check for
920 * @return true if it does
922 protected boolean containsKey(String key
) {
923 return changeMap
.containsKey(key
) || map
.containsKey(key
);
927 * Get the value for the given key if it exists in the internal map, or
928 * <tt>def</tt> if not.
931 * the key to check for
933 * the default value when it is not present in the internal map
935 * @return the value, or <tt>def</tt> if not found
937 protected String
getString(String key
, String def
) {
938 if (changeMap
.containsKey(key
)) {
939 return changeMap
.get(key
);
942 if (map
.containsKey(key
)) {
950 * Set the value for this key, in the change map (it is kept in memory, not
956 * the associated value
958 protected void setString(String key
, String value
) {
959 changeMap
.put(key
, value
== null ?
null : value
.trim());
963 * Return formated, display-able information from the {@link Meta} field
964 * given. Each line will always starts with a "#" character.
967 * the {@link Meta} field
969 * @return the information to display or NULL if none
971 protected String
getMetaInfo(Meta meta
) {
972 String desc
= meta
.description();
973 boolean group
= meta
.group();
974 Meta
.Format format
= meta
.format();
975 String
[] list
= meta
.list();
976 boolean nullable
= meta
.nullable();
977 String def
= meta
.def();
978 boolean array
= meta
.array();
980 // Default, empty values -> NULL
981 if (desc
.length() + list
.length
+ def
.length() == 0 && !group
982 && nullable
&& format
== Format
.STRING
) {
986 StringBuilder builder
= new StringBuilder();
987 for (String line
: desc
.split("\n")) {
988 builder
.append("# ").append(line
).append("\n");
992 builder
.append("# This item is used as a group, its content is not expected to be used.");
994 builder
.append("# (FORMAT: ").append(format
)
995 .append(nullable ?
"" : ", required");
996 builder
.append(") ");
998 if (list
.length
> 0) {
999 builder
.append("\n# ALLOWED VALUES: ");
1000 boolean first
= true;
1001 for (String value
: list
) {
1003 builder
.append(", ");
1005 builder
.append(BundleHelper
.escape(value
));
1011 builder
.append("\n# (This item accepts a list of ^escaped comma-separated values)");
1015 return builder
.toString();
1019 * The display name used in the <tt>.properties file</tt>.
1023 protected String
getBundleDisplayName() {
1024 return keyType
.toString();
1028 * Write the header found in the configuration <tt>.properties</tt> file of
1029 * this {@link Bundles}.
1032 * the {@link Writer} to write the header in
1034 * @throws IOException
1035 * in case of IO error
1037 protected void writeHeader(Writer writer
) throws IOException
{
1038 writer
.write("# " + getBundleDisplayName() + "\n");
1039 writer
.write("#\n");
1043 * Write the given data to the config file, i.e., "MY_ID = my_curent_value"
1044 * followed by a new line.
1046 * Will prepend a # sign if the is is not set (see
1047 * {@link Bundle#isSet(Enum, boolean)}).
1050 * the {@link Writer} to write into
1054 * @throws IOException
1055 * in case of IO error
1057 protected void writeValue(Writer writer
, E id
) throws IOException
{
1058 boolean set
= isSet(id
, false);
1059 writeValue(writer
, id
.name(), getString(id
), set
);
1063 * Write the given data to the config file, i.e., "MY_ID = my_curent_value"
1064 * followed by a new line.
1066 * Will prepend a # sign if the is is not set.
1069 * the {@link Writer} to write into
1075 * the value is set in this {@link Bundle}
1077 * @throws IOException
1078 * in case of IO error
1080 protected void writeValue(Writer writer
, String id
, String value
,
1081 boolean set
) throws IOException
{
1088 writer
.write(" = ");
1090 if (value
== null) {
1094 String
[] lines
= value
.replaceAll("\t", "\\\\\\t").split("\n");
1095 for (int i
= 0; i
< lines
.length
; i
++) {
1096 writer
.write(lines
[i
]);
1097 if (i
< lines
.length
- 1) {
1098 writer
.write("\\n\\");
1105 * Return the source file for this {@link Bundles} from the given path.
1108 * the path where the .properties files are
1110 * @return the source {@link File}
1112 protected File
getUpdateFile(String path
) {
1113 return new File(path
, keyType
.name() + ".properties");
1117 * Change the currently used bundle, and reset all changes.
1120 * the name of the bundle to load
1122 * the {@link Locale} to use
1123 * @param resetToDefault
1124 * reset to the default configuration (do not look into the
1125 * possible user configuration files, only take the original
1128 protected void setBundle(Enum
<?
> name
, Locale locale
, boolean resetToDefault
) {
1130 String dir
= Bundles
.getDirectory();
1131 String bname
= type
.getPackage().getName() + "." + name
.name();
1133 boolean found
= false;
1134 if (!resetToDefault
&& dir
!= null) {
1135 // Look into Bundles.getDirectory() for .properties files
1137 File file
= getPropertyFile(dir
, name
.name(), locale
);
1139 Reader reader
= new InputStreamReader(new FileInputStream(
1141 resetMap(new PropertyResourceBundle(reader
));
1144 } catch (IOException e
) {
1145 e
.printStackTrace();
1150 // Look into the package itself for resources
1152 resetMap(ResourceBundle
1153 .getBundle(bname
, locale
, type
.getClassLoader(),
1154 new FixedResourceBundleControl()));
1156 } catch (MissingResourceException e
) {
1157 } catch (Exception e
) {
1158 e
.printStackTrace();
1163 // We have no bundle for this Bundle
1164 System
.err
.println("No bundle found for: " + bname
);
1170 * Reset the backing map to the content of the given bundle, or with default
1171 * values if bundle is NULL.
1174 * the bundle to copy
1176 protected void resetMap(ResourceBundle bundle
) {
1178 for (Field field
: type
.getDeclaredFields()) {
1180 Meta meta
= field
.getAnnotation(Meta
.class);
1182 E id
= Enum
.valueOf(type
, field
.getName());
1185 if (bundle
!= null) {
1186 value
= bundle
.getString(id
.name());
1191 this.map
.put(id
.name(), value
== null ?
null : value
.trim());
1193 } catch (MissingResourceException e
) {
1199 * Take a snapshot of the changes in memory in this {@link Bundle} made by
1200 * the "set" methods ( {@link Bundle#setString(Enum, String)}...) at the
1203 * @return a snapshot to use with {@link Bundle#restoreSnapshot(Object)}
1205 public Object
takeSnapshot() {
1206 return new HashMap
<String
, String
>(changeMap
);
1210 * Restore a snapshot taken with {@link Bundle}, or reset the current
1211 * changes if the snapshot is NULL.
1214 * the snapshot or NULL
1216 @SuppressWarnings("unchecked")
1217 public void restoreSnapshot(Object snap
) {
1221 if (snap
instanceof Map
) {
1222 changeMap
= (Map
<String
, String
>) snap
;
1224 throw new RuntimeException(
1225 "Restoring changes in a Bundle must be done on a changes snapshot, "
1226 + "or NULL to discard current changes");
1232 * Return the resource file that is closer to the {@link Locale}.
1235 * the directory to look into
1237 * the file base name (without <tt>.properties</tt>)
1239 * the {@link Locale}
1241 * @return the closest match or NULL if none
1243 private File
getPropertyFile(String dir
, String name
, Locale locale
) {
1244 List
<String
> locales
= new ArrayList
<String
>();
1245 if (locale
!= null) {
1246 String country
= locale
.getCountry() == null ?
"" : locale
1248 String language
= locale
.getLanguage() == null ?
"" : locale
1250 if (!language
.isEmpty() && !country
.isEmpty()) {
1251 locales
.add("_" + language
+ "-" + country
);
1253 if (!language
.isEmpty()) {
1254 locales
.add("_" + language
);
1261 for (String loc
: locales
) {
1262 file
= new File(dir
, name
+ loc
+ ".properties");
1263 if (file
.exists()) {