Source code: er/extensions/ERXLocalizer.java
1 //
2 // ERXLocalizer.java
3 // Project armehaut
4 //
5 // Created by ak on Sun Apr 14 2002
6 //
7 package er.extensions;
8
9 import com.webobjects.foundation.*;
10 import com.webobjects.appserver.*;
11 import java.util.*;
12 import java.lang.reflect.Constructor;
13
14 /** KVC access to localization.
15 Monitors a set of files in all frameworks and returns a string given a key for a language.
16 In the current state, it's more a stub for things to come.
17
18 These types of keys are acceptable in the monitored files:
19
20 "this is a test" = "some test";
21 "unittest.key.path.as.string" = "some test";
22 "unittest" = {"key" = { "path" = { "as" = {"dict"="some test";};};};};
23
24 Note that if you only call for "unittest", you'll get a dictionary. So you can localize more complex objects than strings.
25
26 If you set the base class of your session to ERXSession, you can then use this code in your components:
27
28 valueForKeyPath("session.localizer.this is a test")
29 valueForKeyPath("session.localizer.unittest.key.path.as.string")
30 valueForKeyPath("session.localizer.unittest.key.path.as.dict")
31
32 For sessionless Apps, you must use another method to get at the requested language and then call the localizer via
33
34 ERXLocalizer l = ERXLocalizer.localizerForLanguages(languagesThisUserCanHandle) or
35 ERXLocalizer l = ERXLocalizer.localizerForLanguage("German")
36
37 These defaults can be set (listed with their current defaults):
38
39 er.extensions.ERXLocalizer.defaultLanguage=English
40 er.extensions.ERXLocalizer.fileNamesToWatch=("Localizable.strings","ValidationTemplate.strings")
41 er.extensions.ERXLocalizer.availableLanguages=(English,German)
42 er.extensions.ERXLocalizer.frameworkSearchPath=(app,ERDirectToWeb,ERExtensions)
43
44 TODO: chaining of Localizers
45 */
46
47 public class ERXLocalizer implements NSKeyValueCoding, NSKeyValueCodingAdditions {
48 protected static final ERXLogger log = ERXLogger.getERXLogger(ERXLocalizer.class);
49 protected static final ERXLogger createdKeysLog = (ERXLogger)ERXLogger.getLogger(ERXLocalizer.class.getName() + ".createdKeys");
50 private static boolean isLocalizationEnabled = false;
51 private static boolean isInitialized = false;
52
53 public static final String LocalizationDidResetNotification = "LocalizationDidReset";
54
55 public static class Observer {
56 public void fileDidChange(NSNotification n) {
57 ERXLocalizer.resetCache();
58 NSNotificationCenter.defaultCenter().postNotification(LocalizationDidResetNotification, null);
59 }
60
61 public void compilerProxyDidCompileClasses(NSNotification n) {
62 // ENHANCEME: Should deal with ERXLocalizer subclasses too.
63 if (! ERXCompilerProxy.isClassContainedBySet("er.extensions.ERXLocalizer", (NSSet)n.object())) {
64 return;
65 }
66 ERXLocalizer.resetCache();
67 NSNotificationCenter.defaultCenter().postNotification(LocalizationDidResetNotification, null);
68 }
69 }
70
71 private static Observer observer;
72 private static NSMutableArray monitoredFiles;
73
74 public static void initialize() {
75 if(!isInitialized) {
76 observer = new Observer();
77 monitoredFiles = new NSMutableArray();
78 isLocalizationEnabled = ERXProperties.booleanForKeyWithDefault("er.extensions.ERXLocalizer.isLocalizationEnabled", true);
79 if (isLocalizationEnabled) {
80 // To detect ERXLocalizer and its subclasses are recompiled at run-time.
81 NSNotificationCenter.defaultCenter().addObserver(
82 observer,
83 new NSSelector("compilerProxyDidCompileClasses", ERXConstant.NotificationClassArray),
84 ERXCompilerProxy.CompilerProxyDidCompileClassesNotification,
85 null);
86 }
87 isInitialized = true;
88 }
89 }
90
91 public static boolean isLocalizationEnabled() { return isLocalizationEnabled; }
92 public static void setIsLocalizationEnabled(boolean value) { isLocalizationEnabled = value; }
93
94 static NSArray fileNamesToWatch;
95 static NSArray frameworkSearchPath;
96 static NSArray availableLanguages;
97 static String defaultLanguage;
98
99 static NSMutableDictionary localizers = new NSMutableDictionary();
100
101 protected NSMutableDictionary cache;
102 private NSMutableDictionary createdKeys;
103 private String NOT_FOUND = "**NOT_FOUND**";
104
105 /**
106 * Resets the localizer cache. If WOCaching is
107 * enabled then after being reinitialize all of
108 * the localizers will be reloaded.
109 */
110 public static void resetCache() {
111 initialize();
112 if (WOApplication.application().isCachingEnabled()) {
113 Enumeration e = localizers.objectEnumerator();
114 while (e.hasMoreElements()) {
115 ((ERXLocalizer)e.nextElement()).load();
116 }
117 } else {
118 localizers = new NSMutableDictionary();
119 }
120 }
121
122 public static ERXLocalizer localizerForLanguages(NSArray languages) {
123 if (! isLocalizationEnabled)
124 return createLocalizerForLanguage("Nonlocalized", false);
125
126 if (languages == null || languages.count() == 0) return localizerForLanguage(defaultLanguage());
127 ERXLocalizer l = null;
128 Enumeration e = languages.objectEnumerator();
129 while(e.hasMoreElements()) {
130 String language = (String)e.nextElement();
131 l = (ERXLocalizer)localizers.objectForKey(language);
132 if(l != null) {
133 return l;
134 }
135 if(availableLanguages().containsObject(language)) {
136 return localizerForLanguage(language);
137 }
138 }
139 return localizerForLanguage((String)languages.objectAtIndex(0));
140 }
141
142 private static NSArray _languagesWithoutPluralForm = new NSArray(new Object [] {"Japanese"});
143
144 public static ERXLocalizer localizerForLanguage(String language) {
145 if (! isLocalizationEnabled)
146 return createLocalizerForLanguage("Nonlocalized", false);
147
148 ERXLocalizer l = null;
149 l = (ERXLocalizer)localizers.objectForKey(language);
150 if(l == null) {
151 if(availableLanguages().containsObject(language)) {
152 if (_languagesWithoutPluralForm.containsObject(language))
153 l = createLocalizerForLanguage(language, false);
154 else
155 l = createLocalizerForLanguage(language, true);
156 } else {
157 l = (ERXLocalizer)localizers.objectForKey(defaultLanguage());
158 if(l == null) {
159 if (_languagesWithoutPluralForm.containsObject(defaultLanguage()))
160 l = createLocalizerForLanguage(defaultLanguage(), false);
161 else
162 l = createLocalizerForLanguage(defaultLanguage(), true);
163 localizers.setObjectForKey(l, defaultLanguage());
164 }
165 }
166 localizers.setObjectForKey(l, language);
167 }
168 return l;
169 }
170
171 /**
172 * Creates a localizer for a given language and with an
173 * indication if the language supports plural forms. To provide
174 * your own subclass of an ERXLocalizer you can set the system
175 * property <code>er.extensions.ERXLocalizer.pluralFormClassName</code>
176 * or <code>er.extensions.ERXLocalizer.nonPluralFormClassName</code>.
177 * @param language name to construct the localizer for
178 * @param pluralForm denotes if the language supports the plural form
179 * @return a localizer for the given language
180 */
181 protected static ERXLocalizer createLocalizerForLanguage(String language, boolean pluralForm) {
182 ERXLocalizer localizer = null;
183 String className = null;
184 if (pluralForm) {
185 className = ERXProperties.stringForKeyWithDefault("er.extensions.ERXLocalizer.pluralFormClassName", "er.extensions.ERXLocalizer");
186 } else {
187 className = ERXProperties.stringForKeyWithDefault("er.extensions.ERXLocalizer.nonPluralFormClassName", "er.extensions.ERXNonPluralFormLocalizer");
188 }
189 try {
190 Class localizerClass = Class.forName(className);
191 Constructor constructor = localizerClass.getConstructor(ERXConstant.StringClassArray);
192 localizer = (ERXLocalizer)constructor.newInstance(new Object[] {language});
193 } catch (Exception e) {
194 log.error("Unable to create localizer for class name: " + className + " exception: " + e.getMessage() + " will use default classes");
195 }
196 if (localizer == null) {
197 if (pluralForm)
198 localizer = new ERXLocalizer(language);
199 else
200 localizer = new ERXNonPluralFormLocalizer(language);
201 }
202 return localizer;
203 }
204
205 public static void setLocalizerForLanguage(ERXLocalizer l, String language) {
206 localizers.setObjectForKey(l, language);
207 }
208
209 /**
210 * Cover method that calls <code>localizedStringForKey</code>.
211 * @param key to resolve a localized varient of
212 * @return localized string for the given key
213 */
214 public Object valueForKey(String key) {
215 return localizedValueForKey(key);
216 }
217
218 public Object valueForKeyPath(String key) {
219 Object result = localizedValueForKey(key);
220 if(result == null) {
221 int indexOfDot = key.indexOf(".");
222 if(indexOfDot > 0) {
223 String firstComponent = key.substring(0, indexOfDot);
224 String otherComponents = key.substring(indexOfDot+1, key.length());
225 result = cache.objectForKey(firstComponent);
226 if(log.isDebugEnabled())
227 log.debug("Trying " + firstComponent + " . " + otherComponents);
228 if(result != null) {
229 result = NSKeyValueCodingAdditions.Utility.valueForKeyPath(result, otherComponents);
230 if(result != null) {
231 cache.setObjectForKey(result, key);
232 } else {
233 cache.setObjectForKey(NOT_FOUND, key);
234 }
235 }
236 }
237 }
238 return result;
239 }
240 public void takeValueForKey(Object value, String key) {
241 cache.setObjectForKey(value, key);
242 }
243 public void takeValueForKeyPath(Object value, String key) {
244 cache.setObjectForKey(value, key);
245 }
246
247 String language;
248 public ERXLocalizer(String aLanguage) {
249 language = aLanguage;
250 cache = new NSMutableDictionary();
251 createdKeys = new NSMutableDictionary();
252 load();
253 }
254
255 public void load() {
256 cache.removeAllObjects();
257 createdKeys.removeAllObjects();
258
259 if (log.isDebugEnabled())
260 log.debug("Loading templates for language: " + language + " for files: "
261 + fileNamesToWatch() + " with search path: " + frameworkSearchPath());
262
263 NSArray languages = new NSArray(language);
264 WOResourceManager rm = WOApplication.application().resourceManager();
265 Enumeration fn = fileNamesToWatch().objectEnumerator();
266 while(fn.hasMoreElements()) {
267 String fileName = (String)fn.nextElement();
268 Enumeration fr = frameworkSearchPath().reverseObjectEnumerator();
269 while(fr.hasMoreElements()) {
270 String framework = (String)fr.nextElement();
271
272 String path = rm.pathForResourceNamed(fileName, framework, languages);
273 if(path != null) {
274 if(!monitoredFiles.containsObject(path)) {
275 ERXFileNotificationCenter.defaultCenter().addObserver(observer, new NSSelector("fileDidChange", ERXConstant.NotificationClassArray), path);
276 monitoredFiles.addObject(path);
277 }
278 try {
279 framework = "app".equals(framework) ? null : framework;
280 log.debug("Loading: " + fileName + " - "
281 + (framework == null ? "app" : framework) + " - "
282 + languages + WOApplication.application().resourceManager()
283 .pathForResourceNamed(fileName, framework, languages));
284 NSDictionary dict = (NSDictionary)ERXExtensions.readPropertyListFromFileInFramework(fileName, framework, languages);
285 cache.addEntriesFromDictionary(dict);
286 } catch(Exception ex) {
287 log.warn("Exception loading: " + fileName + " - "
288 + (framework == null ? "app" : framework) + " - "
289 + languages + ":" + ex);
290 }
291 } else if (log.isDebugEnabled()) {
292 log.debug("Unable to create path for resource named: " + fileName
293 + " framework: " + (framework == null ? "app" : framework)
294 + " languages: " + languages);
295 }
296 }
297 }
298 }
299
300 // CHECKME: Don't think that we need these any more now that we can have access to
301 // the current localizer via thread storage
302 public static NSDictionary fakeSessionForLanguage(String language) {
303 ERXLocalizer localizer = localizerForLanguage(language);
304 return new NSDictionary(new Object[] {localizer,language}, new Object[] {"localizer", "language"} );
305 }
306
307 // CHECKME: Don't think that we need these any more now that we can have access to
308 // the current localizer via thread storage
309 public static NSDictionary fakeSessionForSession(Object session) {
310 ERXLocalizer localizer = localizerForSession(session);
311 return new NSDictionary(new Object[] {localizer,localizer.language()}, new Object[] {"localizer", "language"} );
312 }
313
314 /**
315 * Returns the current localizer for the current thread.
316 * Note that the localizer for a given session is pushed
317 * onto the thread when a session awakes and is nulled out
318 * when a session sleeps.
319 * @return the current localizer that has been pushed into
320 * thread storage.
321 */
322 public static ERXLocalizer currentLocalizer() {
323 return (ERXLocalizer)ERXThreadStorage.valueForKey("localizer");
324 }
325
326 /**
327 * Sets a localizer for the current thread. This is accomplished
328 * by using the object {@link ERXThreadStorage}
329 * @param currentLocalizer to set in thread storage for the current
330 * thread.
331 */
332 public static void setCurrentLocalizer(ERXLocalizer currentLocalizer) {
333 ERXThreadStorage.takeValueForKey(currentLocalizer, "localizer");
334 }
335
336 /**
337 * Gets the localizer for the default language.
338 * @return localizer for the default language.
339 */
340 public static ERXLocalizer defaultLocalizer() {
341 return localizerForLanguage(defaultLanguage());
342 }
343
344 public static ERXLocalizer localizerForSession(Object session) {
345 if(session instanceof ERXSession) return ((ERXSession)session).localizer();
346 if(session instanceof WOSession) return localizerForLanguages(((WOSession)session).languages());
347 if(session instanceof NSDictionary) {
348 NSDictionary dict = ((NSDictionary)session);
349 Object l = dict.valueForKey("localizer");
350 if(l != null)
351 return (ERXLocalizer)l;
352 Object language = dict.valueForKey("language");
353 if(language != null)
354 return localizerForLanguage((String)language);
355 }
356 return localizerForLanguage(defaultLanguage());
357 }
358
359 public String language() { return language; }
360 public NSDictionary createdKeys() { return createdKeys; }
361
362 public Object localizedValueForKeyWithDefault(String key) {
363 Object result = localizedValueForKey(key);
364 if(result == null) {
365 if(createdKeysLog.isDebugEnabled())
366 createdKeysLog.debug("Default key inserted: '"+key+"'/"+language);
367 cache.setObjectForKey(key, key);
368 createdKeys.setObjectForKey(key, key);
369 result = key;
370 }
371 return result;
372 }
373
374 public Object localizedValueForKey(String key) {
375 Object result = cache.objectForKey(key);
376 if(result == NOT_FOUND) return null;
377 if(result != null) return result;
378
379 if(createdKeysLog.isDebugEnabled())
380 log.debug("Key not found: '"+key+"'/"+language);
381 cache.setObjectForKey(NOT_FOUND, key);
382 return null;
383 }
384
385 public String localizedStringForKeyWithDefault(String key) {
386 return (String)localizedValueForKeyWithDefault(key);
387 }
388 public String localizedStringForKey(String key) {
389 return (String)localizedValueForKey(key);
390 }
391
392 public String localizedTemplateStringForKeyWithObject(String key, Object o1) {
393 return localizedTemplateStringForKeyWithObjectOtherObject(key, o1, null);
394 }
395
396 public String localizedTemplateStringForKeyWithObjectOtherObject(String key, Object o1, Object o2) {
397 String template = localizedStringForKey(key);
398 if (template != null)
399 return ERXSimpleTemplateParser.sharedInstance().parseTemplateWithObject(template, null, o1, o2);
400 return key;
401 }
402
403 private String _plurify(String s, int howMany) {
404 String result=s;
405 if (s!=null && howMany!=1) {
406 if (s.endsWith("y"))
407 result=s.substring(0,s.length()-1)+"ies";
408 else if (s.endsWith("s") && ! s.endsWith("ss")) {
409 // we assume it's already plural. There are a few words this will break this heuristic
410 // e.g. gas --> gases
411 // but otherwise for Documents we get Documentses..
412 } else if (s.endsWith("s") || s.endsWith("ch") || s.endsWith("sh") || s.endsWith("x"))
413 result+="es";
414 else
415 result+= "s";
416 }
417 return result;
418 }
419
420 private String _singularify(String value) {
421 String result = value;
422 if (value!=null) {
423 if (value.endsWith("ies"))
424 result = value.substring(0,value.length()-3)+"y";
425 else if (value.endsWith("hes"))
426 result = value.substring(0,value.length()-2);
427 else if (!value.endsWith("ss") && (value.endsWith("s") || value.endsWith("ses")))
428 result = value.substring(0,value.length()-1);
429 }
430 return result;
431 }
432
433 // name is already localized!
434 // subclasses can override for more sensible behaviour
435 public String plurifiedStringWithTemplateForKey(String key, String name, int count, Object helper) {
436 NSDictionary dict = new NSDictionary( new Object[] {plurifiedString(name, count), new Integer(count)},
437 new Object[] {"pluralString", "pluralCount"});
438 return localizedTemplateStringForKeyWithObjectOtherObject(key, dict, helper);
439 }
440
441 public String plurifiedString(String name, int count) {
442 return _plurify(name, count);
443 }
444
445 public String toString() { return "<" + getClass().getName() + " " + language + ">"; }
446
447 public static String defaultLanguage() {
448 if (defaultLanguage == null) {
449 defaultLanguage = ERXProperties.stringForKeyWithDefault("er.extensions.ERXLocalizer.defaultLanguage", "English");
450 }
451 return defaultLanguage;
452 }
453 public static void setDefaultLanguage(String value) {
454 defaultLanguage = value;
455 resetCache();
456 }
457
458 public static NSArray fileNamesToWatch() {
459 if (fileNamesToWatch == null) {
460 fileNamesToWatch = ERXProperties.arrayForKeyWithDefault("er.extensions.ERXLocalizer.fileNamesToWatch", new NSArray(new Object [] {"Localizable.strings", "ValidationTemplate.strings"}));
461 if (log.isDebugEnabled())
462 log.debug("FileNamesToWatch: " + fileNamesToWatch);
463 }
464 return fileNamesToWatch;
465 }
466 public static void setFileNamesToWatch(NSArray value) {
467 fileNamesToWatch = value;
468 resetCache();
469 }
470
471 public static NSArray availableLanguages() {
472 if(availableLanguages == null) {
473 availableLanguages = ERXProperties.arrayForKeyWithDefault("er.extensions.ERXLocalizer.availableLanguages", new NSArray(new Object [] {"English", "German", "Japanese"}));
474 if (log.isDebugEnabled())
475 log.debug("AvailableLanguages: " + availableLanguages);
476 }
477 return availableLanguages;
478 }
479 public static void setAvailableLanguages(NSArray value) {
480 availableLanguages = value;
481 resetCache();
482 }
483
484 public static NSArray frameworkSearchPath() {
485 if (frameworkSearchPath == null) {
486 frameworkSearchPath = ERXProperties.arrayForKeyWithDefault("er.extensions.ERXLocalizer.frameworkSearchPath", new NSArray(new Object [] {"app", "ERDirectToWeb", "ERExtensions"}));
487 if (log.isDebugEnabled())
488 log.debug("FrameworkSearchPath: " + frameworkSearchPath);
489 }
490 return frameworkSearchPath;
491 }
492 public static void setFrameworkSearchPath(NSArray value) {
493 frameworkSearchPath = value;
494 resetCache();
495 }
496 }