1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 /**
  9   The Locale defined information about a specific locale, including date and
 10   number formatting conventions, and localization strings.  You can define
 11   various locales by adding them to the SC.locales hash, keyed by language
 12   and/or country code.
 13 
 14   On page load, the default locale will be chosen based on the current
 15   languages and saved at SC.Locale.current.  This locale is used for
 16   localization, etc.
 17 
 18   ## Creating a new locale
 19 
 20   You can create a locale by simply extending the SC.Locale class and adding
 21   it to the locales hash:
 22 
 23       SC.Locale.locales['en'] = SC.Locale.extend({ .. config .. }) ;
 24 
 25   Alternatively, you could choose to base your locale on another locale by
 26   extending that locale:
 27 
 28       SC.Locale.locales['en-US'] = SC.Locale.locales['en'].extend({ ... }) ;
 29 
 30   Note that if you do not define your own strings property, then your locale
 31   will inherit any strings added to the parent locale.  Otherwise you must
 32   implement your own strings instead.
 33 
 34   @extends SC.Object
 35   @since SproutCore 1.0
 36 */
 37 SC.Locale = SC.Object.extend({
 38 
 39   init: function() {
 40     // make sure we know the name of our own locale.
 41     if (!this.language) SC.Locale._assignLocales();
 42 
 43     // Make sure we have strings that were set using the new API.  To do this
 44     // we check to a bool that is set by one of the string helpers.  This
 45     // indicates that the new API was used. If the new API was not used, we
 46     // check to see if the old API was used (which places strings on the
 47     // String class).
 48     if (!this.hasStrings) {
 49       var langs = this._deprecatedLanguageCodes || [] ;
 50       langs.push(this.language);
 51       var idx = langs.length ;
 52       var strings = null ;
 53       while(!strings && --idx >= 0) {
 54         strings = String[langs[idx]];
 55       }
 56       if (strings) {
 57         this.hasStrings = YES;
 58         this.strings = strings ;
 59       }
 60     }
 61   },
 62 
 63   /** Set to YES when strings have been added to this locale. */
 64   hasStrings: NO,
 65 
 66   /** The strings hash for this locale. */
 67   strings: {},
 68 
 69   /**
 70     The metrics for this locale.  A metric is a singular value that is usually
 71     used in a user interface layout, such as "width of the OK button".
 72   */
 73   metrics: {},
 74 
 75   /**
 76     The inflection constants for this locale.
 77   */
 78   inflectionConstants: null,
 79 
 80   /**
 81     The method used to compute the ordinal of a number for this locale.
 82   */
 83   ordinalForNumber: function(number) { 
 84     return '';
 85   },
 86 
 87 
 88   toString: function() {
 89     if (!this.language) SC.Locale._assignLocales() ;
 90     return "SC.Locale["+this.language+"]"+SC.guidFor(this) ;
 91   },
 92 
 93   /**
 94     Returns the localized version of the string or the string if no match
 95     was found.
 96 
 97     @param {String} string
 98     @param {String} optional default string to return instead
 99     @returns {String}
100   */
101   locWithDefault: function(string, def) {
102     var ret = this.strings[string];
103 
104     // strings may be blank, so test with typeOf.
105     if (SC.typeOf(ret) === SC.T_STRING) return ret;
106     else if (SC.typeOf(def) === SC.T_STRING) return def;
107     return string;
108   },
109 
110   /**
111     Returns the localized value of the metric for the specified key, or
112     undefined if no match is found.
113 
114     @param {String} key
115     @returns {Number} ret
116   */
117   locMetric: function(key) {
118     var ret = this.metrics[key];
119     if (SC.typeOf(ret) === SC.T_NUMBER) {
120       return ret;
121     }
122     else if (ret === undefined) {
123       SC.warn("No localized metric found for key \"" + key + "\"");
124       return undefined;
125     }
126     else {
127       SC.warn("Unexpected metric type for key \"" + key + "\"");
128       return undefined;
129     }
130   },
131 
132   /**
133     Creates and returns a new hash suitable for use as an SC.View’s 'layout'
134     hash.  This hash will be created by looking for localized metrics following
135     a pattern based on the “base key” you specify.
136 
137     For example, if you specify "Button.Confirm", the following metrics will be
138     used if they are defined:
139 
140       Button.Confirm.left
141       Button.Confirm.top
142       Button.Confirm.right
143       Button.Confirm.bottom
144       Button.Confirm.width
145       Button.Confirm.height
146       Button.Confirm.midWidth
147       Button.Confirm.minHeight
148       Button.Confirm.centerX
149       Button.Confirm.centerY
150 
151     Additionally, you can optionally specify a hash which will be merged on top
152     of the returned hash.  For example, if you wish to allow a button’s width
153     to be configurable per-locale, but always wish for it to be centered
154     vertically and horizontally, you can call:
155 
156       locLayout("Button.Confirm", {centerX:0, centerY:0})
157 
158     …so that you can combine both localized and non-localized elements in the
159     returned hash.  (An exception will be thrown if there is a locale-specific
160     key that matches a key specific in this hash.)
161 
162     @param {String} baseKey
163     @param {String} (optional) additionalHash
164     @returns {Hash}
165   */
166   locLayout: function(baseKey, additionalHash) {
167     // Note:  In this method we'll directly access this.metrics rather than
168     //        going through locMetric() for performance and to avoid
169     //        locMetric()'s sanity checks.
170 
171     var i, len, layoutKey, key, value,
172         layoutKeys = SC.Locale.layoutKeys,
173         metrics    = this.metrics,
174 
175         // Cache, to avoid repeated lookups
176         typeOfFunc = SC.typeOf,
177         numberType = SC.T_NUMBER,
178 
179         ret        = {};
180 
181 
182     // Start off by mixing in the additionalHash; we'll look for collisions with
183     // the localized values in the loop below.
184     if (additionalHash) SC.mixin(ret, additionalHash);
185 
186 
187     // For each possible key that can be included in a layout hash, see whether
188     // we have a localized value.
189     for (i = 0, len = layoutKeys.length;  i < len;  ++i) {
190       layoutKey = layoutKeys[i];
191       key       = baseKey + "." + layoutKey;
192       value     = metrics[key];
193 
194       if (typeOfFunc(value) === numberType) {
195         // We have a localized value!  As a sanity check, if the caller
196         // specified an additional hash and it has the same key, we'll throw an
197         // error.
198         if (additionalHash  &&  additionalHash[layoutKey]) {
199           throw new Error("locLayout():  There is a localized value for the key '" + key + "' but a value for '" + layoutKey + "' was also specified in the non-localized hash");
200         }
201 
202         ret[layoutKey] = value;
203       }
204     }
205 
206     return ret;
207   }
208 
209 }) ;
210 
211 SC.Locale.mixin(/** @scope SC.Locale */ {
212 
213   /**
214     If YES, localization will favor the detected language instead of the
215     preferred one.
216   */
217   useAutodetectedLanguage: NO,
218 
219   /**
220     This property is set by the build tools to the current build language.
221   */
222   preferredLanguage: null,
223 
224   /**
225     This property holds all attributes name which can be used for a layout hash
226     (for an SC.View).  These are what we support inside the layoutFor() method.
227   */
228   layoutKeys: ['left', 'top', 'right', 'bottom', 'width', 'height',
229                'minWidth', 'minHeight', 'centerX', 'centerY'],
230 
231   /**
232     Invoked at the start of SproutCore's document onready handler to setup
233     the currentLocale.  This will use the language properties you have set on
234     the locale to make a decision.
235   */
236   createCurrentLocale: function() {
237     // get values from String if defined for compatibility with < 1.0 build
238     // tools.
239     var autodetect = (String.useAutodetectedLanguage !== undefined) ? String.useAutodetectedLanguage : this.useAutodetectedLanguage;
240     var preferred = (String.preferredLanguage !== undefined) ? String.preferredLanguage : this.preferredLanguage ;
241 
242 
243     // determine the language
244     var lang = ((autodetect) ? SC.browser.language : null) || preferred || SC.browser.language || 'en';
245     lang = SC.Locale.normalizeLanguage(lang) ;
246     // get the locale class.  If a class cannot be found, fall back to generic
247     // language then to english.
248     var klass = this.localeClassFor(lang) ;
249 
250     // if the detected language does not match the current language (or there
251     // is none) then set it up.
252     if (lang != this.currentLanguage) {
253       this.currentLanguage = lang ; // save language
254       this.currentLocale = klass.create(); // setup locale
255     }
256     return this.currentLocale ;
257   },
258 
259   /**
260     Finds the locale class for the names language code or creates on based on
261     its most likely parent.
262   */
263   localeClassFor: function(lang) {
264     lang = SC.Locale.normalizeLanguage(lang) ;
265     var parent, klass = this.locales[lang];
266 
267     // if locale class was not found and there is a broader-based locale
268     // present, create a new locale based on that.
269     if (!klass && ((parent = lang.split('-')[0]) !== lang) && (klass = this.locales[parent])) {
270       klass = this.locales[lang] = klass.extend() ;
271     }
272 
273     // otherwise, try to create a new locale based on english.
274     if (!klass) klass = this.locales[lang] = this.locales.en.extend();
275 
276     return klass;
277   },
278 
279   /**
280     Shorthand method to define the settings for a particular locale.
281     The settings you pass here will be applied directly to the locale you
282     designate.
283 
284     If you are already holding a reference to a locale definition, you can
285     also use this method to operate on the receiver.
286 
287     If the locale you name does not exist yet, this method will create the
288     locale for you, based on the most closely related locale or english.  For
289     example, if you name the locale 'fr-CA', you will be creating a locale for
290     French as it is used in Canada.  This will be based on the general French
291     locale (fr), since that is more generic.  On the other hand, if you create
292     a locale for mandarin (cn), it will be based on generic english (en)
293     since there is no broader language code to match against.
294 
295     @param {String} localeName
296     @param {Hash} options
297     @returns {SC.Locale} the defined locale
298   */
299   define: function(localeName, options) {
300     var locale ;
301     if (options===undefined && (SC.typeOf(localeName) !== SC.T_STRING)) {
302       locale = this; options = localeName ;
303     } else locale = SC.Locale.localeClassFor(localeName) ;
304     SC.mixin(locale.prototype, options) ;
305     return locale ;
306   },
307 
308   /**
309     Gets the current options for the receiver locale.  This is useful for
310     inspecting registered locales that have not been instantiated.
311 
312     @returns {Hash} options + instance methods
313   */
314   options: function() { return this.prototype; },
315 
316   /**
317     Adds the passed hash to the locale's given property name.  Note that
318     if the receiver locale inherits its hashes from its parent, then the
319     property table will be cloned first.
320 
321     @param {String} name
322     @param {Hash} hash
323     @returns {Object} receiver
324   */
325   addHashes: function(name, hash) {
326     // make sure the target hash exists and belongs to the locale
327     var currentHash = this.prototype[name];
328     if (currentHash) {
329       if (!this.prototype.hasOwnProperty(currentHash)) {
330         currentHash = this.prototype[name] = SC.clone(currentHash);
331       }
332     }
333     else {
334       currentHash = this.prototype[name] = {};
335     }
336 
337     // add hash
338     if (hash) this.prototype[name] = SC.mixin(currentHash, hash);
339 
340     return this;
341   },
342 
343   /**
344     Adds the passed method to the locale's given property name. 
345 
346     @param {String} name
347     @param {Function} method
348     @returns {Object} receiver
349   */
350   addMethod: function(name, method) {
351     this.prototype[name] = method;
352     return this;
353   },
354 
355   /**
356     Adds the passed hash of strings to the locale's strings table.  Note that
357     if the receiver locale inherits its strings from its parent, then the
358     strings table will be cloned first.
359 
360     @returns {Object} receiver
361   */
362   addStrings: function(stringsHash) {
363     var ret = this.addHashes('strings', stringsHash);
364 
365     // Note:  We don't need the equivalent of this.hasStrings here, because we
366     //        are not burdened by an older API to look for like the strings
367     //        support is.
368     this.prototype.hasStrings = YES;
369 
370     return ret;
371   },
372 
373   /**
374     Adds the passed hash of metrics to the locale's metrics table, much as
375     addStrings() is used to add in strings.   Note that if the receiver locale
376     inherits its metrics from its parent, then the metrics table will be cloned
377     first.
378 
379     @returns {Object} receiver
380   */
381   addMetrics: function(metricsHash) {
382     return this.addHashes('metrics', metricsHash);
383   },
384 
385   _map: { english: 'en', french: 'fr', german: 'de', japanese: 'ja', jp: 'ja', spanish: 'es' },
386 
387   /**
388     Normalizes the passed language into a two-character language code.
389     This method allows you to specify common languages in their full english
390     name (i.e. English, French, etc). and it will be treated like their two
391     letter code equivalent.
392 
393     @param {String} languageCode
394     @returns {String} normalized code
395   */
396   normalizeLanguage: function(languageCode) {
397     if (!languageCode) return 'en' ;
398     return SC.Locale._map[languageCode.toLowerCase()] || languageCode ;
399   },
400 
401   // this method is called once during init to walk the installed locales
402   // and make sure they know their own names.
403   _assignLocales: function() {
404     for(var key in this.locales) this.locales[key].prototype.language = key;
405   },
406 
407   toString: function() {
408     if (!this.prototype.language) SC.Locale._assignLocales() ;
409     return "SC.Locale["+this.prototype.language+"]" ;
410   },
411 
412   // make sure important properties are copied to new class.
413   extend: function() {
414     var ret= SC.Object.extend.apply(this, arguments) ;
415     ret.addStrings= SC.Locale.addStrings;
416     ret.define = SC.Locale.define ;
417     ret.options = SC.Locale.options ;
418     ret.toString = SC.Locale.toString ;
419     return ret ;
420   }
421 
422 }) ;
423 
424 /**
425   This locales hash contains all of the locales defined by SproutCore and
426   by your own application.  See the SC.Locale class definition for the
427   various properties you can set on your own locales.
428 
429   @type Hash
430 */
431 SC.Locale.locales = {
432   en: SC.Locale.extend({ _deprecatedLanguageCodes: ['English'] }),
433   fr: SC.Locale.extend({ _deprecatedLanguageCodes: ['French'] }),
434   de: SC.Locale.extend({ _deprecatedLanguageCodes: ['German'] }),
435   ja: SC.Locale.extend({ _deprecatedLanguageCodes: ['Japanese', 'jp'] }),
436   es: SC.Locale.extend({ _deprecatedLanguageCodes: ['Spanish'] })
437 };
438 
439 /**
440   This special helper will store the propertyName / hashes pair you pass 
441   in the locale matching the language code.  If a locale is not defined 
442   from the language code you specify, then one will be created for you 
443   with the english locale as the parent.
444 
445   @param {String} languageCode
446   @param {String} propertyName
447   @param {Hash} hashes
448   @returns {void}
449 */
450 SC.hashesForLocale = function(languageCode, propertyName, hashes) {
451   var locale = SC.Locale.localeClassFor(languageCode);
452   locale.addHashes(propertyName, hashes);
453 };
454 
455 /**
456   Just like SC.hashesForLocale, but for methods.
457 
458   @param {String} languageCode
459   @param {String} propertyName
460   @param {Function} method
461   @returns {void}
462 */
463 SC.methodForLocale = function(languageCode, propertyName, method) {
464   var locale = SC.Locale.localeClassFor(languageCode);
465   locale.addMethod(propertyName, method);
466 };
467 
468 /**
469   This special helper will store the strings you pass in the locale matching
470   the language code.  If a locale is not defined from the language code you
471   specify, then one will be created for you with the english locale as the
472   parent.
473 
474   @param {String} languageCode
475   @param {Hash} strings
476   @returns {Object} receiver
477 */
478 SC.stringsFor = function(languageCode, strings) {
479   // get the locale, creating one if needed.
480   var locale = SC.Locale.localeClassFor(languageCode);
481   locale.addStrings(strings);
482   return this ;
483 };
484 
485 /**
486   Just like SC.stringsFor, but for metrics.
487 
488   @param {String} languageCode
489   @param {Hash} metrics
490   @returns {Object} receiver
491 */
492 SC.metricsFor = function(languageCode, metrics) {
493   var locale = SC.Locale.localeClassFor(languageCode);
494   locale.addMetrics(metrics);
495   return this;
496 };
497