UNPKG

cejs

Version:

A JavaScript module framework that is simple to use.

1,614 lines (1,420 loc) 84 kB
/** * @name CeL function for locale / i18n (Internationalization, ja:地域化) 系列 * @fileoverview 本檔案包含了地區語系/文化設定的 functions。 * @since * @see http://blog.miniasp.com/post/2010/12/24/Search-and-Download-International-Terminology-Microsoft-Language-Portal.aspx * http://www.microsoft.com/language/zh-tw/default.aspx Microsoft | 語言入口網站 */ 'use strict'; // 'use asm'; // -------------------------------------------------------------------------------------------- // 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。 typeof CeL === 'function' && CeL.run({ // module name name : 'application.locale', // data.numeral.to_Chinese_numeral|data.numeral.to_positional_Chinese_numeral|data.numeral.to_English_numeral require : 'data.numeral.to_Chinese_numeral' // + '|data.numeral.to_positional_Chinese_numeral', // 設定不匯出的子函式。 // no_extend : '*', // 為了方便格式化程式碼,因此將 module 函式主體另外抽出。 code : module_code }); function module_code(library_namespace) { var module_name = this.id, // requiring to_Chinese_numeral = this.r('to_Chinese_numeral'), to_positional_Chinese_numeral = this .r('to_positional_Chinese_numeral'); /** * null module constructor * * @class locale 的 functions */ var _// JSDT:_module_ = function() { // null module constructor }; /** * for JSDT: 有 prototype 才會將之當作 Class */ _// JSDT:_module_ .prototype = {}; /** * <code> <a href="http://www.ietf.org/rfc/bcp/bcp47.txt" accessdate="2012/8/22 15:23" title="BCP 47: Tags for Identifying Languages">BCP 47</a> language tag http://www.whatwg.org/specs/web-apps/current-work/#the-lang-and-xml:lang-attributes The lang attribute (in no namespace) specifies the primary language for the element's contents and for any of the element's attributes that contain text. Its value must be a valid BCP 47 language tag, or the empty string. <a href="http://www.w3.org/International/articles/language-tags/" accessdate="2012/9/23 13:29">Language tags in HTML and XML</a> language-extlang-script-region-variant-extension-privateuse http://www.cnblogs.com/sink_cup/archive/2011/04/15/written_language_and_spoken_language.html http://zh.wikipedia.org/wiki/%E6%B1%89%E8%AF%AD <a href="http://en.wikipedia.org/wiki/IETF_language_tag" accessdate="2012/8/22 15:25">IETF language tag</a> TODO: en-X-US </code> */ function language_tag(tag) { return language_tag.parse.call(this, tag); } // 3_language[-3_extlang][-3_extlang][-4_script][-2w|3d_region] language_tag.language_RegExp = /^(?:(?:([a-z]{2,3})(?:-([a-z]{4,8}|[a-z]{3}(?:-[a-z]{3}){0,1}))?))(?:-([a-z]{4}))?(?:-([a-z]{2}|\d{3}))?((?:-(?:[a-z\d]{2,8}))*)$/; // x-fragment[-fragment].. language_tag.privateuse_RegExp = /^x((?:-(?:[a-z\d]{1,8}))+)$/; // 片段 language_tag.privateuse_fragment_RegExp = /-([a-z\d]{1,8})/g; language_tag.parse = function(tag) { this.tag = tag; // language tags and their subtags, including private use and // extensions, are to be treated as case insensitive tag = String(tag).toLowerCase(); var i = 1, match = language_tag.language_RegExp.exec(tag); if (match) { library_namespace.debug(match.join('<br />'), 3, 'language_tag.parse'); // 3_language[-3_extlang][-3_extlang][-4_script][-2w|3d_region] // <a href="http://en.wikipedia.org/wiki/ISO_639-3" // accessdate="2012/9/22 17:5">ISO 639-3 codes</a> // list: <a href="http://en.wikipedia.org/wiki/ISO_639:a" // accessdate="2012/9/22 16:56">ISO 639:a</a> // 國際語種代號標準。 this.language = match[i++]; // TODO: 查表對照轉換, fill this.language this.extlang = match[i++]; /** * @see <a * href="http://en.wikipedia.org/wiki/ISO_15924#List_of_codes" * accessdate="2012/9/22 16:57">ISO 15924 code</a> */ // 書寫文字。match[] 可能是 undefined。 this.script = (match[i++] || '').replace(/^[a-z]/, function($0) { return $0.toUpperCase(); }); /** * @see <a * href="http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements" * accessdate="2012/9/22 16:58">ISO 3166-1 alpha-2 code</a> */ // 國家/地區/區域/領域代碼。match[] 可能是 undefined。 this.region = (match[i++] || '').toUpperCase(); // TODO: variant, extension, privateuse this.external = match[i++]; if (library_namespace.is_debug(2)) { for (i in this) { library_namespace.debug(i + ' : ' + this[i], 2, 'language_tag.parse'); } } } else if (match = language_tag.privateuse_RegExp.exec(tag)) { // x-fragment[-fragment].. library_namespace.debug('parse privateuse subtag [' + tag + ']', 2, 'language_tag.parse'); tag = match[1]; this.privateuse = i = []; // reset 'g' flag language_tag.privateuse_fragment_RegExp.exec(''); while (match = language_tag.privateuse_fragment_RegExp.exec(tag)) { i.push(match[1]); } library_namespace.debug('privateuse subtag: ' + i, 2, 'language_tag.parse'); } else if (library_namespace.is_debug()) { library_namespace.warn('unrecognized language tag: [' + tag + ']'); } return this; }; // 查表對照轉換。 language_tag.convert = function() { // TODO throw new Error('language_tag.convert: ' // gettext_config:{"id":"not-yet-implemented"} + gettext('Not Yet Implemented!')); }; /** * <code> new language_tag('cmn-Hant-TW'); new language_tag('zh-cmn-Hant-TW'); new language_tag('zh-Hant-TW'); new language_tag('zh-TW'); new language_tag('cmn-Hant'); new language_tag('zh-Hant'); new language_tag('x-CJK').language; new language_tag('zh-Hant').language; </code> */ // 語系代碼,應使用 language_tag.language_code(region) 的方法。 // 主要的應該放後面。 // mapping: region code (ISO 3166) → default language code (ISO 639) // https://en.wikipedia.org/wiki/Template:ISO_639_name language_tag.LANGUAGE_CODE = { // 中文 ZH : 'zh', // http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry // Preferred-Value: cmn CN : 'cmn-Hans', // cmn-Hant-HK, yue-Hant-HK HK : 'cmn-Hant', TW : 'cmn-Hant', // ja-JP JP : 'ja', // ko-KR KR : 'ko', // en-GB GB : 'en', // There is no "en-UK" language code although it is often used on web // pages. http://microformats.org/wiki/en-uk // https://moz.com/community/q/uk-and-gb-when-selecting-targeted-engines-in-campaign-management UK : 'en', // en-US US : 'en', FR : 'fr', DE : 'de', // ru-RU RU : 'ru', // arb-Arab Arab : 'arb' }; /** * Get the default language code of region. * * @param {String}region * region code (ISO 3166) * @returns {String} language code (ISO 639) */ language_tag.language_code = function(region, regular_only) { var code = language_tag.LANGUAGE_CODE[language_tag.region_code(region)]; if (!code // identity alias && !language_tag.LANGUAGE_CODE[code = region.toLowerCase()]) { if (library_namespace.is_debug()) library_namespace .warn('language_tag.language_code: 無法辨識之國家/區域:[' + region + ']'); if (regular_only) return; } return code; } // mapping: region name → region code (ISO 3166) // https://en.wikipedia.org/wiki/ISO_3166-1 // language_tag.region_code() 會自動測試添加"國"字,因此不用省略這個字。 language_tag.REGION_CODE = { 臺 : 'TW', 臺灣 : 'TW', 台 : 'TW', 台灣 : 'TW', // for language_tag.LANGUAGE_CODE 中文 : 'ZH', 陸 : 'CN', 大陸 : 'CN', 中國 : 'CN', 中國大陸 : 'CN', jpn : 'JP', 日 : 'JP', 日本 : 'JP', 港 : 'HK', 香港 : 'HK', 韓國 : 'KR', 英國 : 'GB', 美國 : 'US', 法國 : 'FR', 德國 : 'DE', 俄國 : 'RU', 俄羅斯 : 'RU', 阿拉伯 : 'Arab' }; // reverse (function() { for ( var region_code in language_tag.REGION_CODE) { if ((region_code = language_tag.REGION_CODE[region_code]) // identity alias: REGION_CODE[TW] = 'TW' && !language_tag.REGION_CODE[region_code]) language_tag.REGION_CODE[region_code] = region_code; } for ( var language_code in language_tag.LANGUAGE_CODE) { // reversed alias // e.g., ja → JP // e.g., cmn-hans → CN language_tag.REGION_CODE[language_tag.LANGUAGE_CODE[language_code] .toLowerCase()] = language_code; } // 因為下面的操作會改變 language_tag.LANGUAGE_CODE,因此不能與上面的同時操作。 for ( var language_code in language_tag.LANGUAGE_CODE) { if ((language_code = language_tag.LANGUAGE_CODE[language_code]) // identity alias && !language_tag.LANGUAGE_CODE[language_code]) language_tag.LANGUAGE_CODE[language_code] = language_code; } })(); /** * Get the default region code of region. * * @param {String}region * region name * @returns {String} region code (ISO 3166) */ language_tag.region_code = function(region, regular_only) { var code = language_tag.REGION_CODE[region]; if (!code) { library_namespace.debug('嘗試解析 [' + region + ']。', 3, 'language_tag.region_code'); if (/^[a-z\-]+$/i.test(region)) { library_namespace.debug('嘗試 reversed alias 的部分。', 3, 'language_tag.region_code'); // language_code → region_code // e.g., cmn-Hant → search cmn-hant code = language_tag.REGION_CODE[region.toLowerCase()]; } else if (code = region.match(/^(.+)[語文]$/)) { code = language_tag.REGION_CODE[code[1]] // e.g., 英語 → search 英國 || language_tag.REGION_CODE[code[1] + '國']; } else { // e.g., 英 → search 英國 code = language_tag.REGION_CODE[region + '國']; } if (!code && (code = region.match(/-([a-z]{2,3})$/))) // e.g., zh-tw → search TW code = language_tag.REGION_CODE[code[1].toUpperCase()]; if (!code // identity alias && !language_tag.REGION_CODE[code = region.toUpperCase()]) { // 依舊無法成功。 if (library_namespace.is_debug()) library_namespace .warn('language_tag.region_code: 無法辨識之國家/區域:[' + region + ']'); if (regular_only) return; } } return code; } _// JSDT:_module_ .language_tag = language_tag; // ----------------------------------------------------------------------------------------------------------------- // 各個 domain 結尾標點符號的轉換。 var halfwidth_to_fullwidth_mapping = { '.' : '。' }, fullwidth_to_halfwidth_mapping = { '、' : ',', '。' : '.' }; var PATTERN_language_code_is_CJK = /^(?:cmn|yue|ja)-/; function convert_punctuation_mark(punctuation_mark, domain_name) { if (!punctuation_mark) return punctuation_mark; // test domains_using_fullwidth_form if (PATTERN_language_code_is_CJK.test(domain_name)) { // 東亞標點符號。 if (punctuation_mark in halfwidth_to_fullwidth_mapping) { return halfwidth_to_fullwidth_mapping[punctuation_mark]; } if (/^ *\.{3,} *$/.test(punctuation_mark)) { // 中文預設標點符號前後無空白。 punctuation_mark = punctuation_mark.trim(); return '…'.repeat(punctuation_mark.length > 6 ? Math .ceil(punctuation_mark.length / 3) : 2); } if (/^ja-/.test(domain_name) && punctuation_mark === ',') { return '、'; } if (punctuation_mark.length === 1) { // https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block) var char_code = punctuation_mark.charCodeAt(0); if (char_code < 0xff) { return String.fromCharCode(char_code + 0xfee0); } } } else if (/^[^\x20-\xfe]/.test(punctuation_mark)) { if (punctuation_mark in fullwidth_to_halfwidth_mapping) { return fullwidth_to_halfwidth_mapping[punctuation_mark]; } if (/^…+$/.test(punctuation_mark)) { return punctuation_mark.length > 2 ? '...' .repeat(punctuation_mark.length) : '...'; } if (punctuation_mark.length === 1) { var char_code = punctuation_mark.charCodeAt(0); if (char_code > 0xfee0) { return String.fromCharCode(char_code - 0xfee0); } } } if (punctuation_mark.length > 1) { // PATTERN_punctuation_marks return punctuation_mark.replace(/%(\d)|(:)\s*|./g, function(p_m, NO, p_m_with_spaces) { if (NO) return p_m; if (p_m_with_spaces) { if (!PATTERN_language_code_is_CJK.test(domain_name)) return p_m; p_m = p_m_with_spaces; } return convert_punctuation_mark(p_m, domain_name); }); } return punctuation_mark; } // ----------------------------------------------------------------------------------------------------------------- var plural_rules__domain_name = 'gettext_plural_rules'; // plural_rules[language_code] // = [ #plural forms, function(){ return #plural form; } ] var plural_rules_of_language_code = Object.create(null); gettext.set_plural_rules = function set_plural_rules(plural_rules_Object) { for ( var language_code in plural_rules_Object) { var plural_rule = plural_rules_Object[language_code]; language_code = gettext.to_standard(language_code); if (language_code) { plural_rules_of_language_code[language_code] = plural_rule; }// else: 尚未支援的語言。 } }; // ------------------------------------ // matched: [ all behavior switch, is NO, NO ] var PATTERN_plural_switch_header = /\{\{PLURAL: *(%)?(\d+) *\|/, // matched: [ all behavior switch, previous, is NO, NO, parameters ] PATTERN_plural_switches_global = new RegExp('(' + PATTERN_plural_switch_header.source + ')' + /([\s\S]+?)\}\}/.source, 'ig'); // 處理 {{PLURAL:%1|summary|summaries}} // 處理 {{PLURAL:$1|1=you|$1 users including you}} // 處理 {{PLURAL:42|42=The answer is 42|Wrong answer|Wrong answers}} // https://raw.githubusercontent.com/wikimedia/mediawiki-extensions-Translate/master/data/plural-gettext.txt // https://translatewiki.net/wiki/Plural // https://docs.transifex.com/formats/gettext#plural-forms-in-a-po-file function adapt_plural(converted_text, value_list, domain_name) { var plural_count, plural_rule = plural_rules_of_language_code[domain_name]; if (Array.isArray(plural_rule)) { plural_count = plural_rule[0]; plural_rule = plural_rule[1]; } // console.trace([ domain_name, plural_count, plural_rule ]); converted_text = converted_text.replace_till_stable( // PATTERN_plural_switches_global, function(all, _previous, is_NO, NO, parameters) { // https://translatewiki.net/wiki/Plural // And you can nest it freely // 自 end_mark 向前回溯。 // TODO: using lookbehind search? var previous = '', nest_matched; while (nest_matched = parameters .match(PATTERN_plural_switch_header)) { previous += _previous // + parameters.slice(0, nest_matched.index); _previous = nest_matched[0]; is_NO = nest_matched[1]; NO = nest_matched[2]; parameters = parameters.slice(nest_matched.index + _previous.length); } var value = is_NO ? value_list[NO] : NO; if (value < 0) value = -value; var plural_NO = (typeof plural_rule === 'function' // ? plural_rule(+value) : plural_rule) + 1; var converted, default_converted, delta = 1; parameters = parameters.split('|'); parameters.some(function(parameter, index) { var matched = parameter.match(/^(\d+)=([\s\S]*)$/); if (matched) { delta--; index = +matched[1]; parameter = matched[2]; if (index == value) { converted = parameter; return true; } if (!default_converted) default_converted = parameter; return; } index += delta; if (plural_NO >= 1) { if (index === plural_NO) { converted = parameter; // Do not return. Incase {{PLURAL:5|one|other|5=5}} } else if (index === 2 && plural_count !== 2 // Special case. e.g., {{PLURAL:2||s}} // @ zh(plural_count=1), ru(3), NOT fr(2) && value != 1 && parameters.length === 2) { converted = parameter; // assert: Should be the last element of parameters. } else { default_converted = parameter; } return; } /** * https://translatewiki.net/wiki/Plural * * If the number of forms written is less than the number of * forms required by the plural rules of the language, the last * available form will be used for all missing forms. */ default_converted = parameter; if (index == value) { converted = parameter; return true; } }); return previous // + (converted === undefined ? default_converted : converted); }); return converted_text; } gettext.adapt_plural = adapt_plural; // ----------------------------------------------------------------------------------------------------------------- // JavaScript 國際化 i18n (Internationalization) / 在地化 本土化 l10n (Localization) // / 全球化 g11n (Globalization). /** * 為各種不同 domain 轉換文字(句子)、轉成符合當地語言的訊息內容。包括但不僅限於各種語系。<br /> * 需要確認系統相應 domain resources 已載入時,請利用 gettext.use_domain(domain, callback)。 * * TODO: using localStorage.<br /> * https://translatewiki.net/wiki/Plural * * @example <code> // More examples: see /_test suite/test.js * </code> * * @param {String|Function|Object}text_id * 欲呼叫之 text id。<br /> ** 若未能取得,將直接使用此值。因此即使使用簡單的代號,也建議使用 * msg#12, msg[12] 之類的表示法,而非直接以整數序號代替。<br /> * 嵌入式的一次性使用,不建議如此作法: { domain : text id } * @param {String|Function}conversion_list * other conversion to include * * @returns {String}轉換過的文字。 * * @since 2012/9/9 00:53:52 * * @see <a * href="http://stackoverflow.com/questions/48726/best-javascript-i18n-techniques-ajax-dates-times-numbers-currency" * accessdate="2012/9/9 0:13">Best JavaScript i18n techniques / Ajax - * dates, times, numbers, currency - Stack Overflow</a>,<br /> * <a * href="http://stackoverflow.com/questions/3084675/internationalization-in-javascript" * accessdate="2012/9/9 0:13">Internationalization in Javascript - * Stack Overflow</a>,<br /> * <a * href="http://stackoverflow.com/questions/9640630/javascript-i18n-internationalization-frameworks-libraries-for-clientside-use" * accessdate="2012/9/9 0:13">javascript i18n (internationalization) * frameworks/libraries for clientside use - Stack Overflow</a>,<br /> * <a href="http://msdn.microsoft.com/en-us/library/txafckwd.aspx" * accessdate="2012/9/17 23:0">Composite Formatting</a>, * http://wiki.ecmascript.org/doku.php?id=strawman:string_format, * http://wiki.ecmascript.org/doku.php?id=strawman:string_format_take_two */ function gettext(/* message */text_id/* , ...value_list */) { // 轉換 / convert function. function convert(text_id, domain_specified) { // 未設定個別 domain 者,將以此訊息(text_id)顯示。 // text_id 一般應採用原文(message of original language), // 或最常用語言;亦可以代碼(message id)表示,但須設定所有可能使用的語言。 // console.log(text_id); var prefix, postfix; if (library_namespace.is_debug(12)) { console.trace(domain); } // 注意: 在 text_id 與所屬 domain 之 converted_text 相同的情況下, // domain 中不會有這一筆記錄。 // 因此無法以 `text_id in domain` 來判別 fallback。 if (typeof text_id === 'function' || typeof text_id === 'object') { using_default = true; } else if (!(text_id in domain)) { var matched = String(text_id).match( PATTERN_message_with_tail_punctuation_mark); if (matched && (matched[2] in domain)) { prefix = matched[1]; postfix = matched[3]; text_id = matched[2]; } else { using_default = true; } } if (!using_default) { text_id = domain[text_id]; if (prefix) { text_id = convert_punctuation_mark(prefix, domain_name) + text_id; } if (postfix // 預防翻譯後有結尾標點符號,但原文沒有的情況。但這情況其實應該警示。 // && !PATTERN_message_with_tail_punctuation_mark.test(text_id) ) { text_id += convert_punctuation_mark(postfix, domain_name); } } return typeof text_id === 'function' ? text_id(domain_name, value_list, domain_specified) : text_id; } function try_domain(_domain_name, recover) { var original_domain_data = [ domain_name, domain ]; domain_name = _domain_name; // 在不明環境,如 node.js 中執行時,((gettext_texts[domain_name])) 可能為 // undefined。 domain = gettext_texts[domain_name]; if (!domain) { if (false) { // No 強制載入 flag here. library_namespace.warn({ // gettext_config:{"id":"specified-domain-$1-is-not-yet-loaded.-you-may-need-to-set-the-force-flag"} T : [ '所指定之 domain [%1] 尚未載入,若有必要請使用強制載入 flag。', domain_name ] }); } domain = Object.create(null); } var _text = String(convert(library_namespace.is_Object(text_id) ? text_id[domain_name] : text_id)); if (recover) { domain_name = original_domain_data[0]; domain = original_domain_data[1]; } return _text; } var value_list = arguments, length = value_list.length, using_default, // this: 本次轉換之特殊設定。 domain_name = this && this.domain_name || gettext_domain_name, // domain, converted_text = try_domain(domain_name), // 強制轉換/必須轉換 force convert. e.g., 輸入 id,因此不能以 text_id 顯示。 force_convert = using_default && this && (this.force_convert // for DOM || this.getAttribute && this.getAttribute('force_convert')); // 設定 force_convert 時,最好先 `gettext.load_domain(force_convert)` // 以避免最後仍找不到任何一個可用的 domain。 if (force_convert) { // force_convert: fallback_domain_name_list if (!Array.isArray(force_convert)) force_convert = force_convert.split(','); force_convert.some(function(_domain_name) { _domain_name = gettext.to_standard(_domain_name); if (!_domain_name || _domain_name === domain_name) return; var _text = try_domain(_domain_name, true); if (!using_default) { domain_name = _domain_name; converted_text = _text; // using the first matched return true; } }); } library_namespace .debug('Use domain_name: ' + domain_name, 6, 'gettext'); converted_text = adapt_plural(converted_text, value_list, domain_name); if (length <= 1) { // assert: {String}converted_text return converted_text; } var text_list = [], matched, last_index = 0, // 允許 convert 出的結果為 object。 has_object = false, // whole conversion specification: // %% || %index || %domain/index // || %\w(conversion format specifier)\d{1,2}(index) // || %[conversion specifications@]index // // 警告: index 以 "|" 終結,後接數字會被視為 patten 明確終結,並且 "|" 將被吃掉。 // e.g., gettest("%1|123", 321) === "321123" // gettest("%1||123", 321) === "321||123" // TODO: 改成 %{index}, %{var_id} // // @see CeL.extract_literals() // // 採用 local variable,因為可能有 multithreading 的問題。 conversion_pattern = /([\s\S]*?)%(?:(%)|(?:([^%@\s\|\/]+)\/)?(?:([^%@\s\|\d]{1,3})|([^%@\|]+)@)?(\d{1,2})(\|[\|\d])?)/g; while (matched = conversion_pattern.exec(converted_text)) { if (matched[7]) { // 回吐最後一個 \d conversion_pattern.lastIndex--; // conversion_pattern.lastIndex -= matched[7].length // - '|'.length; } last_index = conversion_pattern.lastIndex; // matched: // 0: prefix + conversion, 1: prefix, 2: is_escaped "%", // 3: domain_specified, 4: format, 5: object_name, 6: argument NO, // 7: "|" + \d. var conversion = matched[0]; if (matched[2]) { // e.g., 'prefix%%...' // assert: matched[3] 之後全都沒東西。 text_list.push(conversion); continue; } var NO = +matched[6], format = matched[4], _matched; if (NO < length && (!(format || (format = matched[5])) // 有設定 {String}format 的話,就必須在 gettext.conversion 中。 || (format in gettext.conversion))) { if (NO === 0) { conversion = text_id; } else { var domain_specified = matched[3], // domain_used = domain_specified && gettext_texts[domain_specified]; if (domain_used) { // 避免 %0 形成 infinite loop。 var origin_domain = domain, origin_domain_name = domain_name; library_namespace.debug('臨時改變 domain: ' + domain_name + '→' + domain_specified, 6, 'gettext'); domain_name = domain_specified; domain = domain_used; conversion = convert(value_list[NO], domain_specified); library_namespace.debug('回存/回復 domain: ' + domain_name + '→' + origin_domain_name, 6, 'gettext'); domain_name = origin_domain_name; domain = origin_domain; } else { if (domain_specified) { library_namespace.warn('gettext: ' + 'Unknown domain [' + domain_specified + ']'); } conversion = convert(value_list[NO]); } } if (format) { conversion = Array.isArray(NO = gettext.conversion[format]) // ? gettext_conversion_Array(conversion, NO, format) // assert: gettext.conversion[format] is function : NO(conversion, domain_specified || domain_name); } } else if (format && matched[3] // The same index passern as conversion_pattern && (_matched = matched[3].match(/(\d{1,2})/)) && _matched[0] < length) { // e.g., CeL.gettext('<h1>%1</h1>', 't') format = null; NO = _matched[0]; last_index = // reset. // assert: last_index === matched[0].length // last_index - matched[0].length + // // 加回這次處理的部分。 1: 前導 '%'.length 1 + matched[1].length + NO.length; conversion = convert(value_list[NO]); } else { library_namespace.warn('gettext: ' // + (NO < length ? 'Unknown format [' + format + ']' // : 'given too few arguments: ' + length + ' <= No. ' + NO)); } if (typeof conversion === 'object') { has_object = true; text_list.push(matched[1], conversion); } else { // String(conversion): for Symbol value text_list.push(matched[1] + String(conversion)); } } text_list.push(converted_text.slice(last_index)); return has_object ? text_list : text_list.join(''); } var PATTERN_is_punctuation_mark = /^[,;:.?!~、,;:。?!~]$/; // matched: [ all, header punctuation mark, text_id / message, tail // punctuation mark ] var PATTERN_message_with_tail_punctuation_mark = /^(\.{3,}\s*)?([\s\S]+?)(\.{3,}|…+|:\s*(%\d)?|[,;:.?!~、,;:。?!~])$/; function trim_punctuation_marks(text) { var matched = text.match(PATTERN_message_with_tail_punctuation_mark); return matched ? matched[2] : text; } _.trim_punctuation_marks = trim_punctuation_marks; // ------------------------------------------------------------------------ // 應對多個句子在不同語言下結合時使用。 function Sentence_combination(sentence) { // call super constructor. // Array.call(this); var sentence_combination = this; if (sentence) { if (Array.isArray(sentence) && sentence.every(function(_sentence) { return Array.isArray(_sentence); })) { // e.g., new CeL.gettext.Sentence_combination( // [ [ 'message', p1 ], [ 'message' ] ]) sentence_combination.append(sentence); } else { // e.g., new CeL.gettext.Sentence_combination( // [ 'message', p1, p2 ]) sentence_combination.push(sentence); } } } function deep_convert(text) { if (!Array.isArray(text)) { var converted = gettext(text); if (converted === text && PATTERN_is_punctuation_mark.test(text)) { // e.g., text === ',' converted = convert_punctuation_mark(text, gettext_domain_name); } return converted; } // e.g., [ '%1 elapsed.', ['%1 s', 2] ] var converted = [ text[0] ]; for (var index = 1; index < text.length; index++) { converted[index] = deep_convert(text[index]); } return gettext.apply(null, converted); } function Sentence_combination__converting() { var converted_list = []; this.forEach(function(sentence) { sentence = deep_convert(sentence); if (sentence) converted_list.push(sentence); }); return converted_list; } // @see CeL.data.count_word() // 這些標點符號和下一句中間可以不用接空白字元。 // /[\u4e00-\u9fa5]/: 匹配中文字 RegExp。 // https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block) // https://arc-tech.hatenablog.com/entry/2021/01/20/105620 // e.g., start quote marks var PATTERN_no_need_to_append_tail_space = /[\s—、,;:。?!()[]{}「」『』〔〕【】〖〗〈〉《》“”‘’§(\[<{⟨‹«\u4e00-\u9fffぁ-んーァ-ヶ]$/; // e.g., end quote marks var PATTERN_no_need_to_add_header_space = /^[\s)\]>}⟩›»)]}」』〕】〗〉》”’‰‱]/; function Sentence_combination__join(separator) { // console.trace(this); var converted_list = this.converting(); if (separator || separator === '') return converted_list.join(separator); for (var index = 0; index < converted_list.length;) { var converted = converted_list[index]; // console.trace([ index, converted ]); if (!converted // 要處理首字母大小寫轉換,所以不直接跳出。 // || PATTERN_no_need_to_append_tail_space.test(converted) ) { ++index; continue; } var next_sentence, original_index = index, must_lower_case = /[,;、,;]\s*$/ .test(converted) ? true : /[.?!。?!]\s*$/.test(converted) ? false : undefined; while (++index < converted_list.length) { next_sentence = converted_list[index]; // console.trace([ converted, next_sentence, must_lower_case ]); // 處理首字母大小寫轉換。 if (next_sentence && typeof must_lower_case === 'boolean') { var leading_spaces = next_sentence.match(/^\s+/); if (leading_spaces) { leading_spaces = leading_spaces[0]; next_sentence = next_sentence .slice(leading_spaces.length); } var first_char = next_sentence.charAt(0); if (must_lower_case ^ (first_char === first_char.toLowerCase())) { next_sentence = (must_lower_case ? first_char .toLowerCase() : first_char.toUpperCase()) + next_sentence.slice(1); } if (leading_spaces) { next_sentence = leading_spaces + next_sentence; } converted_list[index] = next_sentence; } // 增加子句間的空格。 // 找出下一個(非空內容的)文字,檢查是否該在本token(converted_list[original_index])結尾加上空白字元。 if (next_sentence || next_sentence === 0) { if (!PATTERN_no_need_to_append_tail_space.test(converted) // && !PATTERN_no_need_to_add_header_space.test(next_sentence)) { converted_list[original_index] += ' '; } break; } } // console.trace([ index, converted_list[index] ]); } converted_list = converted_list.join(''); // TODO: upper-case the first char return converted_list; } // https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Object/create Sentence_combination.prototype // 繼承一個父類別 = Object.assign(Object.create(Array.prototype), { // 重新指定建構式 constructor : Sentence_combination, converting : Sentence_combination__converting, join : Sentence_combination__join, toString : Sentence_combination__join }); /** * @example<code> messages = new gettext.Sentence_combination(); messages.push(message, [ message ], [ message, arg_1, arg_2 ]); messages.toString(); </code> */ gettext.Sentence_combination = Sentence_combination; function append_message_tail_space(text, options) { if (!options || typeof options === 'string' || !options.no_more_convert) { // Treat `options` as an argument to gettext. text = gettext.apply(null, arguments); } if (!text || PATTERN_no_need_to_append_tail_space.test(text)) { return text; } var next_sentence = options && options.next_sentence; return next_sentence && !PATTERN_no_need_to_add_header_space.test(next_sentence) || next_sentence === 0 ? text + ' ' : text; } gettext.append_message_tail_space = append_message_tail_space; // ------------------------------------------------------------------------ // 不改變預設domain,直接取得特定domain的轉換過的文字。 // 警告:需要確保系統相應 domain resources 已載入並設定好。 gettext.in_domain = function(domain_name, text_id) { var options = typeof domain_name === 'object' ? domain_name // : typeof domain_name === 'string' ? { domain_name : gettext.to_standard(domain_name) } : { domain : domain_name }; if (false && Array.isArray(text_id)) { return gettext.apply(options, text_id); } if (arguments.length <= 2) { // 沒有多餘的參數設定(e.g., %1, %2)。 return gettext.call(options, text_id); } var value_list = Array.prototype.slice.call(arguments); value_list.shift(); return gettext.apply(options, value_list); }; /** * 檢查指定資源是否已載入,若已完成,則執行 callback 序列。 * * @param {String}[domain_name] * 設定當前使用之 domain name。 * @param {Integer}[type] * 欲設定已載入/未載入之資源類型。 * @param {Boolean}[is_loaded] * 設定/登記是否尚未載入之資源類型。 * @returns {Boolean} 此 type 是否已 loaded。 */ function gettext_check_resources(domain_name, type, is_loaded) { if (!domain_name) domain_name = gettext_domain_name; var domain = gettext_resource[domain_name]; if (!domain) gettext_resource[domain_name] = domain = Object.create(null); if (type) if (type = [ , 'system', 'user' ][type]) { if (typeof is_loaded === 'boolean') { library_namespace.debug('登記 [' + domain_name + '] 已經載入資源 [' + type + ']。', 2, 'gettext_check_resources'); domain[type] = is_loaded; } } else type = null; return type ? domain[type] : domain; } /** * 當設定 conversion 為 Array 時,將預設採用此 function。<br /> * 可用在單數複數形式 (plural) 之表示上。 * * @param {Integer}amount * 數量。 * @param {Array}conversion * 用來轉換的 Array。 * @param {String}name * format name。 * * @returns {String} 轉換過的文字/句子。 */ function gettext_conversion_Array(amount, conversion_Array, name) { var text, // index used. // TODO: check if amount < 0 or amount is not integer. index = amount < conversion_Array.length ? parseInt(amount) : conversion_Array.length - 1; if (index < 0) { library_namespace.debug({ T : [ 'Negative index: %1', index ] }); index = 1; } else while (index >= 0 && !(text = conversion_Array[index])) index--; if (!text || typeof text !== 'string') { library_namespace.warn({ T : [ 'Nothing matched for amount [%1]', amount ] }); return; } if (name) text = text.replace(/%n/g, name); return text.replace(/%d/g, amount); } /** * 設定如何載入指定 domain resources,如語系檔。 * * @param {String|Function}path * (String) prefix of path to load.<br /> * function(domain){return path to load;} */ gettext.use_domain_location = function(path) { if (typeof path === 'string') { gettext_domain_location = path; // 重設 user domain resources path。 gettext_check_resources('', 2, false); } return gettext_domain_location; }; /** * 取得當前使用之 domain name。 * * @returns 當前使用之 domain name。 */ gettext.get_domain_name = function() { return gettext_domain_name; }; gettext.is_domain_name = function(domain_name) { return gettext_domain_name === gettext.to_standard(domain_name); }; // force: 若 domain name 已經載入過,則再度載入。 function load_domain(domain_name, callback, force) { var do_not_register = domain_name === plural_rules__domain_name; if (!domain_name || !do_not_register && !(domain_name = gettext.to_standard(domain_name))) { // using the default domain name. domain_name = gettext.default_domain; } if (!domain_name || domain_name === gettext_domain_name && !force) { typeof callback === 'function' && callback(domain_name); return; } if (!(domain_name in gettext_texts) && !!do_not_register) { // initialization gettext_texts[domain_name] = Object.create(null); } var need_to_load = []; // TODO: use <a href="http://en.wikipedia.org/wiki/JSONP" // accessdate="2012/9/14 23:50">JSONP</a> if (!gettext_check_resources(domain_name, 1)) { library_namespace.debug('準備載入系統相應 domain resources。', 2, 'gettext'); need_to_load.push(library_namespace.get_module_path(module_name, // resources/ CeL.env.resources_directory_name + '/' + domain_name + '.js'), // function() { if (do_not_register) return; library_namespace.debug('Resources of module included.', 2, 'gettext'); gettext_check_resources(domain_name, 1, true); }); } if (typeof gettext_domain_location === 'function') { gettext_domain_location = gettext_domain_location(); } if (typeof gettext_domain_location === 'string' // && !gettext_check_resources(domain_name, 2)) { library_namespace.debug('準備載入 user 指定 domain resources,如語系檔。', 2, 'gettext'); need_to_load.push(typeof gettext_domain_location === 'string' // 因 same-origin policy,採 .js 而非其他 file type 如 .json。 ? gettext_domain_location + domain_name + '.js' : gettext_domain_location(domain_name), function() { library_namespace.debug('User-defined resources included.', 2, 'gettext'); gettext_check_resources(domain_name, 2, true); }); } if (need_to_load.length > 0) { // console.trace(need_to_load); library_namespace.debug('need_to_load: ' + need_to_load, 2, 'load_domain'); library_namespace.run(need_to_load, typeof callback === 'function' && function() { library_namespace.debug('Running callback...', 2, 'gettext'); callback(domain_name); }); } else { library_namespace.debug('Nothing to load.'); gettext_check_resources(domain_name, 2, true); } } gettext.load_domain = load_domain; /** * 取得/設定當前使用之 domain。 * * @example<code> // for i18n: define gettext() user domain resources path / location. // gettext() will auto load (CeL.env.domain_location + language + '.js'). // e.g., resources/cmn-Hant-TW.js, resources/ja-JP.js CeL.gettext.use_domain_location(module.filename.replace(/[^\\\/]*$/, 'resources' + CeL.env.path_separator)); CeL.gettext.use_domain('GUESS', true); </code> * * @param {String}[domain_name] * 設定當前使用之 domain name。 * @param {Function}[callback] * 回撥函式。 callback(domain_name) * @param {Boolean}[force] * 強制載入 flag。即使尚未載入此 domain,亦設定之並自動載入。但是若 domain name * 已經載入過,則不會再度載入。 * * @returns {Object}當前使用之 domain。 */ function use_domain(domain_name, callback, force) { if (typeof callback === 'boolean' && force === undefined) { // shift 掉 callback。 force = callback; callback = undefined; } if (domain_name === 'GUESS') { domain_name = guess_language(); } if (!domain_name) { domain_name = gettext_texts[gettext_domain_name]; typeof callback === 'function' && callback(domain_name); // return domain used now. return domain_name; } // 查驗 domain_name 是否已載入。 var is_loaded = domain_name in gettext_texts; if (!is_loaded) { is_loaded = gettext.to_standard(domain_name); if (is_loaded) { is_loaded = (domain_name = is_loaded) in gettext_texts; } } if (is_loaded) { gettext_domain_name = domain_name; library_namespace.debug({ // gettext_config:{"id":"$1-is-loaded-setting-up-user-domain-resources-now"} T : [ '已載入過 [%1],直接設定使用者自訂資源。', domain_name ] }, 2, 'gettext.use_domain'); gettext_check_resources(domain_name, 2, true); typeof callback === 'function' && callback(domain_name); } else if (force && domain_name) { if (library_namespace.is_WWW() && library_namespace.is_included('interact.DOM')) { // 顯示使用 domain name 之訊息:此時執行,仍無法改採新 domain 顯示訊息。 library_namespace.debug({ T : [ domain_name === gettext_domain_name // gettext_config:{"id":"force-loading-using-domain-locale-$2-($1)"} ? '強制再次載入/使用 [%2] (%1) 領域/語系。' // gettext_config:{"id":"loading-using-domain-locale-$2-($1)"} : '載入/使用 [%2] (%1) 領域/語系。', domain_name, gettext.get_alias(domain_name) ] }, 1, 'gettext.use_domain'); } else { library_namespace.debug( // re-load (domain_name === gettext_domain_name ? 'FORCE ' : '') + 'Loading/Using domain/locale [' + gettext.get_alias(domain_name) + '] (' + domain_name + ').', 1, 'gettext.use_domain'); } if (!(domain_name in gettext_texts)) { // 為確保回傳的是最終的domain,先初始化。 gettext_texts[domain_name] = Object.create(null); } load_domain(domain_name, function() { gettext_domain_name = domain_name; typeof callback === 'function' && callback(domain_name); }); } else { if (domain_name) { if (domain_name !== gettext_domain_name) { library_namespace.warn({ // gettext_config:{"id":"specified-domain-$1-is-not-yet-loaded.-you-may-need-to-set-the-force-flag"} T : [ '所指定之 domain [%1] 尚未載入,若有必要請使用強制載入 flag。', domain_name ] }); } } else if (typeof callback === 'function' && library_namespace.is_debug()) // gettext_config:{"id":"unable-to-distinguish-domain-but-set-callback"} library_namespace.warn('無法判別 domain,卻設定有 callback。'); // 無論如何還是執行 callback。 typeof callback === 'function' && callback(domain_name); } return gettext_texts[domain_name]; } // using_domain gettext.use_domain = use_domain; function guess_language() { if (library_namespace.is_WWW()) { // http://stackoverflow.com/questions/1043339/javascript-for-detecting-browser-language-preference return gettext.to_standard(navigator.userLanguage || navigator.language // || navigator.languages && navigator.languages[0] // IE 11 || navigator.browserLanguage || navigator.systemLanguage); } function exec(command, PATTERN, mapping) { try { // @see https://gist.github.com/kaizhu256/a4568cb7dac2912fc5ed // synchronously run system command in nodejs <= 0.10.x // https://github.com/gvarsanyi/sync-exec/blob/master/js/sync-exec.js // if (!require('child_process').execSync) { return; } var code = require('child_process').execSync(command, { stdio : 'pipe' }).toString(); // console.trace([ command, code ]); if (PATTERN) code = code.match(PATTERN)[1]; if (mapping) code = mapping[code]; return gettext.to_standard(code); } catch (e) { // TODO: handle exception } } // console.trace(library_namespace.platform.is_Windows()); if (library_namespace.platform.is_Windows()) { // TODO: // `REG QUERY HKLM\System\CurrentControlSet\Control\Nls\Language /v // InstallLanguage` // https://www.lisenet.com/2014/get-windows-system-information-via-wmi-command-line-wmic/ // TODO: `wmic OS get Caption,CSDVersion,OSArchitecture,Version` // require('os').release() return exec( // https://docs.microsoft.com/zh-tw/powershell/module/international/get-winsystemlocale?view=win10-ps 'PowerShell.exe -Command "& {Get-WinSystemLocale | Select-Object LCID}"', /(\d+)[^\d]*$/, guess_language.LCID_mapping) // WMIC is deprecated. // https://stackoverflow.com/questions/1610337/how-can-i-find-the-current-windows-language-from-cmd // get 非 Unicode 應用程式的語言與系統地區設定所定義的語言 || exec('WMIC.EXE OS GET CodeSet', /(\d+)[^\d]*$/, guess_language.code_page_mapping) // using windows active console code page // https://docs.microsoft.com/en-us/windows/console/console-code-pages // CHCP may get 65001, so we do not use this at first. || exec('CHCP', /(\d+)[^\d]*$/, guess_language.code_page_mapping); } /** * <code> @see https://www.itread01.com/content/1546711411.html TODO: detect process.env.TZ: node.js 設定測試環境使用 GreenWich時間 process.env.TZ = 'Europe/London'; timezone = { 'Europe/London' : 0, 'Asia/Shanghai' : -8, 'America/New_York' : 5 }; </code> */ var LANG = library_namespace.env.LANG; // e.g., LANG=zh_TW.Big5 // en_US.UTF-8 if (LANG) return gettext.to_standard(LANG); return exec('locale', /(?:^|\n)LANG=([^\n]+)/); } // https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/chcp guess_language.code_page_mapping = { 437 : 'en-US', 866 : 'ru-RU', 932 : 'ja-JP', 936 : 'cmn-Hans-CN', 949 : 'ko-KR', 950 : 'cmn-Hant-TW', 1256 : 'arb-Arab', 54936 : 'cmn-Hans-CN' // 65001: 'Unicode' }; // https://zh.wikipedia.org/wiki/区域设置#列表 guess_language.LCID_mapping = { 1028 : 'cmn-Hant-TW', 1033 : 'en-US', 1041 : 'ja-JP', 1042 : 'ko-KR', 1049 : 'ru-RU', 2052 : 'cmn-Hans-CN', 2057 : 'en-GB', 3076 : 'cmn-Hant-HK', 14337 : 'arb-Arab' }; gettext.guess_language = guess_language; /** * 設定欲轉換的文字格式。 * * @param {Object}text_Object * 文字格式。 {<br /> * text id : text for this domain }<br /> * 函數以回傳文字格式。 {<br /> * text id : function(domain name){ return text for this domain } } * @param {String}[domain] * 指定存入之 domain。 * @param {Boolean}[clean_and_replace] * 是否直接覆蓋掉原先之 domain。 */ gettext.set_text = function set_text(text_Object, domain, clean_and_replace) { if (!library_namespace.is_Object(text_Object)) return; if (!domain) domain = gettext_domain_name; // normalize domain if (!(domain in gettext_texts)) domain = gettext.to_standard(domain); // console.trace(domain); if (clean_and_replace || !(domain in gettext_texts)) { gettext_texts[domain] = text_Object; } else { // specify a new domain. // gettext_texts[domain] = Object.create(null); // CeL.set_method() 不覆蓋原有的設定。 // library_namespace.set_method(gettext_texts[domain], text_Object); // 覆蓋原有的設定。 Object.assign(gettext_texts[domain], text_Object); } }; // ------------------------------------ /** * 取得 domain 別名。 若欲取得某個語言在其他語言中的名稱,應該設定好i18n,並以gettext()取得。 * * @param {String}[language] * 指定之正規名稱。 * @returns {String} 主要使用之別名。 * @returns {Object} { 正規名稱 : 別名 } */ gettext.get_alias = function(language) { return arguments.length > 0 ? gettext_main_alias[language in gettext_main_alias ? language : gettext.to_standard(language)] : gettext_main_alias; }; /** * 設定 domain 別名。<br /> * 本函數會改變 {Object}list! * * @param {Object}list * full alias list / 別名。 = {<br /> * norm/criterion (IANA language tag) : [<br /> * 主要別名放在首個 (e.g., 當地使用之語言名稱),<br /> * 最常用之 language tag (e.g., IETF language tag),<br /> * 其他別名 / other aliases ] } */ gettext.set_alias = function(list) { if (!library_namespace.is_Object(list)) return; /** {String}normalized domain name */ var norm; /** {String}domain alias */ var alias; /** {Array}domain alias list */ var alias_list, i, l; for (norm in list) { alias_list = list[norm]; if (typeof alias_list === 'string') { alias_list = alias_list.split('|'); } else if (!Array.isArray(alias_list)) { library_namespace.warn([ 'gettext.set_alias: ', { // gettext_config:{"id":"illegal-domain-alias-list-$1"} T : [ 'Illegal domain alias list: [%1]', alias_list ] } ]); continue; } // 加入 norm 本身。 alias_list.push(norm); for (i = 0, l = alias_list.length; i < l; i++) { alias = alias_list[i]; if (!alias) { continue; } library_namespace.debug({ // gettext_config:{"id":"adding-domain-alias-$1-→-$2"} T : [ 'Adding domain alias [%1] → [%2]...', // alias, norm ] }, 2, 'gettext.set_alias'); if (!(norm in gettext_main_alias)) gettext_main_alias[norm] = alias; // 正規化: 不分大小寫, _ → - alias = alias.replace(/_/g, '-').toLowerCase(); alias.split(/-/).forEach(function(token) { if (!gettext_aliases[token]) gettext_aliases[token] = []; if (!gettext_aliases[token].includes(norm)) gettext_aliases[token].push(norm); }); continue; // for fallback while (true) { gettext_aliases[alias] = norm; var index = alias.lastIndexOf('-'); if (index < 1) break; alias = alias.slice(0, index); } } } }; /** * 將 domain 別名正規化,轉為正規/標準名稱。<br /> * to a standard form. normalize_domain_name(). * * TODO: fix CeL.gettext.to_standard('cmn-CN') === * CeL.gettext.to_standard('zh-CN') * * @param {String}alias * 指定之別名。 * @param {Object}[options] * 附加參數/設定選擇性/特殊功能與選項 * * @returns {String} 正規名稱。 * @returns undefined : can't found. */ gettext.to_standard = function to_standard(alias, options) { if (typeof alias !== 'string') return; if (options === true) { options = { get_list : true }; } else { options = library_namespace.setup_options(options); } // 正規化: 不分大小寫, _ → - alias = alias.replace(/_/g, '-').toLowerCase(); var candidates; alias.split(/-/) // 通常越後面的越有特殊性。 .reverse().some(function(token) { if (!gettext_aliases[token]) return; // console.log(token + ': ' + // JSON.stringify(gettext_aliases[token])); if (!candidates) { candidates = gettext_aliases[token]; return; } // 取交集。 candidates = Array.intersection(candidates, // gettext_aliases[token]); // console.log('candidates: ' + JSON.stringify(candidates)); if (candidates.length < 2) { return true; } }); return options.get_list ? candidates ? candidates.clone() : [] : candidates && candidates[0]; var index; // for fallback while (true) { library_namespace.debug({ // gettext_config:{"id":"testing-domain-