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