├── lib ├── plugin │ ├── compilers │ │ ├── share.coffee │ │ ├── i18n.yml.coffee │ │ ├── i18n.json.coffee │ │ ├── i18n.coffee │ │ ├── package-tap.i18n.coffee │ │ ├── i18n.generic_compiler.coffee │ │ └── project-tap.i18n.coffee │ ├── helpers │ │ ├── helpers.coffee │ │ ├── load_yml.coffee │ │ ├── load_json.coffee │ │ ├── schema-cleaner.coffee │ │ └── compile_step_helpers.coffee │ ├── compiler_configuration.coffee │ └── etc │ │ └── language_names.js ├── tap_i18n │ ├── tap_i18n-helpers.coffee │ ├── tap_i18n-init.coffee │ ├── tap_i18n-server.coffee │ ├── tap_i18n-common.coffee │ └── tap_i18n-client.coffee ├── tap_i18next │ ├── tap_i18next_init.js │ └── tap_i18next-1.7.3.js └── globals.js ├── TODO ├── .gitignore ├── LICENSE ├── .versions ├── package.js ├── ChangeLog └── README.md /lib/plugin/compilers/share.coffee: -------------------------------------------------------------------------------- 1 | share.compilers = {} -------------------------------------------------------------------------------- /lib/plugin/helpers/helpers.coffee: -------------------------------------------------------------------------------- 1 | share.helpers = {} -------------------------------------------------------------------------------- /lib/tap_i18n/tap_i18n-helpers.coffee: -------------------------------------------------------------------------------- 1 | share.helpers = {} -------------------------------------------------------------------------------- /lib/tap_i18n/tap_i18n-init.coffee: -------------------------------------------------------------------------------- 1 | TAPi18n = new TAPi18n() -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | TODO: 2 | 3 | * Add memcache layer to the integral file server 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | .npm 3 | .DS_Store 4 | smart.lock 5 | packages/test-pack-* 6 | public 7 | .test-run-log 8 | -------------------------------------------------------------------------------- /lib/tap_i18next/tap_i18next_init.js: -------------------------------------------------------------------------------- 1 | TAPi18next.init({resStore: {}, fallbackLng: globals.fallback_language, useCookie: false}); 2 | -------------------------------------------------------------------------------- /lib/plugin/compilers/i18n.yml.coffee: -------------------------------------------------------------------------------- 1 | helpers = share.helpers 2 | compilers = share.compilers 3 | compilers.I18nYml = compilers.generic_compiler "yml", helpers.loadYAML 4 | Plugin.registerCompiler 5 | extensions: ["i18n.yml"] 6 | , -> new compilers.I18nYml -------------------------------------------------------------------------------- /lib/plugin/compilers/i18n.json.coffee: -------------------------------------------------------------------------------- 1 | helpers = share.helpers 2 | compilers = share.compilers 3 | compilers.I18nJson = compilers.generic_compiler("json", helpers.loadJSON) 4 | Plugin.registerCompiler 5 | extensions: ["i18n.json"] 6 | , -> new compilers.I18nJson -------------------------------------------------------------------------------- /lib/globals.js: -------------------------------------------------------------------------------- 1 | // The globals object will be accessible to the build plugin, the server and 2 | // the client 3 | 4 | globals = { 5 | fallback_language: "en", 6 | langauges_tags_regex: "([a-z]{2})(-[A-Z]{2})?", 7 | project_translations_domain: "project", 8 | browser_path: "/tap-i18n", 9 | debug: false 10 | }; 11 | -------------------------------------------------------------------------------- /lib/plugin/compilers/i18n.coffee: -------------------------------------------------------------------------------- 1 | path = Npm.require "path" 2 | 3 | compilers = share.compilers 4 | 5 | I18nConfCompiler = compilers.I18nConfCompiler = -> 6 | @processFilesForTarget = (input_files) -> 7 | input_files.forEach (input_file_obj) -> 8 | if input_file_obj.getBasename() is "package-tap.i18n" 9 | compilers.packageTapI18n input_file_obj 10 | 11 | if input_file_obj.getBasename() is "project-tap.i18n" 12 | compilers.projectTapI18n input_file_obj 13 | 14 | return 15 | 16 | return @ 17 | 18 | Plugin.registerCompiler 19 | extensions: ["i18n"] 20 | , -> new I18nConfCompiler -------------------------------------------------------------------------------- /lib/plugin/helpers/load_yml.coffee: -------------------------------------------------------------------------------- 1 | YAML = Npm.require 'yamljs' 2 | helpers = share.helpers 3 | 4 | # loads a yml from input_file_obj 5 | # 6 | # returns undefined if file doesn't exist, null if file is empty, parsed content otherwise 7 | _.extend share.helpers, 8 | loadYAML: (input_file_obj=null) -> 9 | if not input_file_obj? 10 | return undefined 11 | 12 | if not (content_as_string = input_file_obj.getContentsAsString())? 13 | return null 14 | 15 | try 16 | content = YAML.parse content_as_string 17 | catch error 18 | full_input_path = helpers.getFullInputPath input_file_obj 19 | input_file_obj.error 20 | message: "Can't load `#{full_input_path}' YAML", 21 | sourcePath: full_input_path 22 | 23 | throw new Error "Can't load `#{full_input_path}' YAML" 24 | -------------------------------------------------------------------------------- /lib/plugin/helpers/load_json.coffee: -------------------------------------------------------------------------------- 1 | JSON.minify = JSON.minify || Npm.require("node-json-minify") 2 | helpers = share.helpers 3 | 4 | # loads a json from input_file_obj 5 | # 6 | # returns undefined if file doesn't exist, null if file is empty, parsed content otherwise 7 | _.extend share.helpers, 8 | loadJSON: (input_file_obj) -> 9 | if not input_file_obj? 10 | return undefined 11 | 12 | if not (content_as_string = input_file_obj.getContentsAsString())? 13 | return null 14 | 15 | try 16 | content = JSON.parse content_as_string 17 | catch error 18 | full_input_path = helpers.getFullInputPath input_file_obj 19 | input_file_obj.error 20 | message: "Can't load `#{full_input_path}' JSON", 21 | sourcePath: full_input_path 22 | 23 | throw new Error "Can't load `#{full_input_path}' JSON" 24 | -------------------------------------------------------------------------------- /lib/plugin/helpers/schema-cleaner.coffee: -------------------------------------------------------------------------------- 1 | _.extend share.helpers, 2 | buildCleanerForSchema: (schema, human_readable_reference="Obj") -> 3 | return (obj_to_clean) -> 4 | # Cleans in-place (!) 5 | 6 | try 7 | check obj_to_clean, Object 8 | catch e 9 | throw new Error "#{human_readable_reference} has to be an Object" 10 | 11 | # Remove not supported fields 12 | for key of obj_to_clean 13 | if key not of schema 14 | delete obj_to_clean[key] 15 | 16 | # Apply default values 17 | for key, key_def of schema 18 | if not (obj_to_clean[key])? 19 | if (default_value = key_def.defaultValue)? 20 | obj_to_clean[key] = default_value 21 | 22 | # Check types 23 | for key, val of obj_to_clean 24 | try 25 | check val, schema[key].type 26 | catch e 27 | throw new Error "The field #{key} of #{human_readable_reference} has to be of type: #{schema[key].type}" 28 | 29 | return obj_to_clean -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 TAPevents Asia Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@7.10.5 2 | babel-runtime@1.5.1 3 | base64@1.0.12 4 | blaze@2.4.0 5 | blaze-tools@1.1.2 6 | boilerplate-generator@1.7.2 7 | caching-compiler@1.2.2 8 | caching-html-compiler@1.2.0 9 | callback-hook@1.5.1 10 | check@1.4.1 11 | coffeescript@2.4.1 12 | coffeescript-compiler@2.4.1 13 | diff-sequence@1.1.2 14 | dynamic-import@0.7.3 15 | ecmascript@0.16.8 16 | ecmascript-runtime@0.8.1 17 | ecmascript-runtime-client@0.12.1 18 | ecmascript-runtime-server@0.11.0 19 | ejson@1.1.3 20 | fetch@0.1.4 21 | html-tools@1.1.2 22 | htmljs@1.1.0 23 | inter-process-messaging@0.1.1 24 | jquery@1.11.9 25 | logging@1.3.4 26 | meteor@1.11.5 27 | meteorspark:util@0.2.0 28 | modern-browsers@0.1.10 29 | modules@0.20.0 30 | modules-runtime@0.13.1 31 | mongo-id@1.0.8 32 | observe-sequence@1.0.21 33 | ordered-dict@1.1.0 34 | promise@0.12.2 35 | raix:eventemitter@0.1.3 36 | random@1.2.1 37 | react-fast-refresh@0.2.8 38 | reactive-dict@1.3.1 39 | reactive-var@1.0.12 40 | routepolicy@1.1.1 41 | session@1.2.1 42 | spacebars@1.1.0 43 | spacebars-compiler@1.2.1 44 | tap:i18n@2.0.1 45 | templating@1.4.0 46 | templating-compiler@1.4.1 47 | templating-runtime@1.4.0 48 | templating-tools@1.2.0 49 | tracker@1.3.3 50 | typescript@4.9.5 51 | underscore@1.6.1 52 | webapp@1.13.8 53 | webapp-hashing@1.1.1 54 | -------------------------------------------------------------------------------- /lib/plugin/compiler_configuration.coffee: -------------------------------------------------------------------------------- 1 | path = Npm.require "path" 2 | 3 | # Note: same compiler can be used to compile more then one package (at least in v0.9.x) 4 | 5 | share.compiler_configuration = 6 | fallback_language: globals.fallback_language 7 | packages: [] # Each time we compile package-tap.i18n we push "package_name:arch" to this array 8 | templates_registered_for: [] # Each time we register a template we push "package_name:arch" to this array 9 | default_project_conf_inserted_for: [] # Keeps track of the archs we've inserted the default project conf for. 10 | # Default project conf is inserted by the *.i18.json compiler to be used 11 | # in case the project has no project-tap.i18n 12 | project_tap_i18n_loaded_for: [] # Keeps track of the archs we've loaded project_tap_i18n for 13 | 14 | tap_i18n_input_files: [] 15 | registerInputFile: (input_file_obj) -> 16 | full_file_path = path.join input_file_obj.getSourceRoot(), input_file_obj.getPathInPackage() 17 | input_file = "#{input_file_obj.getArch()}:#{full_file_path}" 18 | if input_file in @tap_i18n_input_files 19 | # A new build cycle 20 | @packages = [] 21 | @templates_registered_for = [] 22 | @default_project_conf_inserted_for = [] 23 | @project_tap_i18n_loaded_for = [] 24 | @tap_i18n_input_files = [] 25 | 26 | @tap_i18n_input_files.push(input_file) 27 | -------------------------------------------------------------------------------- /lib/plugin/helpers/compile_step_helpers.coffee: -------------------------------------------------------------------------------- 1 | path = Npm.require "path" 2 | 3 | compiler_configuration = share.compiler_configuration 4 | 5 | _.extend share.helpers, 6 | getCompileStepArchAndPackage: (input_file_obj) -> 7 | "#{input_file_obj.getPackageName()}:#{input_file_obj.getArch()}" 8 | 9 | markAsPackage: (input_file_obj) -> 10 | compiler_configuration.packages.push @getCompileStepArchAndPackage(input_file_obj) 11 | 12 | isPackage: (input_file_obj) -> 13 | @getCompileStepArchAndPackage(input_file_obj) in compiler_configuration.packages 14 | 15 | markProjectI18nLoaded: (input_file_obj) -> 16 | compiler_configuration.project_tap_i18n_loaded_for.push @getCompileStepArchAndPackage(input_file_obj) 17 | 18 | isProjectI18nLoaded: (input_file_obj) -> 19 | @getCompileStepArchAndPackage(input_file_obj) in compiler_configuration.project_tap_i18n_loaded_for 20 | 21 | markDefaultProjectConfInserted: (input_file_obj) -> 22 | compiler_configuration.default_project_conf_inserted_for.push @getCompileStepArchAndPackage(input_file_obj) 23 | 24 | isDefaultProjectConfInserted: (input_file_obj) -> 25 | @getCompileStepArchAndPackage(input_file_obj) in compiler_configuration.default_project_conf_inserted_for 26 | 27 | getFullInputPath: (input_file_obj) -> path.join input_file_obj.getSourceRoot(), input_file_obj.getPathInPackage() 28 | 29 | # archMatches is taken from https://github.com/meteor/meteor/blob/7da5b32d7882b510df8aa2002f891fc4e1ae1126/tools/utils/archinfo.ts#L232 30 | # due to lack of exposure of meteor/tools/utils/archinfo.ts. 31 | # If you find a way to use the original method, please use it and remove this one. 32 | archMatches: (input_file, program) -> 33 | input_file_arch = input_file.getArch() 34 | return input_file_arch.substr(0, program.length) is program and (input_file_arch.length is program.length or input_file_arch.substr(program.length, 1) is ".") -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'tap:i18n', 3 | summary: 'A comprehensive internationalization solution for Meteor', 4 | version: '2.0.1', 5 | git: 'https://github.com/TAPevents/tap-i18n' 6 | }); 7 | 8 | both = ['server', 'client']; 9 | server = 'server'; 10 | client = 'client'; 11 | 12 | Package.onUse(function (api) { 13 | api.versionsFrom("2.2.4"); 14 | 15 | api.use('coffeescript@2.4.1', both); 16 | api.use('underscore@1.0.10', both); 17 | api.use('isobuild:compiler-plugin@1.0.0', both); 18 | 19 | api.use('raix:eventemitter@0.1.1', both); 20 | api.use('meteorspark:util@0.2.0', both); 21 | 22 | api.use('tracker', both); 23 | api.use('session', client); 24 | api.use('jquery@1.0.10', client); 25 | api.use('templating@1.4.0', client); 26 | 27 | api.use("webapp", server); 28 | 29 | // load TAPi18n 30 | api.addFiles('lib/globals.js', both); 31 | 32 | // load and init TAPi18next 33 | api.addFiles('lib/tap_i18next/tap_i18next-1.7.3.js', both); 34 | api.export('TAPi18next'); 35 | api.addFiles('lib/tap_i18next/tap_i18next_init.js', both); 36 | 37 | api.addFiles('lib/tap_i18n/tap_i18n-helpers.coffee', both); 38 | 39 | // We use the bare option since we need TAPi18n in the package level and 40 | // coffee adds vars to all (so without bare all vars are in the file level) 41 | api.addFiles('lib/tap_i18n/tap_i18n-common.coffee', server); 42 | api.addFiles('lib/tap_i18n/tap_i18n-common.coffee', client, {bare: true}); 43 | 44 | api.addFiles('lib/tap_i18n/tap_i18n-server.coffee', server); 45 | api.addFiles('lib/tap_i18n/tap_i18n-client.coffee', client, {bare: true}); 46 | 47 | api.addFiles('lib/tap_i18n/tap_i18n-init.coffee', server); 48 | api.addFiles('lib/tap_i18n/tap_i18n-init.coffee', client, {bare: true}); 49 | 50 | api.export('TAPi18n'); 51 | }); 52 | 53 | Package.registerBuildPlugin({ 54 | name: 'tap-i18n-compiler', 55 | use: ['coffeescript@2.4.1', 'underscore@1.0.10', 'check@1.3.1'], 56 | npmDependencies: { 57 | "node-json-minify": "0.1.3-a", 58 | "yamljs": "0.2.4" 59 | }, 60 | sources: [ 61 | 'lib/globals.js', 62 | 63 | 'lib/plugin/etc/language_names.js', 64 | 65 | 'lib/plugin/compiler_configuration.coffee', 66 | 67 | 'lib/plugin/helpers/helpers.coffee', 68 | 'lib/plugin/helpers/load_json.coffee', 69 | 'lib/plugin/helpers/load_yml.coffee', 70 | 'lib/plugin/helpers/compile_step_helpers.coffee', 71 | 'lib/plugin/helpers/schema-cleaner.coffee', 72 | 73 | 'lib/plugin/compilers/share.coffee', 74 | 'lib/plugin/compilers/i18n.coffee', 75 | 'lib/plugin/compilers/project-tap.i18n.coffee', 76 | 'lib/plugin/compilers/package-tap.i18n.coffee', 77 | 'lib/plugin/compilers/i18n.generic_compiler.coffee', 78 | 'lib/plugin/compilers/i18n.json.coffee', 79 | 'lib/plugin/compilers/i18n.yml.coffee' 80 | ] 81 | }); 82 | -------------------------------------------------------------------------------- /lib/plugin/compilers/package-tap.i18n.coffee: -------------------------------------------------------------------------------- 1 | helpers = share.helpers 2 | compilers = share.compilers 3 | compiler_configuration = share.compiler_configuration 4 | 5 | package_i18n_obj_schema = 6 | translation_function_name: 7 | type: String 8 | defaultValue: "__" 9 | label: "Translation Function Name" 10 | optional: true 11 | helper_name: 12 | type: String 13 | defaultValue: "_" 14 | label: "Helper Name" 15 | optional: true 16 | namespace: 17 | type: String 18 | defaultValue: null 19 | label: "Translations Namespace" 20 | optional: true 21 | 22 | packageI18nObjCleaner = helpers.buildCleanerForSchema(package_i18n_obj_schema, "package-tap.i18n") 23 | 24 | compilers.packageTapI18n = (input_file_obj) -> 25 | compiler_configuration.registerInputFile(input_file_obj) 26 | input_path = helpers.getFullInputPath input_file_obj 27 | 28 | if helpers.isPackage(input_file_obj) 29 | input_file_obj.error 30 | message: "More than one package-tap.i18n found for package: #{input_file_obj.getPackageName()}", 31 | sourcePath: input_path 32 | return 33 | 34 | if helpers.isProjectI18nLoaded(input_file_obj) 35 | input_file_obj.error 36 | message: "Can't compile package-tap.i18n if project-tap.i18n is present", 37 | sourcePath: input_path 38 | return 39 | 40 | if helpers.isDefaultProjectConfInserted(input_file_obj) 41 | input_file_obj.error 42 | message: "package-tap.i18n should be loaded before languages files (*.i18n.json)", 43 | sourcePath: input_path 44 | return 45 | 46 | helpers.markAsPackage(input_file_obj) 47 | 48 | package_tap_i18n = helpers.loadJSON input_file_obj 49 | 50 | if not package_tap_i18n? 51 | package_tap_i18n = packageI18nObjCleaner({}) 52 | else 53 | packageI18nObjCleaner(package_tap_i18n) 54 | 55 | package_name = input_file_obj.getPackageName() 56 | 57 | if not package_tap_i18n.namespace? 58 | package_tap_i18n.namespace = package_name 59 | 60 | namespace = package_tap_i18n.namespace 61 | 62 | package_i18n_js_file = 63 | """ 64 | TAPi18n.packages["#{package_name}"] = #{JSON.stringify(package_tap_i18n)}; 65 | 66 | // define package's translation function (proxy to the i18next) 67 | #{package_tap_i18n.translation_function_name} = TAPi18n._getPackageI18nextProxy("#{namespace}"); 68 | 69 | """ 70 | 71 | if helpers.archMatches input_file_obj, "web" 72 | package_i18n_js_file += 73 | """ 74 | // define the package's templates registrar 75 | registerI18nTemplate = TAPi18n._getRegisterHelpersProxy("#{package_name}"); 76 | registerTemplate = registerI18nTemplate; // XXX OBSOLETE, kept for backward compatibility will be removed in the future 77 | 78 | // Record the list of templates prior to package load 79 | var _ = Package.underscore._; 80 | non_package_templates = _.keys(Template); 81 | 82 | """ 83 | 84 | return input_file_obj.addJavaScript 85 | path: "package-i18n.js", 86 | sourcePath: input_path, 87 | data: package_i18n_js_file, 88 | bare: false 89 | -------------------------------------------------------------------------------- /lib/tap_i18n/tap_i18n-server.coffee: -------------------------------------------------------------------------------- 1 | _.extend TAPi18n.prototype, 2 | server_translators: null 3 | 4 | _registerServerTranslator: (lang_tag, package_name) -> 5 | if @_enabled() 6 | if not(lang_tag of @server_translators) 7 | @server_translators[lang_tag] = @_getSpecificLangTranslator(lang_tag) 8 | 9 | # fallback language is integrated, and isn't part of @translations 10 | if lang_tag != @_fallback_language 11 | @addResourceBundle(lang_tag, package_name, @translations[lang_tag][package_name]) 12 | 13 | if not(@_fallback_language of @server_translators) 14 | @server_translators[@_fallback_language] = @_getSpecificLangTranslator(@_fallback_language) 15 | 16 | _registerAllServerTranslators: () -> 17 | for lang_tag in @_getProjectLanguages() 18 | for package_name of @translations[lang_tag] 19 | @_registerServerTranslator(lang_tag, package_name) 20 | 21 | _getPackageI18nextProxy: (package_name) -> 22 | # A proxy to TAPi18next.t where the namespace is preset to the package's 23 | (key, options, lang_tag=null) => 24 | if not lang_tag? 25 | # translate to fallback_language 26 | return @server_translators[@_fallback_language] "#{@_getPackageDomain(package_name)}:#{key}", options 27 | else if not(lang_tag of @server_translators) 28 | console.log "Warning: language #{lang_tag} is not supported in this project, fallback language (#{@_fallback_language})" 29 | return @server_translators[@_fallback_language] "#{@_getPackageDomain(package_name)}:#{key}", options 30 | else 31 | return @server_translators[lang_tag] "#{@_getPackageDomain(package_name)}:#{key}", options 32 | 33 | _registerHTTPMethod: -> 34 | self = @ 35 | 36 | methods = {} 37 | 38 | if not self._enabled() 39 | throw new Meteor.Error 500, "tap-i18n has to be enabled in order to register the HTTP method" 40 | 41 | base_route = "#{self.conf.i18n_files_route.replace(/\/$/, "")}" 42 | 43 | multi_lang_route = "#{base_route}/multi/" 44 | multi_lang_regex = new RegExp "^((#{globals.langauges_tags_regex},)*#{globals.langauges_tags_regex}|all)\\.json(\\?.*)?$" 45 | WebApp.connectHandlers.use (req, res, next) -> 46 | if not req.url.startsWith(multi_lang_route) 47 | next() 48 | 49 | return 50 | 51 | langs = req.url.replace multi_lang_route, "" 52 | if not multi_lang_regex.test langs 53 | res.writeHead 401 54 | res.end("tap:i18n: multi language route: couldn't process url: `#{req.url}'; Couldn't parse lang portion of route: `#{langs}'") 55 | return 56 | 57 | # If all lang is requested, return all. 58 | if (langs = langs.replace /\.json\??.*/, "", "") is "all" 59 | res.writeHead 200, 60 | "Content-Type": "text/plain; charset=utf-8" 61 | "Access-Control-Allow-Origin": "*" 62 | res.end JSON.stringify self.translations, "utf8" 63 | return 64 | 65 | output = {} 66 | lang_tags = langs.split "," 67 | for lang_tag in lang_tags 68 | if lang_tag in self._getProjectLanguages() and lang_tag isnt self._fallback_language 69 | if (language_translations = self.translations[lang_tag])? 70 | output[lang_tag] = language_translations 71 | 72 | res.writeHead 200, 73 | "Content-Type": "text/plain; charset=utf-8" 74 | "Access-Control-Allow-Origin": "*" 75 | res.end JSON.stringify output, "utf8" 76 | 77 | return 78 | 79 | single_lang_route = "#{base_route}/" 80 | single_lang_regex = new RegExp "^#{globals.langauges_tags_regex}.json(\\?.*)?$" 81 | WebApp.connectHandlers.use (req, res, next) -> 82 | if not req.url.startsWith(single_lang_route) 83 | next() 84 | 85 | return 86 | 87 | lang = req.url.replace single_lang_route, "" 88 | if not single_lang_regex.test lang 89 | res.writeHead 401 90 | res.end("tap:i18n: single language route: couldn't process url: #{req.url}") 91 | return 92 | lang_tag = lang.replace /\.json\??.*/, "" 93 | 94 | if (lang_tag not in self._getProjectLanguages()) or (lang_tag is self._fallback_language) 95 | res.writeHead 404 96 | res.end() 97 | return 98 | 99 | language_translations = self.translations[lang_tag] or {} 100 | # returning {} if lang_tag is not in translations allows the project 101 | # developer to force a language supporte with project-tap.i18n's 102 | # supported_languages property, even if that language has no lang 103 | # files. 104 | res.writeHead 200, 105 | "Content-Type": "text/plain; charset=utf-8" 106 | "Access-Control-Allow-Origin": "*" 107 | res.end JSON.stringify language_translations, "utf8" 108 | 109 | return 110 | 111 | _onceEnabled: -> 112 | @_registerAllServerTranslators() -------------------------------------------------------------------------------- /lib/plugin/etc/language_names.js: -------------------------------------------------------------------------------- 1 | language_names = { 2 | "af": ["Afrikaans","Afrikaans"], 3 | "ak": ["Akan","Akan"], 4 | "sq": ["Albanian","Shqip"], 5 | "am": ["Amharic","አማርኛ"], 6 | "ar": ["Arabic","العربية"], 7 | "hy": ["Armenian","Հայերեն"], 8 | "rup": ["Aromanian","Armãneashce"], 9 | "as": ["Assamese","অসমীয়া"], 10 | "az": ["Azerbaijani","Azərbaycan dili"], 11 | "az-TR": ["Azerbaijani (Turkey)","Azərbaycan Türkcəsi"], 12 | "ba": ["Bashkir","башҡорт теле"], 13 | "be": ["Belarusian","Беларуская мова"], 14 | "eu": ["Basque","Euskara"], 15 | "bel": ["Belarusian","Беларуская мова"], 16 | "bn": ["Bengali","বাংলা"], 17 | "bs": ["Bosnian","Bosanski"], 18 | "bg": ["Bulgarian","Български"], 19 | "mya": ["Burmese","ဗမာစာ"], 20 | "ca": ["Catalan","Català"], 21 | "bal": ["Catalan (Balear)","Català (Balear)"], 22 | "zh": ["Chinese","中文"], 23 | "zh-CN": ["Chinese (China)","简体中文"], 24 | "zh-HK": ["Chinese (Hong Kong)","繁體中文(香港)"], 25 | "zh-TW": ["Chinese (Taiwan)","繁體中文(台灣)"], 26 | "co": ["Corsican","corsu"], 27 | "hr": ["Croatian","Hrvatski"], 28 | "cs": ["Czech","čeština‎"], 29 | "da": ["Danish","Dansk"], 30 | "nl": ["Dutch","Nederlands"], 31 | "nl-BE": ["Dutch (Belgium)","Nederlands (België)"], 32 | "en": ["English","English"], 33 | "en-AU": ["English (Australia)","English (Australia)"], 34 | "en-CA": ["English (Canada)","English (Canada)"], 35 | "en-GB": ["English (UK)","English (UK)"], 36 | "eo": ["Esperanto","Esperanto"], 37 | "et": ["Estonian","Eesti"], 38 | "fo": ["Faroese","føroyskt"], 39 | "fi": ["Finnish","Suomi"], 40 | "fr-BE": ["French (Belgium)","Français de Belgique"], 41 | "fr": ["French (France)","Français"], 42 | "fy": ["Frisian","Frysk"], 43 | "fuc": ["Fulah","Pulaar"], 44 | "gl": ["Galician","Galego"], 45 | "ka": ["Georgian","ქართული"], 46 | "de": ["German","Deutsch"], 47 | "el": ["Greek","Ελληνικά"], 48 | "gn": ["Guaraní","Avañe'ẽ"], 49 | "haw": ["Hawaiian","Ōlelo Hawaiʻi"], 50 | "haz": ["Hazaragi","هزاره گی"], 51 | "he": ["Hebrew","עברית"], 52 | "hi": ["Hindi","हिन्दी"], 53 | "hu": ["Hungarian","Magyar"], 54 | "is": ["Icelandic","Íslenska"], 55 | "id": ["Indonesian","Bahasa Indonesia"], 56 | "ga": ["Irish","Gaelige"], 57 | "it": ["Italian","Italiano"], 58 | "ja": ["Japanese","日本語"], 59 | "jv": ["Javanese","Basa Jawa"], 60 | "kn": ["Kannada","ಕನ್ನಡ"], 61 | "kk": ["Kazakh","Қазақ тілі"], 62 | "km": ["Khmer","ភាសាខ្មែរ"], 63 | "rw": ["Kinyarwanda","Kinyarwanda"], 64 | "ky": ["Kirghiz","кыргыз тили"], 65 | "ko": ["Korean","한국어"], 66 | "ckb": ["Kurdish (Sorani)","كوردی‎"], 67 | "lo": ["Lao","ພາສາລາວ"], 68 | "lv": ["Latvian","latviešu valoda"], 69 | "li": ["Limburgish","Limburgs"], 70 | "lt": ["Lithuanian","Lietuvių kalba"], 71 | "lb": ["Luxembourgish","Lëtzebuergesch"], 72 | "mk": ["Macedonian","македонски јазик"], 73 | "mg": ["Malagasy","Malagasy"], 74 | "ms": ["Malay","Bahasa Melayu"], 75 | "ml": ["Malayalam","മലയാളം"], 76 | "mr": ["Marathi","मराठी"], 77 | "xmf": ["Mingrelian","მარგალური ნინა"], 78 | "mn": ["Mongolian","Монгол"], 79 | "me": ["Montenegrin","Crnogorski jezik"], 80 | "ne": ["Nepali","नेपाली"], 81 | "nb": ["Norwegian (Bokmål)","Norsk bokmål"], 82 | "nn": ["Norwegian (Nynorsk)","Norsk nynorsk"], 83 | "os": ["Ossetic","Ирон"], 84 | "ps": ["Pashto","پښتو"], 85 | "fa": ["Persian","فارسی"], 86 | "fa-AF": ["Persian (Afghanistan)","فارسی (افغانستان"], 87 | "pl": ["Polish","Polski"], 88 | "pt-BR": ["Portuguese (Brazil)","Português do Brasil"], 89 | "pt-PT": ["Portuguese (Portugal)","Português de Portugal"], 90 | "yi": ["Yiddish","ייִדיש"], 91 | "pt": ["Portuguese","Português"], 92 | "pa": ["Punjabi","ਪੰਜਾਬੀ"], 93 | "rhg": ["Rohingya","Rohingya"], 94 | "ro": ["Romanian","Română"], 95 | "ru": ["Russian","Русский"], 96 | "rue": ["Rusyn","Русиньскый"], 97 | "sah": ["Sakha","Sakha"], 98 | "sa-IN": ["Sanskrit","भारतम्"], 99 | "srd": ["Sardinian","sardu"], 100 | "gd": ["Scottish Gaelic","Gàidhlig"], 101 | "sr": ["Serbian","Српски језик"], 102 | "sd": ["Sindhi","سندھ"], 103 | "si": ["Sinhala","සිංහල"], 104 | "sk": ["Slovak","Slovenčina"], 105 | "sl": ["Slovenian","slovenščina"], 106 | "so": ["Somali","Afsoomaali"], 107 | "azb": ["South Azerbaijani","گؤنئی آذربایجان"], 108 | "es-AR": ["Spanish (Argentina)","Español de Argentina"], 109 | "es-CL": ["Spanish (Chile)","Español de Chile"], 110 | "es-CO": ["Spanish (Colombia)","Español de Colombia"], 111 | "es-MX": ["Spanish (Mexico)","Español de México"], 112 | "es-PE": ["Spanish (Peru)","Español de Perú"], 113 | "es-PR": ["Spanish (Puerto Rico)","Español de Puerto Rico"], 114 | "es": ["Spanish (Spain)","Español"], 115 | "es-VE": ["Spanish (Venezuela)","Español de Venezuela"], 116 | "st": ["Santomean","Sãotomense"], 117 | "su": ["Sundanese","Basa Sunda"], 118 | "sw": ["Swahili","Kiswahili"], 119 | "sv": ["Swedish","Svenska"], 120 | "gsw": ["Swiss German","Schwyzerdütsch"], 121 | "tl": ["Tagalog","Tagalog"], 122 | "tg": ["Tajik","тоҷикӣ"], 123 | "ta": ["Tamil","தமிழ்"], 124 | "ta-LK": ["Tamil (Sri Lanka)","தமிழ்"], 125 | "tt": ["Tatar","Татар теле"], 126 | "te": ["Telugu","తెలుగు"], 127 | "th": ["Thai","ไทย"], 128 | "bo": ["Tibetan","བོད་སྐད"], 129 | "tir": ["Tigrinya","ትግርኛ"], 130 | "tr": ["Turkish","Türkçe"], 131 | "tuk": ["Turkmen","Türkmençe"], 132 | "ua": ["Ukrainian","Українська"], 133 | "ug": ["Uighur","Uyƣurqə"], 134 | "uk": ["Ukrainian","Українська"], 135 | "ur": ["Urdu","اردو"], 136 | "uz": ["Uzbek","O‘zbekcha"], 137 | "vi": ["Vietnamese","Tiếng Việt"], 138 | "wa": ["Walloon","Walon"], 139 | "cy": ["Welsh","Cymraeg"] 140 | }; 141 | -------------------------------------------------------------------------------- /lib/tap_i18n/tap_i18n-common.coffee: -------------------------------------------------------------------------------- 1 | fallback_language = globals.fallback_language 2 | 3 | TAPi18n = -> 4 | EventEmitter.call @ 5 | 6 | @_fallback_language = fallback_language 7 | 8 | @_language_changed_tracker = new Tracker.Dependency 9 | 10 | @_loaded_languages = [fallback_language] # stores the loaded languages, the fallback language is loaded automatically 11 | 12 | @conf = null # If conf isn't null we assume that tap:i18n is enabled for the project. 13 | # We assume conf is valid, we sterilize and validate it during the build process. 14 | 15 | @packages = {} # Stores the packages' package-tap.i18n jsons 16 | 17 | @languages_names = {} # Stores languages that we've found languages files for in the project dir. 18 | # format: 19 | # { 20 | # lang_tag: [lang_name_in_english, lang_name_in_local_language] 21 | # } 22 | 23 | @translations = {} # Stores the packages/project translations - Server side only 24 | # fallback_language translations are not stored here 25 | 26 | 27 | if Meteor.isClient 28 | Session.set @_loaded_lang_session_key, null 29 | 30 | @_languageSpecificTranslators = {} 31 | @_languageSpecificTranslatorsTrackers = {} 32 | 33 | if Meteor.isServer 34 | @server_translators = {} 35 | 36 | Meteor.startup => 37 | # If tap-i18n is enabled for that project 38 | if @_enabled() 39 | @_registerHTTPMethod() 40 | 41 | @__ = @_getPackageI18nextProxy(globals.project_translations_domain) 42 | 43 | TAPi18next.setLng fallback_language 44 | 45 | return @ 46 | 47 | Util.inherits TAPi18n, EventEmitter 48 | 49 | _.extend TAPi18n.prototype, 50 | _loaded_lang_session_key: "TAPi18n::loaded_lang" 51 | 52 | _enable: (conf) -> 53 | # tap:i18n gets enabled for a project once a conf file is set for it. 54 | # It can be either a conf object that was set by project-tap.i18n file or 55 | # a default conf, which is being added if the project has lang files 56 | # (*.i18n.json) but not project-tap.i18n 57 | @conf = conf 58 | 59 | @._onceEnabled() 60 | 61 | _onceEnabled: () -> 62 | # The arch specific code can use this for procedures that should be performed once 63 | # tap:i18n gets enabled (project conf file is being set) 64 | return 65 | 66 | _enabled: -> 67 | # read the comment of @conf 68 | @conf? 69 | 70 | _getPackageDomain: (package_name) -> 71 | package_name.replace(/:/g, "-") 72 | 73 | addResourceBundle: (lang_tag, package_name, translations) -> 74 | TAPi18next.addResourceBundle(lang_tag, @_getPackageDomain(package_name), translations) 75 | 76 | _getSpecificLangTranslator: (lang) -> 77 | current_lang = TAPi18next.lng() 78 | 79 | translator = null 80 | TAPi18next.setLng lang, {fixLng: true}, (lang_translator) => 81 | translator = lang_translator 82 | 83 | # Restore i18next lang that had been changed in the process of generating 84 | # lang specific translator 85 | TAPi18next.setLng current_lang 86 | 87 | return translator 88 | 89 | _getProjectLanguages: () -> 90 | # Return an array of languages available for the current project 91 | if @._enabled() 92 | if _.isArray @.conf.supported_languages 93 | return _.union([@._fallback_language], @.conf.supported_languages) 94 | else 95 | # If supported_languages is null, all the languages we found 96 | # translations files to in the project level are considered supported. 97 | # We use the @.languages_names array to tell which languages we found 98 | # since for every i18n.json file we found in the project level we add 99 | # an entry for its language to @.languages_names in the build process. 100 | # 101 | # We also know for certain that when tap-i18n is enabled the fallback 102 | # lang is in @.languages_names 103 | return _.keys @.languages_names 104 | else 105 | return [@._fallback_language] 106 | 107 | getLanguages: -> 108 | if not @._enabled() 109 | return null 110 | 111 | languages = {} 112 | for lang_tag in @._getProjectLanguages() 113 | languages[lang_tag] = 114 | name: @.languages_names[lang_tag][1] 115 | en: @.languages_names[lang_tag][0] 116 | 117 | languages 118 | 119 | _cdn: (path) -> path 120 | setCdnCb: (cb) -> 121 | @_cdn = cb 122 | return 123 | 124 | _loadLangFileObject: (language_tag, data) -> 125 | for package_name, package_keys of data 126 | # Translations that are added by loadTranslations() have higher priority 127 | package_keys = _.extend({}, package_keys, @_loadTranslations_cache[language_tag]?[package_name] or {}) 128 | 129 | @addResourceBundle(language_tag, package_name, package_keys) 130 | 131 | _loadTranslations_cache: {} 132 | loadTranslations: (translations, namespace) -> 133 | project_languages = @_getProjectLanguages() 134 | 135 | for language_tag, translation_keys of translations 136 | if not @_loadTranslations_cache[language_tag]? 137 | @_loadTranslations_cache[language_tag] = {} 138 | 139 | if not @_loadTranslations_cache[language_tag][namespace]? 140 | @_loadTranslations_cache[language_tag][namespace] = {} 141 | 142 | _.extend(@_loadTranslations_cache[language_tag][namespace], translation_keys) 143 | 144 | @addResourceBundle(language_tag, namespace, translation_keys) 145 | 146 | if Meteor.isClient and @getLanguage() == language_tag 147 | # Retranslate if session language updated 148 | @_language_changed_tracker.changed() -------------------------------------------------------------------------------- /lib/plugin/compilers/i18n.generic_compiler.coffee: -------------------------------------------------------------------------------- 1 | path = Npm.require "path" 2 | 3 | helpers = share.helpers 4 | compilers = share.compilers 5 | compiler_configuration = share.compiler_configuration 6 | 7 | compilers.generic_compiler = (extension, helper) -> 8 | GenericCompiler = -> 9 | @processFilesForTarget = (input_files) -> 10 | input_files.forEach (input_file) -> 11 | compiler_configuration.registerInputFile input_file 12 | 13 | input_path = helpers.getFullInputPath input_file 14 | language = path.basename(input_path).split(".").slice(0, -2).pop() 15 | if _.isUndefined(language) or _.isEmpty(language) 16 | input_file.error 17 | message: "Language-tag is not specified for *.i18n.`#{extension}' file: `#{input_path}'", 18 | sourcePath: input_path 19 | return 20 | 21 | if not RegExp("^#{globals.langauges_tags_regex}$").test(language) 22 | input_file.error 23 | message: "Can't recognise '#{language}' as a language-tag: `#{input_path}'", 24 | sourcePath: input_path 25 | return 26 | 27 | translations = helper input_file 28 | 29 | package_name = if helpers.isPackage(input_file) then input_file.getPackageName() else globals.project_translations_domain 30 | output = 31 | """ 32 | var _ = Package.underscore._, 33 | package_name = "#{package_name}", 34 | namespace = "#{package_name}"; 35 | 36 | if (package_name != "#{globals.project_translations_domain}") { 37 | namespace = TAPi18n.packages[package_name].namespace; 38 | } 39 | 40 | """ 41 | 42 | # only for project 43 | if not helpers.isPackage(input_file) 44 | if /^(client|server)/.test input_file.getPathInPackage() 45 | input_file.error 46 | message: "Languages files should be common to the server and the client. Do not put them under /client or /server .", 47 | sourcePath: input_path 48 | return 49 | 50 | # add the language names to TAPi18n.languages_names 51 | language_name = [language, language] 52 | if language_names[language]? 53 | language_name = language_names[language] 54 | 55 | if language != globals.fallback_language 56 | # the name for the fallback_language is part of the getProjectConfJs()'s output 57 | output += 58 | """ 59 | TAPi18n.languages_names["#{language}"] = #{JSON.stringify language_name}; 60 | 61 | """ 62 | 63 | # If this is a project but project-tap.i18n haven't compiled yet add default project conf 64 | # for case there is no project-tap.i18n defined in this project. 65 | # Reminder: we don't require projects to have project-tap.i18n 66 | if not(helpers.isDefaultProjectConfInserted(input_file)) and \ 67 | not(helpers.isProjectI18nLoaded(input_file)) 68 | output += share.getProjectConfJs(share.projectI18nObjCleaner({})) # defined in project-tap.i18n.coffee 69 | 70 | helpers.markDefaultProjectConfInserted(input_file) 71 | 72 | 73 | # if fallback_language -> integrate, otherwise add to TAPi18n.translations if server arch. 74 | if language == compiler_configuration.fallback_language 75 | output += 76 | """ 77 | // integrate the fallback language translations 78 | translations = {}; 79 | translations[namespace] = #{JSON.stringify translations}; 80 | TAPi18n._loadLangFileObject("#{compiler_configuration.fallback_language}", translations); 81 | 82 | """ 83 | 84 | if helpers.archMatches input_file, "os" 85 | if language != compiler_configuration.fallback_language 86 | output += 87 | """ 88 | if(_.isUndefined(TAPi18n.translations["#{language}"])) { 89 | TAPi18n.translations["#{language}"] = {}; 90 | } 91 | 92 | if(_.isUndefined(TAPi18n.translations["#{language}"][namespace])) { 93 | TAPi18n.translations["#{language}"][namespace] = {}; 94 | } 95 | 96 | _.extend(TAPi18n.translations["#{language}"][namespace], #{JSON.stringify translations}); 97 | 98 | """ 99 | 100 | output += 101 | """ 102 | TAPi18n._registerServerTranslator("#{language}", namespace); 103 | 104 | """ 105 | 106 | # register i18n helper for templates, only once per web arch, only for packages 107 | if helpers.isPackage input_file 108 | if helpers.archMatches(input_file, "web") and helpers.getCompileStepArchAndPackage(input_file) not in compiler_configuration.templates_registered_for 109 | output += 110 | """ 111 | var package_templates = _.difference(_.keys(Template), non_package_templates); 112 | 113 | for (var i = 0; i < package_templates.length; i++) { 114 | var package_template = package_templates[i]; 115 | 116 | registerI18nTemplate(package_template); 117 | } 118 | 119 | """ 120 | compiler_configuration.templates_registered_for.push helpers.getCompileStepArchAndPackage(input_file) 121 | 122 | output_path = input_file.getPathInPackage().replace new RegExp("`#{extension}'$"), "js" 123 | input_file.addJavaScript 124 | path: output_path, 125 | sourcePath: input_path, 126 | data: output, 127 | bare: false 128 | 129 | return @ 130 | 131 | return GenericCompiler 132 | -------------------------------------------------------------------------------- /lib/plugin/compilers/project-tap.i18n.coffee: -------------------------------------------------------------------------------- 1 | helpers = share.helpers 2 | compilers = share.compilers 3 | compiler_configuration = share.compiler_configuration 4 | 5 | project_i18n_obj_schema = 6 | helper_name: 7 | type: String 8 | defaultValue: "_" 9 | label: "Helper Name" 10 | supported_languages: 11 | type: [String] 12 | label: "Supported Languages" 13 | defaultValue: null 14 | i18n_files_route: 15 | type: String 16 | label: "Unified languages files path" 17 | defaultValue: globals.browser_path 18 | preloaded_langs: 19 | type: [String] 20 | label: "Preload languages" 21 | defaultValue: [] 22 | 'preloaded_langs.$': 23 | type: String 24 | cdn_path: 25 | type: String 26 | label: "[OBSOLETE] Unified languages files path on CDN" 27 | defaultValue: null 28 | 29 | share.projectI18nObjCleaner = projectI18nObjCleaner = helpers.buildCleanerForSchema(project_i18n_obj_schema, "project-tap.i18n") 30 | 31 | getProjectConfJs = share.getProjectConfJs = (conf) -> 32 | fallback_language_name = language_names[globals.fallback_language] 33 | 34 | project_conf_js = """ 35 | TAPi18n._enable(#{JSON.stringify(conf)}); 36 | TAPi18n.languages_names["#{globals.fallback_language}"] = #{JSON.stringify fallback_language_name}; 37 | 38 | """ 39 | 40 | # If we get a list of supported languages we must make sure that we'll have a 41 | # language name for each one of its languages. 42 | # 43 | # Though languages names are added for every language we find i18n.json file 44 | # for (by the i18n.json compiler). We shouldn't rely on the existence of 45 | # *.i18n.json file for each supported language, because a language might be 46 | # defined as supported even when it has no i18n.json files (it's especially 47 | # true when tap:i18n is used with tap:i18n-db) 48 | if conf.supported_languages? 49 | for lang_tag in conf.supported_languages 50 | if language_names[lang_tag]? 51 | project_conf_js += """ 52 | TAPi18n.languages_names["#{lang_tag}"] = #{JSON.stringify language_names[lang_tag]}; 53 | 54 | """ 55 | 56 | return project_conf_js 57 | 58 | compilers.projectTapI18n = (input_file_obj) -> 59 | compiler_configuration.registerInputFile input_file_obj 60 | input_path = helpers.getFullInputPath input_file_obj 61 | 62 | if helpers.isPackage input_file_obj 63 | input_file_obj.error 64 | message: "Can't load project-tap.i18n in a package: #{input_file_obj.packageName}", 65 | sourcePath: input_path 66 | return 67 | 68 | if helpers.isProjectI18nLoaded input_file_obj 69 | input_file_obj.error 70 | message: "Can't have more than one project-tap.i18n", 71 | sourcePath: input_path 72 | return 73 | 74 | project_tap_i18n = helpers.loadJSON input_file_obj 75 | 76 | if not project_tap_i18n? 77 | project_tap_i18n = projectI18nObjCleaner({}) 78 | else 79 | projectI18nObjCleaner project_tap_i18n 80 | 81 | if project_tap_i18n.cdn_path? 82 | console.warn "As of version v1.11.0 of tap:i18n we no longer support the cdn_path option in project.tap.i18n please refer to our README on: https://github.com/TAPevents/tap-i18n to learn how to setup your CDN" 83 | 84 | project_i18n_js_file = getProjectConfJs project_tap_i18n 85 | 86 | if helpers.archMatches input_file_obj, "web" 87 | preloaded_langs = ["all"] 88 | if project_tap_i18n.preloaded_langs[0] != "*" 89 | preloaded_langs = project_tap_i18n.preloaded_langs 90 | 91 | project_i18n_js_file += """ 92 | var project_preloaded_langs = #{JSON.stringify preloaded_langs}; 93 | 94 | // The following code is generated from this coffeescript code: 95 | // if TAP_I18N_PRELOADED_LANGS? 96 | // if not _.isArray TAP_I18N_PRELOADED_LANGS 97 | // console.error("tap-i18n: An invalid TAP_I18N_PRELOADED_LANGS encountered, skipping.") 98 | // else 99 | // alpha_numeric_regex = /^[a-z\-0-9]+$/i 100 | 101 | // is_runtime_preloaded_langs_valid = _.every TAP_I18N_PRELOADED_LANGS, (lang_tag) -> 102 | // return (lang_tag.length <= 10) and _.isString(lang_tag) and alpha_numeric_regex.test lang_tag 103 | 104 | // if not is_runtime_preloaded_langs_valid 105 | // console.error("tap-i18n: An invalid TAP_I18N_PRELOADED_LANGS encountered, skipping.") 106 | // else 107 | // runtime_preloaded_langs = TAP_I18N_PRELOADED_LANGS 108 | var runtime_preloaded_langs = []; 109 | 110 | if (typeof TAP_I18N_PRELOADED_LANGS !== "undefined" && TAP_I18N_PRELOADED_LANGS !== null) { 111 | if (!_.isArray(TAP_I18N_PRELOADED_LANGS)) { 112 | console.error("tap-i18n: An invalid TAP_I18N_PRELOADED_LANGS encountered, skipping."); 113 | } else { 114 | var alpha_numeric_regex = /^[a-z\-0-9]+$/i; 115 | is_runtime_preloaded_langs_valid = _.every(TAP_I18N_PRELOADED_LANGS, function(lang_tag) { 116 | return (lang_tag.length <= 10) && _.isString(lang_tag) && alpha_numeric_regex.test(lang_tag); 117 | }); 118 | if (!is_runtime_preloaded_langs_valid) { 119 | console.error("tap-i18n: An invalid TAP_I18N_PRELOADED_LANGS encountered, skipping."); 120 | } else { 121 | runtime_preloaded_langs = TAP_I18N_PRELOADED_LANGS; 122 | } 123 | } 124 | } 125 | 126 | var preloaded_langs = []; 127 | if (project_preloaded_langs[0] === "all") { 128 | preloaded_langs = ["all"] 129 | } 130 | else if (!_.isEmpty(runtime_preloaded_langs)) { 131 | preloaded_langs = _.union(project_preloaded_langs, runtime_preloaded_langs); 132 | } 133 | 134 | preloaded_langs.sort() 135 | 136 | if (!_.isEmpty(preloaded_langs)) { 137 | $.ajax({ 138 | type: 'GET', 139 | url: TAPi18n._cdn(`#{project_tap_i18n.i18n_files_route}/multi/${preloaded_langs.join(",")}.json`), 140 | dataType: 'json', 141 | success: function(data) { 142 | for (lang_tag in data) { 143 | TAPi18n._loadLangFileObject(lang_tag, data[lang_tag]); 144 | TAPi18n._loaded_languages.push(lang_tag); 145 | } 146 | }, 147 | data: {}, 148 | async: false 149 | }); 150 | } 151 | """ 152 | 153 | helpers.markProjectI18nLoaded(input_file_obj) 154 | 155 | return input_file_obj.addJavaScript 156 | path: "project-i18n.js", 157 | sourcePath: input_path, 158 | data: project_i18n_js_file, 159 | bare: false 160 | -------------------------------------------------------------------------------- /lib/tap_i18n/tap_i18n-client.coffee: -------------------------------------------------------------------------------- 1 | _.extend TAPi18n.prototype, 2 | _languageSpecificTranslators: null 3 | _languageSpecificTranslatorsTrackers: null 4 | 5 | _getLanguageFilePath: (lang_tag) -> 6 | if not @_enabled() 7 | return null 8 | 9 | path = @conf.i18n_files_route 10 | path = path.replace /\/$/, "" 11 | if Meteor.isCordova and path[0] == "/" 12 | path = Meteor.absoluteUrl().replace(/\/+$/, "") + path 13 | 14 | return @_cdn("#{path}/#{lang_tag}.json") 15 | 16 | _loadLanguage: (languageTag) -> 17 | # Load languageTag and its dependencies languages to TAPi18next if we 18 | # haven't loaded them already. 19 | # 20 | # languageTag dependencies languages are: 21 | # * The base language if languageTag is a dialect. 22 | # * The fallback language (en) if we haven't loaded it already. 23 | # 24 | # Returns a deferred object that resolves with no arguments if all files 25 | # loaded successfully to TAPi18next and rejects with array of error 26 | # messages otherwise 27 | # 28 | # Example: 29 | # TAPi18n._loadLanguage("pt-BR") 30 | # .done(function () { 31 | # console.log("languageLoaded successfully"); 32 | # }) 33 | # .fail(function (messages) { 34 | # console.log("Couldn't load languageTag", messages); 35 | # }) 36 | # 37 | # The above example will attempt to load pt-BR, pt and en 38 | 39 | dfd = new $.Deferred() 40 | 41 | if not @_enabled() 42 | return dfd.reject "tap-i18n is not enabled in the project level, check tap-i18n README" 43 | 44 | project_languages = @_getProjectLanguages() 45 | 46 | if languageTag in project_languages 47 | if languageTag not in @_loaded_languages 48 | loadLanguageTag = => 49 | jqXHR = $.getJSON(@_getLanguageFilePath(languageTag)) 50 | 51 | jqXHR.done (data) => 52 | @_loadLangFileObject(languageTag, data) 53 | 54 | @_loaded_languages.push languageTag 55 | 56 | dfd.resolve() 57 | 58 | jqXHR.fail (xhr, error_code) => 59 | dfd.reject("Couldn't load language '#{languageTag}' JSON: #{error_code}") 60 | 61 | directDependencyLanguageTag = if "-" in languageTag then languageTag.replace(/-.*/, "") else fallback_language 62 | 63 | # load dependency language if it is part of the project and not the fallback language 64 | if languageTag != fallback_language and directDependencyLanguageTag in project_languages 65 | dependencyLoadDfd = @_loadLanguage directDependencyLanguageTag 66 | 67 | dependencyLoadDfd.done => 68 | # All dependencies loaded successfully 69 | loadLanguageTag() 70 | 71 | dependencyLoadDfd.fail (message) => 72 | dfd.reject("Loading process failed since dependency language 73 | '#{directDependencyLanguageTag}' failed to load: " + message) 74 | else 75 | loadLanguageTag() 76 | else 77 | # languageTag loaded already 78 | dfd.resolve() 79 | else 80 | dfd.reject(["Language #{languageTag} is not supported"]) 81 | 82 | return dfd.promise() 83 | 84 | _registerHelpers: (package_name, template) -> 85 | if package_name != globals.project_translations_domain 86 | tapI18nextProxy = @_getPackageI18nextProxy(@packages[package_name].namespace) 87 | else 88 | tapI18nextProxy = @_getPackageI18nextProxy(globals.project_translations_domain) 89 | 90 | underscore_helper = (key, args...) -> 91 | options = (args.pop()).hash 92 | if not _.isEmpty(args) 93 | options.sprintf = args 94 | 95 | tapI18nextProxy(key, options) 96 | 97 | # template specific helpers 98 | if package_name != globals.project_translations_domain 99 | # {{_ }} 100 | if Template[template]? and Template[template].helpers? 101 | helpers = {} 102 | helpers[@packages[package_name].helper_name] = underscore_helper 103 | Template[template].helpers(helpers) 104 | 105 | # global helpers 106 | else 107 | # {{_ }} 108 | UI.registerHelper @conf.helper_name, underscore_helper 109 | 110 | # {{languageTag}} 111 | UI.registerHelper "languageTag", () => @getLanguage() 112 | 113 | return 114 | 115 | _getRegisterHelpersProxy: (package_name) -> 116 | # A proxy to _registerHelpers where the package_name is fixed to package_name 117 | (template) => 118 | @_registerHelpers(package_name, template) 119 | 120 | _prepareLanguageSpecificTranslator: (lang_tag) -> 121 | dfd = (new $.Deferred()).resolve().promise() 122 | 123 | if lang_tag of @_languageSpecificTranslatorsTrackers 124 | return dfd 125 | 126 | @_languageSpecificTranslatorsTrackers[lang_tag] = new Tracker.Dependency 127 | 128 | if not(lang_tag of @_languageSpecificTranslators) 129 | dfd = @_loadLanguage(lang_tag) 130 | .done => 131 | @_languageSpecificTranslators[lang_tag] = @_getSpecificLangTranslator(lang_tag) 132 | 133 | @_languageSpecificTranslatorsTrackers[lang_tag].changed() 134 | 135 | return dfd 136 | 137 | _getPackageI18nextProxy: (package_name) -> 138 | # A proxy to TAPi18next.t where the namespace is preset to the package's 139 | 140 | (key, options, lang_tag=null) => 141 | # Devs get confused and use lang option instead of lng option, make lang 142 | # alias of lng 143 | if options?.lang? and not options?.lng? 144 | options.lng = options.lang 145 | 146 | if options?.lng? and not lang_tag? 147 | lang_tag = options.lng 148 | # Remove options.lng so we won't pass it to the regular TAPi18next 149 | # before the language specific translator is ready to keep behavior 150 | # consistent. 151 | # 152 | # If lang is actually ready before the language specifc translator is 153 | # ready, TAPi18next will translate to lang_tag if we won't remove 154 | # options.lng. 155 | delete options.lng 156 | 157 | if lang_tag? 158 | @_prepareLanguageSpecificTranslator(lang_tag) 159 | 160 | @_languageSpecificTranslatorsTrackers[lang_tag].depend() 161 | 162 | if lang_tag of @_languageSpecificTranslators 163 | return @_languageSpecificTranslators[lang_tag] "#{TAPi18n._getPackageDomain(package_name)}:#{key}", options 164 | else 165 | return TAPi18next.t "#{TAPi18n._getPackageDomain(package_name)}:#{key}", options 166 | 167 | # If inside a reactive computation, we want to invalidate the computation if the client lang changes 168 | @_language_changed_tracker.depend() 169 | 170 | 171 | TAPi18next.t "#{TAPi18n._getPackageDomain(package_name)}:#{key}", options 172 | 173 | _onceEnabled: () -> 174 | @_registerHelpers globals.project_translations_domain 175 | 176 | _abortPreviousSetLang: null 177 | setLanguage: (lang_tag) -> 178 | self = @ 179 | 180 | @_abortPreviousSetLang?() 181 | 182 | isAborted = false 183 | @_abortPreviousSetLang = -> isAborted = true 184 | 185 | @_loadLanguage(lang_tag).then => 186 | if not isAborted 187 | TAPi18next.setLng(lang_tag) 188 | 189 | @_language_changed_tracker.changed() 190 | Session.set @_loaded_lang_session_key, lang_tag 191 | 192 | getLanguage: -> 193 | if not @._enabled() 194 | return null 195 | 196 | session_lang = Session.get @_loaded_lang_session_key 197 | 198 | if session_lang? then session_lang else @._fallback_language 199 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2024.07.23, Version 2.0.1 2 | 3 | * Fix small issue during package deployment to Atmosphere 4 | 5 | 2024.07.23, Version 2.0.0 6 | 7 | * Build plguin API updated: Replaced Plugin.registerSourceHandler with 8 | Plugin.registerCompiler. 9 | 10 | * Background: The registerCompiler API, introduced in Meteor 1.2, replaces the 11 | deprecated registerSourceHandler. 12 | 13 | * Fix a bug that causes conflicts when tap:i18n is used in packages alongside 14 | the ecmascript package. 15 | 16 | * Fix an issue where i18n files inside packages are loaded without being 17 | included in package.js. 18 | 19 | * Fixes an issue where i18n files in the folder tree that were part of the 20 | packages folder but weren't used by the project were still loaded to the 21 | bundle. 22 | 23 | * Major Version Increase: The major version increases to avoid potential 24 | breaking changes with the transition to Plugin.registerCompiler. 25 | 26 | * Removed the incorrect tj language tag (correct lang tag should be tg, which 27 | is available in tap:i18n). 28 | 29 | 2024.07.12, Version 1.12.4 30 | 31 | * Add be and tj to supported languages 32 | 33 | 2024.07.09, Version 1.12.3 34 | 35 | * Bugfix: "Access-Control-Allow-Origin" header isn't added when requesting 36 | all.txt 37 | 38 | 2024.06.26, Version 1.12.2 39 | 40 | * add pt-PT and yi 41 | 42 | 2024.06.24, Version 1.12.1 43 | 44 | * Sort the preloaded languages to increase chance of hitting the CDN for same 45 | requests 46 | 47 | 2024.06.24, Version 1.12.0 48 | 49 | * Introduce support for the TAP_I18N_PRELOADED_LANGS env var 50 | 51 | 2024.02.29, Version 1.11.2 52 | 53 | * Properly set CORS headers 54 | 55 | 2023.12.07, Version 1.11.1 56 | 57 | * Fix a bug in regex used to strip .json and query params when extracting lang 58 | tag 59 | 60 | 2023.12.06, Version 1.11.0 61 | 62 | * Introduce the ability to configure CDN using: TAPi18n.setCdnCb(cb) 63 | * Drop cfh:http-methods dependency and use Meteor's built-in 64 | WebApp.connectionHandler instead 65 | 66 | 2023.12.01, Version 1.10.2 67 | 68 | * Fix an issue with the version of tap-i18next used by tap:i18n: avoid 69 | modifying options object passed by reference to one of the functions. As a 70 | result of the issue a repeated use of the same option object could cause a text 71 | to be translated to the wrong language after a language been changed. 72 | 73 | * Add the Santomean language 74 | 75 | 2023.12.01, Version 1.10.1 76 | 77 | * package.js: Add packages versions required in order to publish to Atmosphere 78 | 79 | 2023.12.01, Version 1.10.0 80 | 81 | * Update package.js file that used deprecated non-camel-case api 82 | 83 | 2023.11.24, Version 1.9.0 84 | 85 | * Stop using aldeed:simple-schema@1.3.0 that caused issues in the build process 86 | 87 | * Fix various other minor issues that broke the build 88 | 89 | * Remove versionsFrom and unnecessary deps from package.js 90 | 91 | 2016.05.24, Version 1.8.1 92 | 93 | * Avoid race condition in setLanguage method. Fixes #86, thanks @mpowaga 94 | 95 | 2016.03.14, Version 1.8.0 96 | 97 | * YAML translations files support, thanks @karfield @nscarcella 98 | 99 | 2015.10.06, Version 1.7.0 100 | 101 | * Ready for Meteor v1.2 102 | 103 | 2015.09.19, Version 1.6.1 104 | 105 | * [BUGFIX] Fix compiler's output path, thanks @glyphing 106 | 107 | 2015.07.24, Version 1.6.0 108 | 109 | * Implement project-tap.i18n's preloaded_langs option 110 | * Improve Chinese dialects names 111 | 112 | 2015.07.06, Version 1.5.1 113 | 114 | * Register server side translator for fallback language correctly, fix #82 115 | 116 | 2015.05.08, Version 1.5.0 117 | 118 | * Make TAPi18n a constructor instead of object, inherit from EventEmitter 119 | * Make the lang option alias to the lng option in tap:i18n translator functions 120 | * Allow translation to a specific language on the client 121 | * [BUGFIX] Allow pre Meteor.startup() server side translations, resolves #56 122 | * [BUGFIX] Allow 0 as first argument of sprintf args. fix #43 123 | * [MINOR] i18next: Fix plurals function for Mandinka @hamoid 124 | * [MINOR] README: Added note about dot notation @SachaG 125 | * [MINOR] Correct name for Ukrainina language @shkomg 126 | * [MINOR] Travis CI: Stop testing node v0.8 127 | 128 | 2015.02.28, Version 1.4.1 129 | 130 | * [bugfix] Under cordova ensure absoluteUrl have no trailing / 131 | 132 | 2015.02.05, Version 1.4.0 133 | 134 | * Add Cordova support (requires internet connection) 135 | 136 | 2015.01.16, Version 1.3.2 137 | 138 | * Use simple-schema 1.3.0 139 | 140 | 2015.01.14, Version 1.3.1 141 | 142 | * [bugfix] getLanguages() should return languages defined as supported in 143 | project-tap.i18n even if there is no *.i18n.json for these languages in 144 | the project level 145 | * [bugfix] getLanguages() shouldn't return non-supported languages Fix #38 146 | * Use cfs:http-methods@0.0.27 instead of tap:http-methods@0.0.23 147 | 148 | 2015.01.11, Version 1.3.0 149 | 150 | * Fix language_names object keys, fix #36 151 | * Disable TAPi18next cookie feature that caused anomalies 152 | * Introduce TAPi18n.loadTranslations 153 | 154 | 2014.11.25, Version 1.2.1 155 | 156 | * Register helpers with Template.template.helpers() 157 | * [bugfix] fix the package templates detection (got broken by a Meteor API change) 158 | 159 | 2014.11.25, Version 1.2.0 160 | 161 | * package-tap.i18n: Introduce the namespace option 162 | 163 | 2014.11.22, Version 1.1.1 164 | 165 | * [bugfix] build plugin: don't assign _ var to global namespace. Thanks 166 | @smeijer . #30 167 | 168 | 2014.11.18, Version 1.1.0 169 | 170 | * Allow the use of comments in *.i18n.json files 171 | * [MINOR] package.js: use METEOR@0.9.4, and the stable version of registerBuildPlugin 172 | * [MINOR] fix a symbolic link 173 | * [MINOR] add to the tree files that were missing due to wrong .gitignore 174 | 175 | 2014.09.25, Version 1.0.7 176 | 177 | * [bugfix] Packages non-fallback languages now load see issue #13 Thanks 178 | to @francocatena . 179 | * [bugfix] tap:i18n files of packages under /packages directory now load 180 | correctly 181 | 182 | 2014.09.24, Version 1.0.6, 304e1ee00e2916646bf672fd53ef5a6cceb69db4 183 | 184 | * [bugfix] Project level: Make loading of tap:i18n files insensitive to order by 185 | Registering template helpers as soon as tap:i18n gets enabled and not on 186 | Meteor.ready() . Thanks to @danieljonce for reporting this issue 187 | 188 | 2014.09.24, Version 1.0.5, df044dc3523d1ebb608f1e115a31539b4ba42742 189 | 190 | * Project level: languages files should be common to server and client 191 | * Fix unitests - add languages files to the tree 192 | * Remove redundant output from i18n.json and project-tap.i18n compilers 193 | 194 | 2014.09.19, Version 1.0.4, 4d843a11cf757ef6d2ff6534a1c6b757b91eedda 195 | 196 | * [bugfix] each language file should have its own output path 197 | (this bug prevented multiple files per language in the app level) 198 | 199 | 2014.09.16, Version 1.0.3, 888f3dd5066e320513a76f5bb1ce711a7a84478d 200 | 201 | * [bugfix] TAPi18n.getLanguages() format is now compatible with docs 202 | 203 | 2014.09.10, Version 1.0.2, 66fcd441f3b6a107db0aa32c8f3ea9692c1c8c09 204 | 205 | * [bugfix] compiler: fix error for invalid JSON 206 | 207 | 2014.09.10, Version 1.0.1, 7f92b8fbd8ced171873b8afd70a808f51167e7ac 208 | 209 | * Allow project-tap.i18n's supported_languages to have a lang with no 210 | translation files 211 | * [bugfix] tap-i18n compiler: init compiler_configuration upon a rebuild 212 | 213 | 2014.09.09, Version 1.0.0, 5964838f9ab085136d45899d38bb126958c3deda 214 | 215 | * Build plugin rewritten 216 | * tap-i18n now fully support Meteor v0.9 217 | * Ready to be used with Cordova 218 | * **New Features:** 219 | * Server side internationalization is now supported. 220 | * The template translation helper name (_) and the package translation function 221 | name (__) are now customizable. 222 | * Transparent bundling, no need for special procedures for deploying project 223 | that uses tap-i18n. 224 | * Language files and project-tap.i18n can now be located anywhere in the project tree. 225 | * A project/package can now have more than one language file for the same 226 | language. 227 | * getLanguages() now works in both server and client. 228 | * **Backward compatibility:** 229 | * package i18n files now have to be added to both the client and the server 230 | architectures, not only the client. 231 | * The base language of a dialects are no longer added automatically as a supported 232 | language. 233 | * project-tap.i18n: languages_files_dir and build_files_path properties are now 234 | obsolete. browser_path property renamed to cdn_path. 235 | * package-tap.i18n: languages_files_dir is now obsolete. 236 | 237 | 2014.08.30, Version 0.9.2, 742e44f659dfb7800d332bf4b2aa990e6f220d36 238 | 239 | * Bugfix: Build plugin should consider projects with a *:i18n package as tap:i18n enabled 240 | * Use tap:http-methods instead of raix:http-methods which isn't ready for v0.9.0 241 | 242 | 2014.08.30, Version 0.9.0, 118aa825e76165aac9df9f3153fbb8edc044a864 243 | 244 | * tap-i18n is now tap:i18n 245 | * Migrate to Meteor v0.9 246 | 247 | 2014.08.11, Version 0.8.0, a500ae5c5c6da2aa0ccd56bfe407bfa9c8a77b62 248 | 249 | * [MINOR] package.js: only use single quotes to enclose strings 250 | * Do not require having a file for the base language of a dialect 251 | * Build plugin: make sure isString before removeFileTrailingSeparator 252 | 253 | 2014.07.30, Version 0.7.0, 7c414420e65cb67a9e49896826542db1815a257c 254 | 255 | * Refactor build plugin. Fix a rare bug causing the proj base lang not to load 256 | * build plugin: catch all build errors and use compileStep.error to report them 257 | * Do not build/load supported languages with no translation files 258 | 259 | 2014.06.26, Version 0.6.0, 7e9685ce75165a6a5998f6f4643490fc1e14c166 260 | 261 | * Introduce {{languageTag}} 262 | * README: add instuctions for deploying tap-i18n projects to *.meteor.com 263 | * tap-i18n now works in Meteor bundles 264 | 265 | 2014.06.14, Version 0.5.1, 71a9ad595e972998e16d7cbd60fed699127464c3 266 | 267 | * Bugfix: Trigger buildFilesOnce for .i18n.json files for the os arch so 268 | TAPi18n.conf will get set if there is no project-tap.i18n 269 | 270 | 2014.06.13, Version 0.5.0, 0d7ea8c3ac8307b3d48efc6c8b80b1ce2dd1e8b1 271 | 272 | * Unittests now work on mac 273 | * Introduce TAPi18n.getLanguages() 274 | 275 | 2014.06.12, Version 0.4.0, f583aa179b559d447519c61dcdb019f05a0b10a3 276 | 277 | * README restructured 278 | * Set lisence to: MIT 279 | 280 | 2014.06.11, Version 0.3.0, ba328abf4e057b60c82cfb455d183fe1ff4605cd 281 | 282 | * Refresh the clients when .i18n.json files change 283 | * bugfix: build files if i18n.json files change for case we don't have 284 | project-tap.i18n to trigger build 285 | * Show debug meesages only if globals.debug is true or env variable 286 | TAP_I18N_DEBUG is "true" 287 | * project-tap.i18n ignore default_browser_path if build_files_path is null 288 | * Fixes to README 289 | 290 | 2014.06.10, Version 0.2.0, 50bb1e9643e8233438ff7614bb79ca3dd575a3a8 291 | 292 | * Implement project-level translations 293 | * For enabled tap-i18n projects use the http-methods package as the default 294 | mean for serving unified languages files instead of /public 295 | * API change: Consider projects with no project-tap.i18n as tap-i18n enabled 296 | * API change: By default, regard all the languages we find translations for as 297 | supported_languages 298 | * API change: No package-level default language - en is our fallback_language 299 | everywhere 300 | * Improve the unittesting framework 301 | * Naming: Use TAPi18n and TAPi18next instead of TapI18n and TapI18next 302 | * Bugfix: don't add an error object when throwing exception if error 303 | 304 | 2014.05.22, Version 0.1.0, 676f50f0bea154596cacf44c34c352b09aa1d215 305 | 306 | * tap-i18n first release 307 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tap-i18n 2 | 3 | A comprehensive internationalization solution for Meteor 4 | 5 | ### Internationalization for Meteor 6 | 7 | **tap-i18n** is a [Meteor](http://www.meteor.com) package that provides a comprehensive [i18n](http://www.i18nguy.com/origini18n.html) solution for Meteor apps and packages, with the goal of standardizing the way package developers internationalize their 8 | packages. 9 | 10 | [Watch a talk about tap:i18n & tap:i18n-db](https://www.youtube.com/watch?v=cu_dsoIc_0E) 11 | 12 | **Developed by:** MeteorSpark [Professional Meteor Services](http://www.meteorspark.com) for [TAPevents](http://tapevents.com/). 13 | 14 | **Maintained by:** JustDo.com [Project Management Tool](https://justdo.com). 15 | 16 | **Related Packages:** 17 | 18 | * Check [tap:i18n-db](https://github.com/TAPevents/tap-i18n-db) for Meteor collections internationalization. 19 | * Check [tap:i18n-ui](https://github.com/TAPevents/tap-i18n-ui) for bootstrap based UI components for tap:i18n. 20 | * Check [tap:i18n-bundler](https://github.com/TAPevents/i18n-bundler) for Cordova & static file deployments. 21 | 22 | ## Contents 23 | 24 | - [Key Features](#key-features) 25 | - [Quickstart](#quickstart) 26 | - [Documentation & Examples](#documentation--examples) 27 | - [TAPi18n API](#tapi18n-api) 28 | - [The tap-i18n Helpers](#the-tap-i18n-helpers) 29 | - [Languages Tags and Translations Prioritization](#languages-tags-and-translations-prioritization) 30 | - [Structure of Languages Files](#structure-of-languages-files) 31 | - [Configuring tap-i18n](#configuring-tap-i18n) 32 | - [Configuring CDN in tap-i18n](#configuring-cdn-in-tap-i18n) 33 | - [Disabling tap-i18n](#disabling-tap-i18n) 34 | - [Using tap-i18n in Cordova apps](#using-tap-i18n-in-cordova-apps) 35 | - [Developing Packages](#developing-packages) 36 | - [tap-i18n Two Work Modes](#tap-i18n-two-work-modes) 37 | - [Setup tap-i18n](#setup-tap-i18n) 38 | - [Package Level tap-i18n Functions](#package-level-tap-i18n-functions) 39 | - [Using tap-i18n in Your Package Templates](#using-tap-i18n-in-your-package-templates) 40 | - [Unit Testing](#unit-testing) 41 | - [License](#license) 42 | - [Credits](#credits) 43 | 44 | ## Key Features 45 | 46 | ### All Encompassing 47 | 48 | tap-i18n is designed in a way that distinguishes the role of the package developer, that is, making the package available in multiple languages, from the role of the app developer which is to translate the app, but more importantly, to manage the app's internationalization aspects, such as: setting the supported languages for the project, setting the client language, configuring CDNs for language files, and so on. 49 | 50 | ### Readable Syntax 51 | 52 | ```handlebars 53 |
{{_ "sign_up"}}
54 | ``` 55 | 56 | ### Advanced i18n 57 | 58 | tap-i18n uses [i18next v1.11](http://i18next.github.io/i18next/) as its internationalization engine and exposes all its capabilities to the Meteor's templates - variables, dialects, count/context aware keys, and more. 59 | 60 | **client/messages.html** 61 | 62 | ```handlebars 63 | 66 | ``` 67 | 68 | **i18n/en.i18n.json** 69 | 70 | ```json 71 | { 72 | "inbox_status": "Hey, %s! You have received one new message today.", 73 | "inbox_status_plural": "Hey, %s! You have received %s new messages today." 74 | } 75 | ``` 76 | See more examples below. 77 | 78 | ### Transparent Namespacing 79 | 80 | You don't need to worry about domain prefixing or package conflicts when you translate your project or package. Behind the scenes we automatically generate scoped namespaces for you. 81 | 82 | ### Ready to Scale 83 | 84 | * Translations are unified into a single JSON file per language that includes both package and project-level translations 85 | * On-demand: translations are loaded only when they are needed 86 | * 3rd Party CDN Support 87 | 88 | 89 | ## Quickstart 90 | 91 | **Step 1:** Install tap-i18n using meteor: 92 | 93 | ```bash 94 | $ meteor add tap:i18n 95 | ``` 96 | 97 | **Step 2:** Add translation helpers to your markup: 98 | 99 | **\*.html** 100 | 101 | ```handlebars 102 |
{{_ "hello"}}
103 | ``` 104 | 105 | **Step 3:** Define translations in JSON or YAML format: 106 | 107 | **i18n/en.i18n.json** 108 | 109 | ```json 110 | { "hello": "Hey there" } 111 | ``` 112 | 113 | **i18n/fr.i18n.json** 114 | 115 | ```json 116 | { "hello": "Bonjour" } 117 | ``` 118 | 119 | **i18n/es.i18n.yml** 120 | 121 | ```yaml 122 | hello: Hola 123 | ``` 124 | 125 | Translations files should end with lang_tag.i18n.json/yml. 126 | 127 | You can split translations of a certain language to multiple files, we ignore 128 | the prefixed text, e.g., we add the translations of menu.en.i18n.json in the 129 | same way we add those of en.i18n.json . 130 | 131 | You can put languages files anywhere in your project tree, as long as they are 132 | common to both your server and client - **do not put languages files under 133 | /client, /server or /public**. 134 | 135 | Note: Languages files have to be saved in utf-8 encoding. 136 | 137 | **Step 4:** Initiate the client language on startup (optional) 138 | 139 | If you want the client to be served by a specific language on startup 140 | 141 | Assuming that you have a function getUserLanguage() that returns the language 142 | for tag for the current user. 143 | 144 | ```javascript 145 | getUserLanguage = function () { 146 | // Put here the logic for determining the user language 147 | 148 | return "fr"; 149 | }; 150 | 151 | if (Meteor.isClient) { 152 | Meteor.startup(function () { 153 | Session.set("showLoadingIndicator", true); 154 | 155 | TAPi18n.setLanguage(getUserLanguage()) 156 | .done(function () { 157 | Session.set("showLoadingIndicator", false); 158 | }) 159 | .fail(function (error_message) { 160 | // Handle the situation 161 | console.log(error_message); 162 | }); 163 | }); 164 | } 165 | ``` 166 | 167 | * If you won't set a language on startup your project will be served in the 168 | fallback language: English 169 | * You probably want to show a loading indicator until the language is ready (as 170 | shown in the example), otherwise the templates in your projects will be in 171 | English until the language will be ready 172 | 173 | ## Documentation & Examples 174 | 175 | ### TAPi18n API 176 | 177 | **TAPi18n.setLanguage(language\_tag) (Client)** 178 | 179 | Sets the client's translation language. 180 | 181 | Returns a jQuery deferred object that resolves if the language load 182 | succeed and fails otherwise. 183 | 184 | **Notes:** 185 | 186 | * language\_tag has to be a supported Language. 187 | * jQuery deferred docs: [jQuery Deferred](http://api.jquery.com/jQuery.Deferred/) 188 | 189 | **TAPi18n.getLanguage() (Client)** 190 | 191 | Returns the tag of the client's current language or null if 192 | tap-i18n is not installed. 193 | 194 | If inside a reactive computation, invalidate the computation the next time the 195 | client language get changed (by TAPi18n.setLanguage) 196 | 197 | **TAPi18n.getLanguages() (Anywhere)** 198 | 199 | Returns an object with all the supported languages and their names. 200 | 201 | A language is considred supported if it is in the supported_languages array of 202 | the project-tap.i18n json. If supported_languages is null or not defined in 203 | project-tap.i18n we consider all the languages we find *.i18n.json/yml files to as 204 | supported. 205 | 206 | The returned object is in the following format: 207 | 208 | ```javascript 209 | { 210 | 'en': { 211 | 'name':'English', // Local name 212 | 'en':'English' // English name 213 | }, 214 | 'zh': { 215 | 'name':'中文' // Local name 216 | 'en':'Chinese' // English name 217 | } 218 | . 219 | . 220 | . 221 | } 222 | ``` 223 | 224 | **TAPi18n.__(key, options, lang_tag=null) (Anywhere)** 225 | 226 | *If `lang_tag` is null:* 227 | 228 | Translates key to the current client's language. If inside a reactive 229 | computation, invalidate the computation the next time the client language get 230 | changed (by TAPi18n.setLanguage). 231 | 232 | *Otherwise:* 233 | 234 | Translates key to lang_tag. if you use `lang_tag` you should use `__` in a 235 | reactive computation since the string will be translated to the current client 236 | language if a translator to lang_tag is not ready in the client (if called for 237 | the first time with that lang_tag, or until language data load from the server 238 | finishes) and will get invalidated (trigger reactivity) when the translator to 239 | that lang_tag is ready to be used to translate the key. 240 | 241 | Using `i18next.t` `lng` option or `lang`, which we made as alias to `lang` in 242 | tap:i18n, is equivalent to setting the `lang_tag` attribute. 243 | 244 | The function is a proxy to the i18next.t() method. 245 | Refer to the [documentation of i18next.t()](http://i18next.github.io/i18next/pages/doc_features.html) 246 | to learn about its possible options. (Make sure you refer to i18next v1.11 documentation and not v2) 247 | 248 | **On the server**, TAPi18n.__ is not a reactive resource. You have to specify 249 | the language tag you want to translate the key to. 250 | 251 | **TAPi18n.loadTranslations(translations, namespace="project") (Anywhere)** 252 | 253 | Use *translations* in addition or instead of the translations defined in the 254 | i18n.json files. Translations defined by loadTranslations will have priority 255 | over those defined in language files (i18n.json) of *namespace* (the project, 256 | or package name). 257 | 258 | To enjoy [the benefits of tap:i18n](#key-features), you should use language 259 | files to internationalize your project whenever you can. 260 | 261 | Legitimate cases for *loadTranslations* are: 262 | 263 | * Allowing users to change the project translations 264 | * Changing translations of 3rd party packages that you don't want to fork (see 265 | the Note below). 266 | 267 | Example: 268 | 269 | ```javascript 270 | TAPi18n.loadTranslations( 271 | { 272 | es: { 273 | meteor_status_waiting: "Desconectado" 274 | }, 275 | fr: { 276 | meteor_status_failed: "La connexion au serveur a échoué" 277 | } 278 | }, 279 | "francocatena:status" 280 | ); 281 | ``` 282 | 283 | **Arguments:** 284 | 285 | * `translations`: An object of the following format: 286 | 287 | ```javascript 288 | { 289 | 'lang-tag': { 290 | 'translation-key1': 'translation', 291 | 'translation-key2': 'translation', 292 | ... 293 | }, 294 | ... 295 | } 296 | ``` 297 | 298 | * `namespace="project"`: The namespace you want to add the translations to. by 299 | default translations are added to the project namespace, if you want to 300 | change a package translation use the package name as the namespace like the 301 | above example. 302 | 303 | **Notes:** 304 | 305 | * **Adding support to a new language in your app:** You can't use 306 | *addTranslations* in order to add support to a new language, that is, to allow 307 | users to change the interface language of the app to that language. In order 308 | to start support a new language in your app, you'll have to either add a 309 | language file to that language (*.i18n.json file) or add that languages to your 310 | project-tap.i18n file. 311 | 312 | * **Translating a package that uses tap:i18n to another language**: If you want 313 | to add a new language to a 3rd party package (and you can't get it's owner to 314 | merge your pull request) consider introducing a "translation" package in which 315 | package-tap.i18n has the "namespace" options set to the package you are 316 | translating. That way you can translate with languages files instead of 317 | *addTranslations* and share your translation package with others. 318 | 319 | ### The tap-i18n Helpers 320 | 321 | ### The \_ Helper 322 | 323 | To use tap-i18n to internationalize your templates you can use the \_ helper 324 | that we set on the project's templates and on packages' templates for packages 325 | that uses tap-i18n: 326 | 327 | {{_ "key" "sprintf_arg1" "sprintf_arg2" ... op1="option-value" op2="option-value" ... }} 328 | 329 | **You can customize the helper name, see "Configuring tap-i18n" section.** 330 | 331 | The translation files that will be used to translate key depends on the 332 | template from which it is being used: 333 | * If the helper is being used in a template that belongs to a package that uses 334 | tap-i18n we'll always look for the translation in that package's translation 335 | files. 336 | * If the helper is being used in one of the project's templates we'll look for 337 | the translation in the project's translation files (tap-i18n has to be 338 | installed of course). 339 | 340 | **Usage Examples:** 341 | 342 | Assuming the client language is en. 343 | 344 | **Example 1:** Simple key: 345 | 346 | en.i18n.json: 347 | ------------- 348 | { 349 | "click": "Click Here", 350 | "html_key": "BOLD" 351 | } 352 | 353 | page.html: 354 | ---------- 355 | 359 | 360 | output: 361 | ------- 362 | Click Here 363 | BOLD 364 | 365 | **Example 2:** Simple key specific language: 366 | 367 | en.i18n.json: 368 | ------------- 369 | { 370 | "click": "Click Here" 371 | } 372 | 373 | fr.i18n.json: 374 | ------------- 375 | { 376 | "click": "Cliquez Ici" 377 | } 378 | 379 | page.html (lng and lang options are the same in tap:i18n you can use both): 380 | ---------- 381 | 384 | 385 | 388 | 389 | output: 390 | ------- 391 | Cliquez Ici 392 | 393 | **Example 3:** Sprintf: 394 | 395 | en.i18n.json: 396 | ------------- 397 | { 398 | "hello": "Hello %s, your last visit was on: %s" 399 | } 400 | 401 | page.html: 402 | ---------- 403 | 406 | 407 | output: 408 | ------- 409 | Hello Daniel, your last visit was on: 2014-05-22 410 | 411 | **Example 4:** Named variables and sprintf: 412 | 413 | en.i18n.json: 414 | ------------- 415 | { 416 | "hello": "Hello __user_name__, your last visit was on: %s" 417 | } 418 | 419 | page.html: 420 | ---------- 421 | 424 | 425 | output: 426 | ------- 427 | Hello Daniel, your last visit was on: 2014-05-22 428 | 429 | **Note:** Named variables have to be after all the sprintf parameters. 430 | 431 | **Example 5:** Named variables, sprintf, singular/plural: 432 | 433 | en.i18n.json: 434 | ------------- 435 | { 436 | "inbox_status": "__username__, You have a new message (inbox last checked %s)", 437 | "inbox_status_plural": "__username__, You have __count__ new messages (last checked %s)" 438 | } 439 | 440 | page.html: 441 | ---------- 442 | 446 | 447 | output: 448 | ------- 449 | Daniel, You have a new message (inbox last checked 2014-05-22) 450 | Chris, You have 4 new messages (last checked 2014-05-22) 451 | 452 | **Example 6:** Singular/plural, context: 453 | 454 | en.i18n.json: 455 | ------------- 456 | { 457 | "actors_count": "There is one actor in the movie", 458 | "actors_count_male": "There is one actor in the movie", 459 | "actors_count_female": "There is one actress in the movie", 460 | "actors_count_plural": "There are __count__ actors in the movie", 461 | "actors_count_male_plural": "There are __count__ actors in the movie", 462 | "actors_count_female_plural": "There are __count__ actresses in the movie", 463 | } 464 | 465 | page.html: 466 | ---------- 467 | 475 | 476 | output: 477 | ------- 478 | There is one actor in the movie 479 | There is one actor in the movie 480 | There is one actress in the movie 481 | There are 2 actors in the movie 482 | There are 2 actors in the movie 483 | There are 2 actresses in the movie 484 | 485 | * Refer to the [documentation of i18next.t() v1.11](http://i18next.github.io/i18next/pages/doc_features.html) 486 | to learn more about its possible options. (Make sure you refer to i18next v1.11 documentation and not v2) 487 | * The translation will get updated automatically after calls to 488 | TAPi18n.setLanguage(). 489 | 490 | ### More helpers 491 | 492 | **{{languageTag}}:** 493 | 494 | The {{languageTag}} helper calls TAPi18n.getLanguage(). 495 | 496 | It's useful when you need to load assets depending on the current language, for 497 | example: 498 | 499 | ```handlebars 500 | 503 | ``` 504 | 505 | ### Languages Tags and Translations Prioritization 506 | 507 | We use the [IETF language tag system](http://en.wikipedia.org/wiki/IETF_language_tag) 508 | for languages tagging. With it developers can refer to a certain language or 509 | pick one of its dialects. 510 | 511 | Example: A developer can either refer to English in general using: "en" or to 512 | use the Great Britain dialect with "en-GB". 513 | 514 | **If tap-i18n is install** we'll attempt to look for a translation of a certain 515 | string in the following order: 516 | * Language dialect, if specified ("pt-BR") 517 | * Base language ("pt") 518 | * Base English ("en") 519 | 520 | **Notes:** 521 | 522 | * We currently support only one dialect level. e.g. nan-Hant-TW is not 523 | supported. 524 | * "en-US" is the dialect we use for the base English translations "en". 525 | * If tap-i18n is not installed, packages will be served in English, the fallback language. 526 | 527 | ### Structure of Languages Files 528 | 529 | Languages files should be named: arbitrary.text.lang_tag.i18n.json . e.g., en.i18n.json, menu.pt-BR.i18n.json. 530 | 531 | You can have more than one file for the same language. 532 | 533 | You can put languages files anywhere in your project tree, as long as they are 534 | common to both your server and client - **do not put languages files under 535 | /client, /server or /public**. 536 | 537 | Example for languages files: 538 | 539 | en.i18n.json 540 | { 541 | "sky": "Sky", 542 | "color": "Color" 543 | } 544 | 545 | pt.i18n.json 546 | { 547 | "sky": "Céu", 548 | "color": "Cor" 549 | } 550 | 551 | fr.i18n.json 552 | { 553 | "sky": "Ciel" 554 | } 555 | 556 | en-GB.i18n.json 557 | { 558 | "color": "Colour" 559 | } 560 | 561 | * Do not use colons and periods (see note below) in translation keys. 562 | * To avoid translation bugs all the keys in your package must be translated to 563 | English ("en") which is the fallback language we use if tap-i18n is not installed, 564 | or when we can't find a translation for a certain key. 565 | * In the above example there is no need to translate "sky" in en-GB which is the 566 | same in en. Remember that thanks to the Languages Tags and Translations 567 | Prioritization (see above) if a translation for a certain key is the same for a 568 | language and one of its dialects you don't need to translate it again in the 569 | dialect file. 570 | * The French file above have no translation for the color key above, it will 571 | fallback to English. 572 | * Check [i18next features documentation](http://i18next.github.io/i18next/pages/doc_features.html) for 573 | more advanced translations structures you can use in your JSONs files (Such as 574 | variables, plural form, etc.). (Make sure you refer to i18next v1.11 documentation and not v2) 575 | 576 | #### A note about dot notation 577 | 578 | Note that `{_ "foo.bar"}` will be looked under `{foo: {bar: "Hello World"}}`, and not under `"foo.bar"`. 579 | 580 | ### Configuring tap-i18n 581 | 582 | To configure tap-i18n add to it a file named **project-tap.i18n**. 583 | 584 | This JSON can have the following properties. All of them are optional. The values bellow 585 | are the defaults. 586 | 587 | project-root/project-tap.i18n 588 | ----------------------------- 589 | { 590 | "helper_name": "_", 591 | "supported_languages": null, 592 | "i18n_files_route": "/tap-i18n", 593 | "preloaded_langs": [] 594 | } 595 | 596 | Options: 597 | 598 | **helper\_name:** the name for the templates' translation helper. 599 | 600 | **supported\_languages:** A list of languages tags you want to make available on 601 | your project. If null, all the languages we'll find translation files for, in the 602 | project, will be available. 603 | 604 | **i18n\_files\_route:** The route in which the tap-i18n resources will be available in the project. 605 | 606 | **preloaded_langs:** An array of languages tags. If isn't empty, a single synchronous ajax requrest will load the translation strings for all the languages tags listed. If you want to load all the supported languages set preloaded_langs to `["*"]` (`"*"` must be the first item of the array, the rest of the array will be ignored. `["zh-*"]` won't work). 607 | An alernative way to dynamically set preloaded_langs on runtime is by defining the **TAP_I18N_PRELOADED_LANGS** global variable. This variable should be an array of language tags. 608 | ```html 609 | 610 | 613 | 614 | 615 | ``` 616 | 617 | **Notes:** 618 | 619 | * English is loaded by default, so you don't need to include it in the preloaded language list. 620 | * We use AJAX to load the languages files so you'll have to set CORS on your CDN. 621 | * Support of TAP_I18N_PRELOADED_LANGS relies on the `project-tap.i18n` build plugin. In other words, if `project-tap.i18n` doesn't exist, TAP_I18N_PRELOADED_LANGS will have **no** effect. 622 | * Preloaded languages are the union of `preloaded_langs` (specified in `project-tap.i18n`) and `TAP_I18N_PRELOADED_LANGS` (specified in the ``). 623 | Examples: 624 | 1. `preloaded_langs = ["fr"] ` 625 | `TAP_I18N_PRELOADED_LANGS = ["he"]` 626 | Result: "fr" and "he" will be preloaded 627 | 2. `preloaded_langs = ["*"]` 628 | **All** languages will be preloaded, regardless of whether TAP_I18N_PRELOADED_LANGS is defined. 629 | 3. `preloaded_langs = ["ar"]` 630 | `TAP_I18N_PRELOADED_LANGS = ["ar", "vi"]` 631 | Result: "ar" and "vi" will be preloaded. 632 | 633 | ### Configuring CDN in tap-i18n 634 | 635 | To utilize a Content Delivery Network (CDN) with tap:i18n, invoke `TAPi18n.setCdnCb(cb)`. This function expects cb, a callback function, which receives a URL path as its parameter and should return the CDN-modified URL. 636 | 637 | **Important:** If your project incorporates project-tap.i18n, it is crucial to execute TAPi18n.setCdnCb(cb) before project-tap.i18n loads. Arrange your configuration files such that the one for CDN setup is named in a way that it loads lexicographically prior to project-tap.i18n. 638 | 639 | For instance, in JustDo.com, the CDN setup is managed as follows: 640 | 641 | ```coffeescript 642 | # /lib/030-i18n/000-tap-i18n-cdn-loader.coffee 643 | TAPi18n.setCdnCb JustdoCoreHelpers.getCDNUrl 644 | ``` 645 | 646 | This configuration file is placed lexicographically before /lib/030-i18n/project-tap.i18n in the directory structure, ensuring the correct load order. 647 | 648 | ### Disabling tap-i18n 649 | 650 | **Step 1:** Remove tap-i18n method calls from your project. 651 | 652 | **Step 2:** Remove tap-i18n package 653 | 654 | ```bash 655 | $ meteor remove tap:i18n 656 | ``` 657 | 658 | ### Using tap-i18n in Cordova apps 659 | 660 | In order to use tap-i18n in a Cordova app you must set the `--server` flag 661 | to your server's root url when building your project. 662 | 663 | ```bash 664 | $ meteor build --server="http://www.your-site-domain.com" 665 | ``` 666 | 667 | If your app should work when the user is offline, install the [tap:i18n-bundler](https://atmospherejs.com/tap/i18n-bundler) package and follow [its instructions](https://github.com/TAPevents/i18n-bundler#usage). 668 | 669 | ## Developing Packages 670 | 671 | Though the decision to translate a package and to internationalize it is a 672 | decision made by the **package** developer, the control over the 673 | internationalization configurations are done by the **project** developer and 674 | are global to all the packages within the project. 675 | 676 | Therefore if you wish to use tap-i18n to internationalize your Meteor 677 | package your docs will have to refer projects developers that will use it to 678 | the "Usage - Project Developers" section above to enable internationalization. 679 | If the project developer won't enable tap-i18n your package will be served in 680 | the fallback language English. 681 | 682 | ### tap-i18n Two Work Modes 683 | 684 | tap-i18n can be used to internationalize projects and packages, but its 685 | behavior is determined by whether or not it's installed on the project level. 686 | We call these two work modes: *enabled* and *disabled*. 687 | 688 | When tap-i18n is disabled we don't unify the languages files that the packages 689 | being used by the project uses, and serve all the packages in the fallback 690 | language (English) 691 | 692 | ### Setup tap-i18n 693 | 694 | In order to use tap-i18n to internationalize your package: 695 | 696 | **Step 1:** Add the package-tap.i18n configuration file: 697 | 698 | You can use empty file or an empty JSON object if you don't need to change them. 699 | 700 | The values below are the defaults. 701 | 702 | package_dir/package-tap.i18n 703 | ---------------------------- 704 | { 705 | // The name for the translation function that 706 | // will be available in package's namespace. 707 | "translation_function_name": "__", 708 | 709 | // the name for the package templates' translation helper 710 | "helper_name": "_", 711 | 712 | // directory for the translation files (without leading slash) 713 | "languages_files_dir": "i18n", 714 | 715 | // tap:i18n automatically separates the translation strings of each package to a 716 | // namespace dedicated to that package, which is used by the package's translation 717 | // function and helper. Use the namespace option to set a custom namespace for 718 | // the package. By using the name of another package you can use your package to 719 | // add to that package or modify its translations. You can also set the namespace to 720 | // "project" to add translations that will be available in the project level. 721 | "namespace": null 722 | } 723 | 724 | **Step 2:** Create your languages\_files\_dir: 725 | 726 | Example for the default languages\_files\_dir path and its structure: 727 | 728 | . 729 | |--package_name 730 | |----package.js 731 | |----package-tap.i18n 732 | |----i18n # Should be the same path as languages_files_dir option above 733 | |------en.i18n.json 734 | |------fr.i18n.json 735 | |------pt.i18n.json 736 | |------pt-BR.i18n.json 737 | . 738 | . 739 | . 740 | 741 | NOTE: the file for the fallback language (`en.i18n.json`) **must** exist (it may be empty though). 742 | 743 | The leanest set up (for instance in a private package, where you keep the translations at the project level) is two empty files: `package-tap.i18n` and `i18n/en.i18n.json`. 744 | 745 | **Step 3:** Setup your package.js: 746 | 747 | Your package's package.js should be structured as follow: 748 | 749 | Package.onUse(function (api) { 750 | api.use(["tap:i18n"], ["client", "server"]); 751 | 752 | . 753 | . 754 | . 755 | 756 | // You must load your package's package-tap.i18n before you load any 757 | // template 758 | api.addFiles("package-tap.i18n", ["client", "server"]); 759 | 760 | // Templates loads (if any) 761 | 762 | // List your languages files so Meteor will watch them and rebuild your 763 | // package as they change. 764 | // You must load the languages files after you load your templates - 765 | // otherwise the templates won't have the i18n capabilities (unless 766 | // you'll register them with tap-i18n yourself, see below). 767 | api.addFiles([ 768 | "i18n/en.i18n.json", 769 | "i18n/fr.i18n.json", 770 | "i18n/pt.i18n.json", 771 | "i18n/pt-br.i18n.json" 772 | ], ["client", "server"]); 773 | }); 774 | 775 | Note: en, which is the fallback language, is the only language we integrate 776 | into the clients bundle. All the other languages files will be loaded only 777 | to the server bundle and will be served as part of the unified languages files, 778 | that contain all the project's translations. 779 | 780 | ### Package Level tap-i18n Functions 781 | 782 | The following functions are added to your package namespace by tap-i18n: 783 | 784 | **\_\_("key", options, lang_tag) (Anywhere)** 785 | 786 | Read documenation for `TAPi18n.__` above. 787 | 788 | **On the server**, TAPi18n.__ is not a reactive resource. You have to specify 789 | the language tag you want to translate the key to. 790 | 791 | You can use package-tap.i18n to change the name of this function. 792 | 793 | **registerI18nHelper(template\_name) (Client)** 794 | 795 | **registerTemplate(template\_name) (Client) [obsolete alias, will be removed in future versions]** 796 | 797 | Register the \_ helper that maps to the \_\_ function for the 798 | template with the given name. 799 | 800 | **Important:** As long as you load the package templates after you add package-tap.i18n 801 | and before you start adding the languages files you won't need to register templates yourself. 802 | 803 | ### Using tap-i18n in Your Package Templates 804 | 805 | See "The tap-i18n helper" section above. 806 | 807 | ## Unit Testing 808 | 809 | See /unittest/test-packages/README.md . 810 | 811 | ## License 812 | 813 | MIT 814 | 815 | ## Author 816 | 817 | [Daniel Chcouri](https://www.linkedin.com/in/danielchcouri/) 818 | 819 | ## Contributors 820 | 821 | * [Chris Hitchcott](https://github.com/hitchcott/) 822 | * [Brian Chan](http://github.com/iovecoldpizza) 823 | * [Kevin Iamburg](http://www.slickdevelopment.com) 824 | * [Abe Pazos](https://github.com/hamoid/) 825 | * [@karfield](https://github.com/karfield/) 826 | * [@nscarcella](https://github.com/nscarcella/) 827 | * [@mpowaga](https://github.com/mpowaga/) 828 | 829 | ## Credits 830 | 831 | * [i18next v1.11](http://i18next.github.io/i18next/) 832 | * [simple-schema](https://github.com/aldeed/meteor-simple-schema) 833 | * [http-methods](https://github.com/CollectionFS/Meteor-http-methods) 834 | -------------------------------------------------------------------------------- /lib/tap_i18next/tap_i18next-1.7.3.js: -------------------------------------------------------------------------------- 1 | // tap_i18next is a copy of i18next that expose i18next to the global namespace 2 | // under the name name TAPi18next instead of i18n to (1) avoid interfering with other 3 | // Meteor packages that might use i18n with different configurations than we do 4 | // or worse - (2) using a different version of i18next 5 | // 6 | // setJqueryExt is disabled by default in TAPi18next 7 | // sprintf is a default postProcess in TAPi18next 8 | // 9 | // TAPi18next is set outside of the singleton builder to make it available in the 10 | // package level 11 | 12 | // i18next, v1.7.3 13 | // Copyright (c)2014 Jan Mühlemann (jamuhl). 14 | // Distributed under MIT license 15 | // http://i18next.com 16 | 17 | // set TAPi18next outside of the singleton builder to make it available in the package level 18 | TAPi18next = {}; 19 | (function() { 20 | 21 | // add indexOf to non ECMA-262 standard compliant browsers 22 | if (!Array.prototype.indexOf) { 23 | Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { 24 | "use strict"; 25 | if (this == null) { 26 | throw new TypeError(); 27 | } 28 | var t = Object(this); 29 | var len = t.length >>> 0; 30 | if (len === 0) { 31 | return -1; 32 | } 33 | var n = 0; 34 | if (arguments.length > 0) { 35 | n = Number(arguments[1]); 36 | if (n != n) { // shortcut for verifying if it's NaN 37 | n = 0; 38 | } else if (n != 0 && n != Infinity && n != -Infinity) { 39 | n = (n > 0 || -1) * Math.floor(Math.abs(n)); 40 | } 41 | } 42 | if (n >= len) { 43 | return -1; 44 | } 45 | var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); 46 | for (; k < len; k++) { 47 | if (k in t && t[k] === searchElement) { 48 | return k; 49 | } 50 | } 51 | return -1; 52 | } 53 | } 54 | 55 | // add lastIndexOf to non ECMA-262 standard compliant browsers 56 | if (!Array.prototype.lastIndexOf) { 57 | Array.prototype.lastIndexOf = function(searchElement /*, fromIndex*/) { 58 | "use strict"; 59 | if (this == null) { 60 | throw new TypeError(); 61 | } 62 | var t = Object(this); 63 | var len = t.length >>> 0; 64 | if (len === 0) { 65 | return -1; 66 | } 67 | var n = len; 68 | if (arguments.length > 1) { 69 | n = Number(arguments[1]); 70 | if (n != n) { 71 | n = 0; 72 | } else if (n != 0 && n != (1 / 0) && n != -(1 / 0)) { 73 | n = (n > 0 || -1) * Math.floor(Math.abs(n)); 74 | } 75 | } 76 | var k = n >= 0 ? Math.min(n, len - 1) : len - Math.abs(n); 77 | for (; k >= 0; k--) { 78 | if (k in t && t[k] === searchElement) { 79 | return k; 80 | } 81 | } 82 | return -1; 83 | }; 84 | } 85 | 86 | // Add string trim for IE8. 87 | if (typeof String.prototype.trim !== 'function') { 88 | String.prototype.trim = function() { 89 | return this.replace(/^\s+|\s+$/g, ''); 90 | } 91 | } 92 | 93 | var root = this 94 | , $ = root.jQuery || root.Zepto 95 | , resStore = {} 96 | , currentLng 97 | , replacementCounter = 0 98 | , languages = [] 99 | , initialized = false; 100 | 101 | 102 | // Export the i18next object for **CommonJS**. 103 | // If we're not in CommonJS, add `i18n` to the 104 | // global object or to jquery. 105 | if (typeof module !== 'undefined' && module.exports) { 106 | module.exports = TAPi18next; 107 | } else { 108 | if ($) { 109 | $.TAPi18next = $.TAPi18next || TAPi18next; 110 | } 111 | 112 | root.TAPi18next = root.TAPi18next || TAPi18next; 113 | } 114 | // defaults 115 | var o = { 116 | lng: undefined, 117 | load: 'all', 118 | preload: [], 119 | lowerCaseLng: false, 120 | returnObjectTrees: false, 121 | fallbackLng: ['dev'], 122 | fallbackNS: [], 123 | detectLngQS: 'setLng', 124 | ns: 'translation', 125 | fallbackOnNull: true, 126 | fallbackOnEmpty: false, 127 | fallbackToDefaultNS: false, 128 | nsseparator: ':', 129 | keyseparator: '.', 130 | selectorAttr: 'data-i18n', 131 | debug: false, 132 | 133 | resGetPath: 'locales/__lng__/__ns__.json', 134 | resPostPath: 'locales/add/__lng__/__ns__', 135 | 136 | getAsync: true, 137 | postAsync: true, 138 | 139 | resStore: undefined, 140 | useLocalStorage: false, 141 | localStorageExpirationTime: 7*24*60*60*1000, 142 | 143 | dynamicLoad: false, 144 | sendMissing: false, 145 | sendMissingTo: 'fallback', // current | all 146 | sendType: 'POST', 147 | 148 | interpolationPrefix: '__', 149 | interpolationSuffix: '__', 150 | reusePrefix: '$t(', 151 | reuseSuffix: ')', 152 | pluralSuffix: '_plural', 153 | pluralNotFound: ['plural_not_found', Math.random()].join(''), 154 | contextNotFound: ['context_not_found', Math.random()].join(''), 155 | escapeInterpolation: false, 156 | 157 | setJqueryExt: false, 158 | defaultValueFromContent: true, 159 | useDataAttrOptions: false, 160 | cookieExpirationTime: undefined, 161 | useCookie: true, 162 | cookieName: 'TAPi18next', 163 | cookieDomain: undefined, 164 | 165 | objectTreeKeyHandler: undefined, 166 | postProcess: ["sprintf"], 167 | parseMissingKey: undefined, 168 | 169 | shortcutFunction: 'sprintf' // or: defaultValue 170 | }; 171 | function _extend(target, source) { 172 | if (!source || typeof source === 'function') { 173 | return target; 174 | } 175 | 176 | for (var attr in source) { target[attr] = source[attr]; } 177 | return target; 178 | } 179 | 180 | function _each(object, callback, args) { 181 | var name, i = 0, 182 | length = object.length, 183 | isObj = length === undefined || Object.prototype.toString.apply(object) !== '[object Array]' || typeof object === "function"; 184 | 185 | if (args) { 186 | if (isObj) { 187 | for (name in object) { 188 | if (callback.apply(object[name], args) === false) { 189 | break; 190 | } 191 | } 192 | } else { 193 | for ( ; i < length; ) { 194 | if (callback.apply(object[i++], args) === false) { 195 | break; 196 | } 197 | } 198 | } 199 | 200 | // A special, fast, case for the most common use of each 201 | } else { 202 | if (isObj) { 203 | for (name in object) { 204 | if (callback.call(object[name], name, object[name]) === false) { 205 | break; 206 | } 207 | } 208 | } else { 209 | for ( ; i < length; ) { 210 | if (callback.call(object[i], i, object[i++]) === false) { 211 | break; 212 | } 213 | } 214 | } 215 | } 216 | 217 | return object; 218 | } 219 | 220 | var _entityMap = { 221 | "&": "&", 222 | "<": "<", 223 | ">": ">", 224 | '"': '"', 225 | "'": ''', 226 | "/": '/' 227 | }; 228 | 229 | function _escape(data) { 230 | if (typeof data === 'string') { 231 | return data.replace(/[&<>"'\/]/g, function (s) { 232 | return _entityMap[s]; 233 | }); 234 | }else{ 235 | return data; 236 | } 237 | } 238 | 239 | function _ajax(options) { 240 | 241 | // v0.5.0 of https://github.com/goloroden/http.js 242 | var getXhr = function (callback) { 243 | // Use the native XHR object if the browser supports it. 244 | if (window.XMLHttpRequest) { 245 | return callback(null, new XMLHttpRequest()); 246 | } else if (window.ActiveXObject) { 247 | // In Internet Explorer check for ActiveX versions of the XHR object. 248 | try { 249 | return callback(null, new ActiveXObject("Msxml2.XMLHTTP")); 250 | } catch (e) { 251 | return callback(null, new ActiveXObject("Microsoft.XMLHTTP")); 252 | } 253 | } 254 | 255 | // If no XHR support was found, throw an error. 256 | return callback(new Error()); 257 | }; 258 | 259 | var encodeUsingUrlEncoding = function (data) { 260 | if(typeof data === 'string') { 261 | return data; 262 | } 263 | 264 | var result = []; 265 | for(var dataItem in data) { 266 | if(data.hasOwnProperty(dataItem)) { 267 | result.push(encodeURIComponent(dataItem) + '=' + encodeURIComponent(data[dataItem])); 268 | } 269 | } 270 | 271 | return result.join('&'); 272 | }; 273 | 274 | var utf8 = function (text) { 275 | text = text.replace(/\r\n/g, '\n'); 276 | var result = ''; 277 | 278 | for(var i = 0; i < text.length; i++) { 279 | var c = text.charCodeAt(i); 280 | 281 | if(c < 128) { 282 | result += String.fromCharCode(c); 283 | } else if((c > 127) && (c < 2048)) { 284 | result += String.fromCharCode((c >> 6) | 192); 285 | result += String.fromCharCode((c & 63) | 128); 286 | } else { 287 | result += String.fromCharCode((c >> 12) | 224); 288 | result += String.fromCharCode(((c >> 6) & 63) | 128); 289 | result += String.fromCharCode((c & 63) | 128); 290 | } 291 | } 292 | 293 | return result; 294 | }; 295 | 296 | var base64 = function (text) { 297 | var keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; 298 | 299 | text = utf8(text); 300 | var result = '', 301 | chr1, chr2, chr3, 302 | enc1, enc2, enc3, enc4, 303 | i = 0; 304 | 305 | do { 306 | chr1 = text.charCodeAt(i++); 307 | chr2 = text.charCodeAt(i++); 308 | chr3 = text.charCodeAt(i++); 309 | 310 | enc1 = chr1 >> 2; 311 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 312 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 313 | enc4 = chr3 & 63; 314 | 315 | if(isNaN(chr2)) { 316 | enc3 = enc4 = 64; 317 | } else if(isNaN(chr3)) { 318 | enc4 = 64; 319 | } 320 | 321 | result += 322 | keyStr.charAt(enc1) + 323 | keyStr.charAt(enc2) + 324 | keyStr.charAt(enc3) + 325 | keyStr.charAt(enc4); 326 | chr1 = chr2 = chr3 = ''; 327 | enc1 = enc2 = enc3 = enc4 = ''; 328 | } while(i < text.length); 329 | 330 | return result; 331 | }; 332 | 333 | var mergeHeaders = function () { 334 | // Use the first header object as base. 335 | var result = arguments[0]; 336 | 337 | // Iterate through the remaining header objects and add them. 338 | for(var i = 1; i < arguments.length; i++) { 339 | var currentHeaders = arguments[i]; 340 | for(var header in currentHeaders) { 341 | if(currentHeaders.hasOwnProperty(header)) { 342 | result[header] = currentHeaders[header]; 343 | } 344 | } 345 | } 346 | 347 | // Return the merged headers. 348 | return result; 349 | }; 350 | 351 | var ajax = function (method, url, options, callback) { 352 | // Adjust parameters. 353 | if(typeof options === 'function') { 354 | callback = options; 355 | options = {}; 356 | } 357 | 358 | // Set default parameter values. 359 | options.cache = options.cache || false; 360 | options.data = options.data || {}; 361 | options.headers = options.headers || {}; 362 | options.jsonp = options.jsonp || false; 363 | options.async = options.async === undefined ? true : options.async; 364 | 365 | // Merge the various header objects. 366 | var headers = mergeHeaders({ 367 | 'accept': '*/*', 368 | 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8' 369 | }, ajax.headers, options.headers); 370 | 371 | // Encode the data according to the content-type. 372 | var payload; 373 | if (headers['content-type'] === 'application/json') { 374 | payload = JSON.stringify(options.data); 375 | } else { 376 | payload = encodeUsingUrlEncoding(options.data); 377 | } 378 | 379 | // Specially prepare GET requests: Setup the query string, handle caching and make a JSONP call 380 | // if neccessary. 381 | if(method === 'GET') { 382 | // Setup the query string. 383 | var queryString = []; 384 | if(payload) { 385 | queryString.push(payload); 386 | payload = null; 387 | } 388 | 389 | // Handle caching. 390 | if(!options.cache) { 391 | queryString.push('_=' + (new Date()).getTime()); 392 | } 393 | 394 | // If neccessary prepare the query string for a JSONP call. 395 | if(options.jsonp) { 396 | queryString.push('callback=' + options.jsonp); 397 | queryString.push('jsonp=' + options.jsonp); 398 | } 399 | 400 | // Merge the query string and attach it to the url. 401 | queryString = queryString.join('&'); 402 | if (queryString.length > 1) { 403 | if (url.indexOf('?') > -1) { 404 | url += '&' + queryString; 405 | } else { 406 | url += '?' + queryString; 407 | } 408 | } 409 | 410 | // Make a JSONP call if neccessary. 411 | if(options.jsonp) { 412 | var head = document.getElementsByTagName('head')[0]; 413 | var script = document.createElement('script'); 414 | script.type = 'text/javascript'; 415 | script.src = url; 416 | head.appendChild(script); 417 | return; 418 | } 419 | } 420 | 421 | // Since we got here, it is no JSONP request, so make a normal XHR request. 422 | getXhr(function (err, xhr) { 423 | if(err) return callback(err); 424 | 425 | // Open the request. 426 | xhr.open(method, url, options.async); 427 | 428 | // Set the request headers. 429 | for(var header in headers) { 430 | if(headers.hasOwnProperty(header)) { 431 | xhr.setRequestHeader(header, headers[header]); 432 | } 433 | } 434 | 435 | // Handle the request events. 436 | xhr.onreadystatechange = function () { 437 | if(xhr.readyState === 4) { 438 | var data = xhr.responseText || ''; 439 | 440 | // If no callback is given, return. 441 | if(!callback) { 442 | return; 443 | } 444 | 445 | // Return an object that provides access to the data as text and JSON. 446 | callback(xhr.status, { 447 | text: function () { 448 | return data; 449 | }, 450 | 451 | json: function () { 452 | return JSON.parse(data); 453 | } 454 | }); 455 | } 456 | }; 457 | 458 | // Actually send the XHR request. 459 | xhr.send(payload); 460 | }); 461 | }; 462 | 463 | // Define the external interface. 464 | var http = { 465 | authBasic: function (username, password) { 466 | ajax.headers['Authorization'] = 'Basic ' + base64(username + ':' + password); 467 | }, 468 | 469 | connect: function (url, options, callback) { 470 | return ajax('CONNECT', url, options, callback); 471 | }, 472 | 473 | del: function (url, options, callback) { 474 | return ajax('DELETE', url, options, callback); 475 | }, 476 | 477 | get: function (url, options, callback) { 478 | return ajax('GET', url, options, callback); 479 | }, 480 | 481 | head: function (url, options, callback) { 482 | return ajax('HEAD', url, options, callback); 483 | }, 484 | 485 | headers: function (headers) { 486 | ajax.headers = headers || {}; 487 | }, 488 | 489 | isAllowed: function (url, verb, callback) { 490 | this.options(url, function (status, data) { 491 | callback(data.text().indexOf(verb) !== -1); 492 | }); 493 | }, 494 | 495 | options: function (url, options, callback) { 496 | return ajax('OPTIONS', url, options, callback); 497 | }, 498 | 499 | patch: function (url, options, callback) { 500 | return ajax('PATCH', url, options, callback); 501 | }, 502 | 503 | post: function (url, options, callback) { 504 | return ajax('POST', url, options, callback); 505 | }, 506 | 507 | put: function (url, options, callback) { 508 | return ajax('PUT', url, options, callback); 509 | }, 510 | 511 | trace: function (url, options, callback) { 512 | return ajax('TRACE', url, options, callback); 513 | } 514 | }; 515 | 516 | 517 | var methode = options.type ? options.type.toLowerCase() : 'get'; 518 | 519 | http[methode](options.url, options, function (status, data) { 520 | if (status === 200) { 521 | options.success(data.json(), status, null); 522 | } else { 523 | options.error(data.text(), status, null); 524 | } 525 | }); 526 | } 527 | 528 | var _cookie = { 529 | create: function(name,value,minutes,domain) { 530 | var expires; 531 | if (minutes) { 532 | var date = new Date(); 533 | date.setTime(date.getTime()+(minutes*60*1000)); 534 | expires = "; expires="+date.toGMTString(); 535 | } 536 | else expires = ""; 537 | domain = (domain)? "domain="+domain+";" : ""; 538 | document.cookie = name+"="+value+expires+";"+domain+"path=/"; 539 | }, 540 | 541 | read: function(name) { 542 | var nameEQ = name + "="; 543 | var ca = document.cookie.split(';'); 544 | for(var i=0;i < ca.length;i++) { 545 | var c = ca[i]; 546 | while (c.charAt(0)==' ') c = c.substring(1,c.length); 547 | if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length,c.length); 548 | } 549 | return null; 550 | }, 551 | 552 | remove: function(name) { 553 | this.create(name,"",-1); 554 | } 555 | }; 556 | 557 | var cookie_noop = { 558 | create: function(name,value,minutes,domain) {}, 559 | read: function(name) { return null; }, 560 | remove: function(name) {} 561 | }; 562 | 563 | 564 | 565 | // move dependent functions to a container so that 566 | // they can be overriden easier in no jquery environment (node.js) 567 | var f = { 568 | extend: $ ? $.extend : _extend, 569 | each: $ ? $.each : _each, 570 | ajax: $ ? $.ajax : (typeof document !== 'undefined' ? _ajax : function() {}), 571 | cookie: typeof document !== 'undefined' ? _cookie : cookie_noop, 572 | detectLanguage: detectLanguage, 573 | escape: _escape, 574 | log: function(str) { 575 | if (o.debug && typeof console !== "undefined") console.log(str); 576 | }, 577 | toLanguages: function(lng) { 578 | var languages = []; 579 | if (typeof lng === 'string' && lng.indexOf('-') > -1) { 580 | var parts = lng.split('-'); 581 | 582 | lng = o.lowerCaseLng ? 583 | parts[0].toLowerCase() + '-' + parts[1].toLowerCase() : 584 | parts[0].toLowerCase() + '-' + parts[1].toUpperCase(); 585 | 586 | if (o.load !== 'unspecific') languages.push(lng); 587 | if (o.load !== 'current') languages.push(parts[0]); 588 | } else { 589 | languages.push(lng); 590 | } 591 | 592 | for (var i = 0; i < o.fallbackLng.length; i++) { 593 | if (languages.indexOf(o.fallbackLng[i]) === -1 && o.fallbackLng[i]) languages.push(o.fallbackLng[i]); 594 | } 595 | 596 | return languages; 597 | }, 598 | regexEscape: function(str) { 599 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 600 | } 601 | }; 602 | function init(options, cb) { 603 | 604 | if (typeof options === 'function') { 605 | cb = options; 606 | options = {}; 607 | } 608 | options = options || {}; 609 | 610 | // override defaults with passed in options 611 | f.extend(o, options); 612 | delete o.fixLng; /* passed in each time */ 613 | 614 | // create namespace object if namespace is passed in as string 615 | if (typeof o.ns == 'string') { 616 | o.ns = { namespaces: [o.ns], defaultNs: o.ns}; 617 | } 618 | 619 | // fallback namespaces 620 | if (typeof o.fallbackNS == 'string') { 621 | o.fallbackNS = [o.fallbackNS]; 622 | } 623 | 624 | // fallback languages 625 | if (typeof o.fallbackLng == 'string' || typeof o.fallbackLng == 'boolean') { 626 | o.fallbackLng = [o.fallbackLng]; 627 | } 628 | 629 | // escape prefix/suffix 630 | o.interpolationPrefixEscaped = f.regexEscape(o.interpolationPrefix); 631 | o.interpolationSuffixEscaped = f.regexEscape(o.interpolationSuffix); 632 | 633 | if (!o.lng) o.lng = f.detectLanguage(); 634 | if (o.lng) { 635 | // set cookie with lng set (as detectLanguage will set cookie on need) 636 | if (o.useCookie) f.cookie.create(o.cookieName, o.lng, o.cookieExpirationTime, o.cookieDomain); 637 | } else { 638 | o.lng = o.fallbackLng[0]; 639 | if (o.useCookie) f.cookie.remove(o.cookieName); 640 | } 641 | 642 | languages = f.toLanguages(o.lng); 643 | currentLng = languages[0]; 644 | f.log('currentLng set to: ' + currentLng); 645 | 646 | var lngTranslate = translate; 647 | if (options.fixLng) { 648 | lngTranslate = function(key, options) { 649 | if (typeof options !== "undefined") { 650 | options = Object.create(options); 651 | } else { 652 | options = {}; 653 | } 654 | 655 | options.lng = options.lng || lngTranslate.lng; 656 | return translate(key, options); 657 | }; 658 | lngTranslate.lng = currentLng; 659 | } 660 | 661 | pluralExtensions.setCurrentLng(currentLng); 662 | 663 | // add JQuery extensions 664 | if ($ && o.setJqueryExt) addJqueryFunct(); 665 | 666 | // jQuery deferred 667 | var deferred; 668 | if ($ && $.Deferred) { 669 | deferred = $.Deferred(); 670 | } 671 | 672 | // return immidiatly if res are passed in 673 | if (o.resStore) { 674 | resStore = o.resStore; 675 | initialized = true; 676 | if (cb) cb(lngTranslate); 677 | if (deferred) deferred.resolve(lngTranslate); 678 | if (deferred) return deferred.promise(); 679 | return; 680 | } 681 | 682 | // languages to load 683 | var lngsToLoad = f.toLanguages(o.lng); 684 | if (typeof o.preload === 'string') o.preload = [o.preload]; 685 | for (var i = 0, l = o.preload.length; i < l; i++) { 686 | var pres = f.toLanguages(o.preload[i]); 687 | for (var y = 0, len = pres.length; y < len; y++) { 688 | if (lngsToLoad.indexOf(pres[y]) < 0) { 689 | lngsToLoad.push(pres[y]); 690 | } 691 | } 692 | } 693 | 694 | // else load them 695 | TAPi18next.sync.load(lngsToLoad, o, function(err, store) { 696 | resStore = store; 697 | initialized = true; 698 | 699 | if (cb) cb(lngTranslate); 700 | if (deferred) deferred.resolve(lngTranslate); 701 | }); 702 | 703 | if (deferred) return deferred.promise(); 704 | } 705 | function preload(lngs, cb) { 706 | if (typeof lngs === 'string') lngs = [lngs]; 707 | for (var i = 0, l = lngs.length; i < l; i++) { 708 | if (o.preload.indexOf(lngs[i]) < 0) { 709 | o.preload.push(lngs[i]); 710 | } 711 | } 712 | return init(cb); 713 | } 714 | 715 | function addResourceBundle(lng, ns, resources) { 716 | if (typeof ns !== 'string') { 717 | resources = ns; 718 | ns = o.ns.defaultNs; 719 | } else if (o.ns.namespaces.indexOf(ns) < 0) { 720 | o.ns.namespaces.push(ns); 721 | } 722 | 723 | resStore[lng] = resStore[lng] || {}; 724 | resStore[lng][ns] = resStore[lng][ns] || {}; 725 | 726 | f.extend(resStore[lng][ns], resources); 727 | } 728 | 729 | function removeResourceBundle(lng, ns) { 730 | if (typeof ns !== 'string') { 731 | ns = o.ns.defaultNs; 732 | } 733 | 734 | resStore[lng] = resStore[lng] || {}; 735 | resStore[lng][ns] = {}; 736 | } 737 | 738 | function setDefaultNamespace(ns) { 739 | o.ns.defaultNs = ns; 740 | } 741 | 742 | function loadNamespace(namespace, cb) { 743 | loadNamespaces([namespace], cb); 744 | } 745 | 746 | function loadNamespaces(namespaces, cb) { 747 | var opts = { 748 | dynamicLoad: o.dynamicLoad, 749 | resGetPath: o.resGetPath, 750 | getAsync: o.getAsync, 751 | customLoad: o.customLoad, 752 | ns: { namespaces: namespaces, defaultNs: ''} /* new namespaces to load */ 753 | }; 754 | 755 | // languages to load 756 | var lngsToLoad = f.toLanguages(o.lng); 757 | if (typeof o.preload === 'string') o.preload = [o.preload]; 758 | for (var i = 0, l = o.preload.length; i < l; i++) { 759 | var pres = f.toLanguages(o.preload[i]); 760 | for (var y = 0, len = pres.length; y < len; y++) { 761 | if (lngsToLoad.indexOf(pres[y]) < 0) { 762 | lngsToLoad.push(pres[y]); 763 | } 764 | } 765 | } 766 | 767 | // check if we have to load 768 | var lngNeedLoad = []; 769 | for (var a = 0, lenA = lngsToLoad.length; a < lenA; a++) { 770 | var needLoad = false; 771 | var resSet = resStore[lngsToLoad[a]]; 772 | if (resSet) { 773 | for (var b = 0, lenB = namespaces.length; b < lenB; b++) { 774 | if (!resSet[namespaces[b]]) needLoad = true; 775 | } 776 | } else { 777 | needLoad = true; 778 | } 779 | 780 | if (needLoad) lngNeedLoad.push(lngsToLoad[a]); 781 | } 782 | 783 | if (lngNeedLoad.length) { 784 | TAPi18next.sync._fetch(lngNeedLoad, opts, function(err, store) { 785 | var todo = namespaces.length * lngNeedLoad.length; 786 | 787 | // load each file individual 788 | f.each(namespaces, function(nsIndex, nsValue) { 789 | 790 | // append namespace to namespace array 791 | if (o.ns.namespaces.indexOf(nsValue) < 0) { 792 | o.ns.namespaces.push(nsValue); 793 | } 794 | 795 | f.each(lngNeedLoad, function(lngIndex, lngValue) { 796 | resStore[lngValue] = resStore[lngValue] || {}; 797 | resStore[lngValue][nsValue] = store[lngValue][nsValue]; 798 | 799 | todo--; // wait for all done befor callback 800 | if (todo === 0 && cb) { 801 | if (o.useLocalStorage) TAPi18next.sync._storeLocal(resStore); 802 | cb(); 803 | } 804 | }); 805 | }); 806 | }); 807 | } else { 808 | if (cb) cb(); 809 | } 810 | } 811 | 812 | function setLng(lng, options, cb) { 813 | if (typeof options === 'function') { 814 | cb = options; 815 | options = {}; 816 | } else if (!options) { 817 | options = {}; 818 | } 819 | 820 | options.lng = lng; 821 | return init(options, cb); 822 | } 823 | 824 | function lng() { 825 | return currentLng; 826 | } 827 | function addJqueryFunct() { 828 | // $.t shortcut 829 | $.t = $.t || translate; 830 | 831 | function parse(ele, key, options) { 832 | if (key.length === 0) return; 833 | 834 | var attr = 'text'; 835 | 836 | if (key.indexOf('[') === 0) { 837 | var parts = key.split(']'); 838 | key = parts[1]; 839 | attr = parts[0].substr(1, parts[0].length-1); 840 | } 841 | 842 | if (key.indexOf(';') === key.length-1) { 843 | key = key.substr(0, key.length-2); 844 | } 845 | 846 | var optionsToUse; 847 | if (attr === 'html') { 848 | optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.html() }, options) : options; 849 | ele.html($.t(key, optionsToUse)); 850 | } else if (attr === 'text') { 851 | optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.text() }, options) : options; 852 | ele.text($.t(key, optionsToUse)); 853 | } else if (attr === 'prepend') { 854 | optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.html() }, options) : options; 855 | ele.prepend($.t(key, optionsToUse)); 856 | } else if (attr === 'append') { 857 | optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.html() }, options) : options; 858 | ele.append($.t(key, optionsToUse)); 859 | } else if (attr.indexOf("data-") === 0) { 860 | var dataAttr = attr.substr(("data-").length); 861 | optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.data(dataAttr) }, options) : options; 862 | var translated = $.t(key, optionsToUse); 863 | //we change into the data cache 864 | ele.data(dataAttr, translated); 865 | //we change into the dom 866 | ele.attr(attr, translated); 867 | } else { 868 | optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.attr(attr) }, options) : options; 869 | ele.attr(attr, $.t(key, optionsToUse)); 870 | } 871 | } 872 | 873 | function localize(ele, options) { 874 | var key = ele.attr(o.selectorAttr); 875 | if (!key && typeof key !== 'undefined' && key !== false) key = ele.text() || ele.val(); 876 | if (!key) return; 877 | 878 | var target = ele 879 | , targetSelector = ele.data("i18n-target"); 880 | if (targetSelector) { 881 | target = ele.find(targetSelector) || ele; 882 | } 883 | 884 | if (!options && o.useDataAttrOptions === true) { 885 | options = ele.data("i18n-options"); 886 | } 887 | options = options || {}; 888 | 889 | if (key.indexOf(';') >= 0) { 890 | var keys = key.split(';'); 891 | 892 | $.each(keys, function(m, k) { 893 | if (k !== '') parse(target, k, options); 894 | }); 895 | 896 | } else { 897 | parse(target, key, options); 898 | } 899 | 900 | if (o.useDataAttrOptions === true) ele.data("i18n-options", options); 901 | } 902 | 903 | // fn 904 | $.fn.TAPi18next = function (options) { 905 | return this.each(function() { 906 | // localize element itself 907 | localize($(this), options); 908 | 909 | // localize childs 910 | var elements = $(this).find('[' + o.selectorAttr + ']'); 911 | elements.each(function() { 912 | localize($(this), options); 913 | }); 914 | }); 915 | }; 916 | } 917 | function applyReplacement(str, replacementHash, nestedKey, options) { 918 | if (!str) return str; 919 | 920 | options = options || replacementHash; // first call uses replacement hash combined with options 921 | if (str.indexOf(options.interpolationPrefix || o.interpolationPrefix) < 0) return str; 922 | 923 | var prefix = options.interpolationPrefix ? f.regexEscape(options.interpolationPrefix) : o.interpolationPrefixEscaped 924 | , suffix = options.interpolationSuffix ? f.regexEscape(options.interpolationSuffix) : o.interpolationSuffixEscaped 925 | , unEscapingSuffix = 'HTML'+suffix; 926 | 927 | f.each(replacementHash, function(key, value) { 928 | var nextKey = nestedKey ? nestedKey + o.keyseparator + key : key; 929 | if (typeof value === 'object' && value !== null) { 930 | str = applyReplacement(str, value, nextKey, options); 931 | } else { 932 | if (options.escapeInterpolation || o.escapeInterpolation) { 933 | str = str.replace(new RegExp([prefix, nextKey, unEscapingSuffix].join(''), 'g'), value); 934 | str = str.replace(new RegExp([prefix, nextKey, suffix].join(''), 'g'), f.escape(value)); 935 | } else { 936 | str = str.replace(new RegExp([prefix, nextKey, suffix].join(''), 'g'), value); 937 | } 938 | // str = options.escapeInterpolation; 939 | } 940 | }); 941 | return str; 942 | } 943 | 944 | // append it to functions 945 | f.applyReplacement = applyReplacement; 946 | 947 | function applyReuse(translated, options) { 948 | var comma = ','; 949 | var options_open = '{'; 950 | var options_close = '}'; 951 | 952 | var opts = f.extend({}, options); 953 | delete opts.postProcess; 954 | 955 | while (translated.indexOf(o.reusePrefix) != -1) { 956 | replacementCounter++; 957 | if (replacementCounter > o.maxRecursion) { break; } // safety net for too much recursion 958 | var index_of_opening = translated.lastIndexOf(o.reusePrefix); 959 | var index_of_end_of_closing = translated.indexOf(o.reuseSuffix, index_of_opening) + o.reuseSuffix.length; 960 | var token = translated.substring(index_of_opening, index_of_end_of_closing); 961 | var token_without_symbols = token.replace(o.reusePrefix, '').replace(o.reuseSuffix, ''); 962 | 963 | 964 | if (token_without_symbols.indexOf(comma) != -1) { 965 | var index_of_token_end_of_closing = token_without_symbols.indexOf(comma); 966 | if (token_without_symbols.indexOf(options_open, index_of_token_end_of_closing) != -1 && token_without_symbols.indexOf(options_close, index_of_token_end_of_closing) != -1) { 967 | var index_of_opts_opening = token_without_symbols.indexOf(options_open, index_of_token_end_of_closing); 968 | var index_of_opts_end_of_closing = token_without_symbols.indexOf(options_close, index_of_opts_opening) + options_close.length; 969 | try { 970 | opts = f.extend(opts, JSON.parse(token_without_symbols.substring(index_of_opts_opening, index_of_opts_end_of_closing))); 971 | token_without_symbols = token_without_symbols.substring(0, index_of_token_end_of_closing); 972 | } catch (e) { 973 | } 974 | } 975 | } 976 | 977 | var translated_token = _translate(token_without_symbols, opts); 978 | translated = translated.replace(token, translated_token); 979 | } 980 | return translated; 981 | } 982 | 983 | function hasContext(options) { 984 | return (options.context && (typeof options.context == 'string' || typeof options.context == 'number')); 985 | } 986 | 987 | function needsPlural(options) { 988 | return (options.count !== undefined && typeof options.count != 'string' && options.count !== 1); 989 | } 990 | 991 | function exists(key, options) { 992 | options = options || {}; 993 | 994 | var notFound = _getDefaultValue(key, options) 995 | , found = _find(key, options); 996 | 997 | return found !== undefined || found === notFound; 998 | } 999 | 1000 | function translate(key, options) { 1001 | if (typeof options === 'undefined') { 1002 | options = {}; 1003 | } 1004 | 1005 | if (!initialized) { 1006 | f.log('i18next not finished initialization. you might have called t function before loading resources finished.') 1007 | return options.defaultValue || ''; 1008 | }; 1009 | replacementCounter = 0; 1010 | return _translate.apply(null, arguments); 1011 | } 1012 | 1013 | function _getDefaultValue(key, options) { 1014 | return (options.defaultValue !== undefined) ? options.defaultValue : key; 1015 | } 1016 | 1017 | function _injectSprintfProcessor() { 1018 | 1019 | var values = []; 1020 | 1021 | // mh: build array from second argument onwards 1022 | for (var i = 1; i < arguments.length; i++) { 1023 | values.push(arguments[i]); 1024 | } 1025 | 1026 | return { 1027 | postProcess: 'sprintf', 1028 | sprintf: values 1029 | }; 1030 | } 1031 | 1032 | function _translate(potentialKeys, options) { 1033 | if (typeof options !== "undefined" && options !== null && typeof options !== 'object') { 1034 | if (o.shortcutFunction === 'sprintf') { 1035 | // mh: gettext like sprintf syntax found, automatically create sprintf processor 1036 | options = _injectSprintfProcessor.apply(null, arguments); 1037 | } else if (o.shortcutFunction === 'defaultValue') { 1038 | options = { 1039 | defaultValue: options 1040 | } 1041 | } 1042 | } else { 1043 | options = options || {}; 1044 | } 1045 | 1046 | if (potentialKeys === undefined || potentialKeys === null) return ''; 1047 | 1048 | if (typeof potentialKeys == 'string') { 1049 | potentialKeys = [potentialKeys]; 1050 | } 1051 | 1052 | var key = potentialKeys[0]; 1053 | 1054 | if (potentialKeys.length > 1) { 1055 | for (var i = 0; i < potentialKeys.length; i++) { 1056 | key = potentialKeys[i]; 1057 | if (exists(key, options)) { 1058 | break; 1059 | } 1060 | } 1061 | } 1062 | 1063 | var notFound = _getDefaultValue(key, options) 1064 | , found = _find(key, options) 1065 | , lngs = options.lng ? f.toLanguages(options.lng) : languages 1066 | , ns = options.ns || o.ns.defaultNs 1067 | , parts; 1068 | 1069 | // split ns and key 1070 | if (key.indexOf(o.nsseparator) > -1) { 1071 | parts = key.split(o.nsseparator); 1072 | ns = parts[0]; 1073 | key = parts[1]; 1074 | } 1075 | 1076 | if (found === undefined && o.sendMissing) { 1077 | if (options.lng) { 1078 | sync.postMissing(lngs[0], ns, key, notFound, lngs); 1079 | } else { 1080 | sync.postMissing(o.lng, ns, key, notFound, lngs); 1081 | } 1082 | } 1083 | 1084 | var postProcessor = options.postProcess || o.postProcess; 1085 | if (found !== undefined && postProcessor) { 1086 | if (postProcessors[postProcessor]) { 1087 | found = postProcessors[postProcessor](found, key, options); 1088 | } 1089 | } 1090 | 1091 | // process notFound if function exists 1092 | var splitNotFound = notFound; 1093 | if (notFound.indexOf(o.nsseparator) > -1) { 1094 | parts = notFound.split(o.nsseparator); 1095 | splitNotFound = parts[1]; 1096 | } 1097 | if (splitNotFound === key && o.parseMissingKey) { 1098 | notFound = o.parseMissingKey(notFound); 1099 | } 1100 | 1101 | if (found === undefined) { 1102 | notFound = applyReplacement(notFound, options); 1103 | notFound = applyReuse(notFound, options); 1104 | 1105 | if (postProcessor && postProcessors[postProcessor]) { 1106 | var val = _getDefaultValue(key, options); 1107 | found = postProcessors[postProcessor](val, key, options); 1108 | } 1109 | } 1110 | 1111 | return (found !== undefined) ? found : notFound; 1112 | } 1113 | 1114 | function _find(key, options) { 1115 | options = options || {}; 1116 | 1117 | var optionWithoutCount, translated 1118 | , notFound = _getDefaultValue(key, options) 1119 | , lngs = languages; 1120 | 1121 | if (!resStore) { return notFound; } // no resStore to translate from 1122 | 1123 | // CI mode 1124 | if (lngs[0].toLowerCase() === 'cimode') return notFound; 1125 | 1126 | // passed in lng 1127 | if (options.lng) { 1128 | lngs = f.toLanguages(options.lng); 1129 | 1130 | if (!resStore[lngs[0]]) { 1131 | var oldAsync = o.getAsync; 1132 | o.getAsync = false; 1133 | 1134 | TAPi18next.sync.load(lngs, o, function(err, store) { 1135 | f.extend(resStore, store); 1136 | o.getAsync = oldAsync; 1137 | }); 1138 | } 1139 | } 1140 | 1141 | var ns = options.ns || o.ns.defaultNs; 1142 | if (key.indexOf(o.nsseparator) > -1) { 1143 | var parts = key.split(o.nsseparator); 1144 | ns = parts[0]; 1145 | key = parts[1]; 1146 | } 1147 | 1148 | if (hasContext(options)) { 1149 | optionWithoutCount = f.extend({}, options); 1150 | delete optionWithoutCount.context; 1151 | optionWithoutCount.defaultValue = o.contextNotFound; 1152 | 1153 | var contextKey = ns + o.nsseparator + key + '_' + options.context; 1154 | 1155 | translated = translate(contextKey, optionWithoutCount); 1156 | if (translated != o.contextNotFound) { 1157 | return applyReplacement(translated, { context: options.context }); // apply replacement for context only 1158 | } // else continue translation with original/nonContext key 1159 | } 1160 | 1161 | if (needsPlural(options)) { 1162 | optionWithoutCount = f.extend({}, options); 1163 | delete optionWithoutCount.count; 1164 | optionWithoutCount.defaultValue = o.pluralNotFound; 1165 | 1166 | var pluralKey = ns + o.nsseparator + key + o.pluralSuffix; 1167 | var pluralExtension = pluralExtensions.get(lngs[0], options.count); 1168 | if (pluralExtension >= 0) { 1169 | pluralKey = pluralKey + '_' + pluralExtension; 1170 | } else if (pluralExtension === 1) { 1171 | pluralKey = ns + o.nsseparator + key; // singular 1172 | } 1173 | 1174 | translated = translate(pluralKey, optionWithoutCount); 1175 | if (translated != o.pluralNotFound) { 1176 | return applyReplacement(translated, { 1177 | count: options.count, 1178 | interpolationPrefix: options.interpolationPrefix, 1179 | interpolationSuffix: options.interpolationSuffix 1180 | }); // apply replacement for count only 1181 | } // else continue translation with original/singular key 1182 | } 1183 | 1184 | var found; 1185 | var keys = key.split(o.keyseparator); 1186 | for (var i = 0, len = lngs.length; i < len; i++ ) { 1187 | if (found !== undefined) break; 1188 | 1189 | var l = lngs[i]; 1190 | 1191 | var x = 0; 1192 | var value = resStore[l] && resStore[l][ns]; 1193 | while (keys[x]) { 1194 | value = value && value[keys[x]]; 1195 | x++; 1196 | } 1197 | if (value !== undefined) { 1198 | var valueType = Object.prototype.toString.apply(value); 1199 | if (typeof value === 'string') { 1200 | value = applyReplacement(value, options); 1201 | value = applyReuse(value, options); 1202 | } else if (valueType === '[object Array]' && !o.returnObjectTrees && !options.returnObjectTrees) { 1203 | value = value.join('\n'); 1204 | value = applyReplacement(value, options); 1205 | value = applyReuse(value, options); 1206 | } else if (value === null && o.fallbackOnNull === true) { 1207 | value = undefined; 1208 | } else if (value !== null) { 1209 | if (!o.returnObjectTrees && !options.returnObjectTrees) { 1210 | if (o.objectTreeKeyHandler && typeof o.objectTreeKeyHandler == 'function') { 1211 | value = o.objectTreeKeyHandler(key, value, l, ns, options); 1212 | } else { 1213 | value = 'key \'' + ns + ':' + key + ' (' + l + ')\' ' + 1214 | 'returned an object instead of string.'; 1215 | f.log(value); 1216 | } 1217 | } else if (valueType !== '[object Number]' && valueType !== '[object Function]' && valueType !== '[object RegExp]') { 1218 | var copy = (valueType === '[object Array]') ? [] : {}; // apply child translation on a copy 1219 | f.each(value, function(m) { 1220 | copy[m] = _translate(ns + o.nsseparator + key + o.keyseparator + m, options); 1221 | }); 1222 | value = copy; 1223 | } 1224 | } 1225 | 1226 | if (typeof value === 'string' && value.trim() === '' && o.fallbackOnEmpty === true) 1227 | value = undefined; 1228 | 1229 | found = value; 1230 | } 1231 | } 1232 | 1233 | if (found === undefined && !options.isFallbackLookup && (o.fallbackToDefaultNS === true || (o.fallbackNS && o.fallbackNS.length > 0))) { 1234 | // set flag for fallback lookup - avoid recursion 1235 | options.isFallbackLookup = true; 1236 | 1237 | if (o.fallbackNS.length) { 1238 | 1239 | for (var y = 0, lenY = o.fallbackNS.length; y < lenY; y++) { 1240 | found = _find(o.fallbackNS[y] + o.nsseparator + key, options); 1241 | 1242 | if (found) { 1243 | /* compare value without namespace */ 1244 | var foundValue = found.indexOf(o.nsseparator) > -1 ? found.split(o.nsseparator)[1] : found 1245 | , notFoundValue = notFound.indexOf(o.nsseparator) > -1 ? notFound.split(o.nsseparator)[1] : notFound; 1246 | 1247 | if (foundValue !== notFoundValue) break; 1248 | } 1249 | } 1250 | } else { 1251 | found = _find(key, options); // fallback to default NS 1252 | } 1253 | } 1254 | 1255 | return found; 1256 | } 1257 | function detectLanguage() { 1258 | var detectedLng; 1259 | 1260 | // get from qs 1261 | var qsParm = []; 1262 | if (typeof window !== 'undefined') { 1263 | (function() { 1264 | var query = window.location.search.substring(1); 1265 | var parms = query.split('&'); 1266 | for (var i=0; i 0) { 1269 | var key = parms[i].substring(0,pos); 1270 | var val = parms[i].substring(pos+1); 1271 | qsParm[key] = val; 1272 | } 1273 | } 1274 | })(); 1275 | if (qsParm[o.detectLngQS]) { 1276 | detectedLng = qsParm[o.detectLngQS]; 1277 | } 1278 | } 1279 | 1280 | // get from cookie 1281 | if (!detectedLng && typeof document !== 'undefined' && o.useCookie ) { 1282 | var c = f.cookie.read(o.cookieName); 1283 | if (c) detectedLng = c; 1284 | } 1285 | 1286 | // get from navigator 1287 | if (!detectedLng && typeof navigator !== 'undefined') { 1288 | detectedLng = (navigator.language) ? navigator.language : navigator.userLanguage; 1289 | } 1290 | 1291 | return detectedLng; 1292 | } 1293 | var sync = { 1294 | 1295 | load: function(lngs, options, cb) { 1296 | if (options.useLocalStorage) { 1297 | sync._loadLocal(lngs, options, function(err, store) { 1298 | var missingLngs = []; 1299 | for (var i = 0, len = lngs.length; i < len; i++) { 1300 | if (!store[lngs[i]]) missingLngs.push(lngs[i]); 1301 | } 1302 | 1303 | if (missingLngs.length > 0) { 1304 | sync._fetch(missingLngs, options, function(err, fetched) { 1305 | f.extend(store, fetched); 1306 | sync._storeLocal(fetched); 1307 | 1308 | cb(null, store); 1309 | }); 1310 | } else { 1311 | cb(null, store); 1312 | } 1313 | }); 1314 | } else { 1315 | sync._fetch(lngs, options, function(err, store){ 1316 | cb(null, store); 1317 | }); 1318 | } 1319 | }, 1320 | 1321 | _loadLocal: function(lngs, options, cb) { 1322 | var store = {} 1323 | , nowMS = new Date().getTime(); 1324 | 1325 | if(window.localStorage) { 1326 | 1327 | var todo = lngs.length; 1328 | 1329 | f.each(lngs, function(key, lng) { 1330 | var local = window.localStorage.getItem('res_' + lng); 1331 | 1332 | if (local) { 1333 | local = JSON.parse(local); 1334 | 1335 | if (local.i18nStamp && local.i18nStamp + options.localStorageExpirationTime > nowMS) { 1336 | store[lng] = local; 1337 | } 1338 | } 1339 | 1340 | todo--; // wait for all done befor callback 1341 | if (todo === 0) cb(null, store); 1342 | }); 1343 | } 1344 | }, 1345 | 1346 | _storeLocal: function(store) { 1347 | if(window.localStorage) { 1348 | for (var m in store) { 1349 | store[m].i18nStamp = new Date().getTime(); 1350 | window.localStorage.setItem('res_' + m, JSON.stringify(store[m])); 1351 | } 1352 | } 1353 | return; 1354 | }, 1355 | 1356 | _fetch: function(lngs, options, cb) { 1357 | var ns = options.ns 1358 | , store = {}; 1359 | 1360 | if (!options.dynamicLoad) { 1361 | var todo = ns.namespaces.length * lngs.length 1362 | , errors; 1363 | 1364 | // load each file individual 1365 | f.each(ns.namespaces, function(nsIndex, nsValue) { 1366 | f.each(lngs, function(lngIndex, lngValue) { 1367 | 1368 | // Call this once our translation has returned. 1369 | var loadComplete = function(err, data) { 1370 | if (err) { 1371 | errors = errors || []; 1372 | errors.push(err); 1373 | } 1374 | store[lngValue] = store[lngValue] || {}; 1375 | store[lngValue][nsValue] = data; 1376 | 1377 | todo--; // wait for all done befor callback 1378 | if (todo === 0) cb(errors, store); 1379 | }; 1380 | 1381 | if(typeof options.customLoad == 'function'){ 1382 | // Use the specified custom callback. 1383 | options.customLoad(lngValue, nsValue, options, loadComplete); 1384 | } else { 1385 | //~ // Use our inbuilt sync. 1386 | sync._fetchOne(lngValue, nsValue, options, loadComplete); 1387 | } 1388 | }); 1389 | }); 1390 | } else { 1391 | // Call this once our translation has returned. 1392 | var loadComplete = function(err, data) { 1393 | cb(null, data); 1394 | }; 1395 | 1396 | if(typeof options.customLoad == 'function'){ 1397 | // Use the specified custom callback. 1398 | options.customLoad(lngs, ns.namespaces, options, loadComplete); 1399 | } else { 1400 | var url = applyReplacement(options.resGetPath, { lng: lngs.join('+'), ns: ns.namespaces.join('+') }); 1401 | // load all needed stuff once 1402 | f.ajax({ 1403 | url: url, 1404 | success: function(data, status, xhr) { 1405 | f.log('loaded: ' + url); 1406 | loadComplete(null, data); 1407 | }, 1408 | error : function(xhr, status, error) { 1409 | f.log('failed loading: ' + url); 1410 | loadComplete('failed loading resource.json error: ' + error); 1411 | }, 1412 | dataType: "json", 1413 | async : options.getAsync 1414 | }); 1415 | } 1416 | } 1417 | }, 1418 | 1419 | _fetchOne: function(lng, ns, options, done) { 1420 | var url = applyReplacement(options.resGetPath, { lng: lng, ns: ns }); 1421 | f.ajax({ 1422 | url: url, 1423 | success: function(data, status, xhr) { 1424 | f.log('loaded: ' + url); 1425 | done(null, data); 1426 | }, 1427 | error : function(xhr, status, error) { 1428 | if ((status && status == 200) || (xhr && xhr.status && xhr.status == 200)) { 1429 | // file loaded but invalid json, stop waste time ! 1430 | f.log('There is a typo in: ' + url); 1431 | } else if ((status && status == 404) || (xhr && xhr.status && xhr.status == 404)) { 1432 | f.log('Does not exist: ' + url); 1433 | } else { 1434 | var theStatus = status ? status : ((xhr && xhr.status) ? xhr.status : null); 1435 | f.log(theStatus + ' when loading ' + url); 1436 | } 1437 | 1438 | done(error, {}); 1439 | }, 1440 | dataType: "json", 1441 | async : options.getAsync 1442 | }); 1443 | }, 1444 | 1445 | postMissing: function(lng, ns, key, defaultValue, lngs) { 1446 | var payload = {}; 1447 | payload[key] = defaultValue; 1448 | 1449 | var urls = []; 1450 | 1451 | if (o.sendMissingTo === 'fallback' && o.fallbackLng[0] !== false) { 1452 | for (var i = 0; i < o.fallbackLng.length; i++) { 1453 | urls.push({lng: o.fallbackLng[i], url: applyReplacement(o.resPostPath, { lng: o.fallbackLng[i], ns: ns })}); 1454 | } 1455 | } else if (o.sendMissingTo === 'current' || (o.sendMissingTo === 'fallback' && o.fallbackLng[0] === false) ) { 1456 | urls.push({lng: lng, url: applyReplacement(o.resPostPath, { lng: lng, ns: ns })}); 1457 | } else if (o.sendMissingTo === 'all') { 1458 | for (var i = 0, l = lngs.length; i < l; i++) { 1459 | urls.push({lng: lngs[i], url: applyReplacement(o.resPostPath, { lng: lngs[i], ns: ns })}); 1460 | } 1461 | } 1462 | 1463 | for (var y = 0, len = urls.length; y < len; y++) { 1464 | var item = urls[y]; 1465 | f.ajax({ 1466 | url: item.url, 1467 | type: o.sendType, 1468 | data: payload, 1469 | success: function(data, status, xhr) { 1470 | f.log('posted missing key \'' + key + '\' to: ' + item.url); 1471 | 1472 | // add key to resStore 1473 | var keys = key.split('.'); 1474 | var x = 0; 1475 | var value = resStore[item.lng][ns]; 1476 | while (keys[x]) { 1477 | if (x === keys.length - 1) { 1478 | value = value[keys[x]] = defaultValue; 1479 | } else { 1480 | value = value[keys[x]] = value[keys[x]] || {}; 1481 | } 1482 | x++; 1483 | } 1484 | }, 1485 | error : function(xhr, status, error) { 1486 | f.log('failed posting missing key \'' + key + '\' to: ' + item.url); 1487 | }, 1488 | dataType: "json", 1489 | async : o.postAsync 1490 | }); 1491 | } 1492 | } 1493 | }; 1494 | // definition http://translate.sourceforge.net/wiki/l10n/pluralforms 1495 | var pluralExtensions = { 1496 | 1497 | rules: { 1498 | "ach": { 1499 | "name": "Acholi", 1500 | "numbers": [ 1501 | 1, 1502 | 2 1503 | ], 1504 | "plurals": function(n) { return Number(n > 1); } 1505 | }, 1506 | "af": { 1507 | "name": "Afrikaans", 1508 | "numbers": [ 1509 | 1, 1510 | 2 1511 | ], 1512 | "plurals": function(n) { return Number(n != 1); } 1513 | }, 1514 | "ak": { 1515 | "name": "Akan", 1516 | "numbers": [ 1517 | 1, 1518 | 2 1519 | ], 1520 | "plurals": function(n) { return Number(n > 1); } 1521 | }, 1522 | "am": { 1523 | "name": "Amharic", 1524 | "numbers": [ 1525 | 1, 1526 | 2 1527 | ], 1528 | "plurals": function(n) { return Number(n > 1); } 1529 | }, 1530 | "an": { 1531 | "name": "Aragonese", 1532 | "numbers": [ 1533 | 1, 1534 | 2 1535 | ], 1536 | "plurals": function(n) { return Number(n != 1); } 1537 | }, 1538 | "ar": { 1539 | "name": "Arabic", 1540 | "numbers": [ 1541 | 0, 1542 | 1, 1543 | 2, 1544 | 3, 1545 | 11, 1546 | 100 1547 | ], 1548 | "plurals": function(n) { return Number(n===0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5); } 1549 | }, 1550 | "arn": { 1551 | "name": "Mapudungun", 1552 | "numbers": [ 1553 | 1, 1554 | 2 1555 | ], 1556 | "plurals": function(n) { return Number(n > 1); } 1557 | }, 1558 | "ast": { 1559 | "name": "Asturian", 1560 | "numbers": [ 1561 | 1, 1562 | 2 1563 | ], 1564 | "plurals": function(n) { return Number(n != 1); } 1565 | }, 1566 | "ay": { 1567 | "name": "Aymar\u00e1", 1568 | "numbers": [ 1569 | 1 1570 | ], 1571 | "plurals": function(n) { return 0; } 1572 | }, 1573 | "az": { 1574 | "name": "Azerbaijani", 1575 | "numbers": [ 1576 | 1, 1577 | 2 1578 | ], 1579 | "plurals": function(n) { return Number(n != 1); } 1580 | }, 1581 | "be": { 1582 | "name": "Belarusian", 1583 | "numbers": [ 1584 | 1, 1585 | 2, 1586 | 5 1587 | ], 1588 | "plurals": function(n) { return Number(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); } 1589 | }, 1590 | "bg": { 1591 | "name": "Bulgarian", 1592 | "numbers": [ 1593 | 1, 1594 | 2 1595 | ], 1596 | "plurals": function(n) { return Number(n != 1); } 1597 | }, 1598 | "bn": { 1599 | "name": "Bengali", 1600 | "numbers": [ 1601 | 1, 1602 | 2 1603 | ], 1604 | "plurals": function(n) { return Number(n != 1); } 1605 | }, 1606 | "bo": { 1607 | "name": "Tibetan", 1608 | "numbers": [ 1609 | 1 1610 | ], 1611 | "plurals": function(n) { return 0; } 1612 | }, 1613 | "br": { 1614 | "name": "Breton", 1615 | "numbers": [ 1616 | 1, 1617 | 2 1618 | ], 1619 | "plurals": function(n) { return Number(n > 1); } 1620 | }, 1621 | "bs": { 1622 | "name": "Bosnian", 1623 | "numbers": [ 1624 | 1, 1625 | 2, 1626 | 5 1627 | ], 1628 | "plurals": function(n) { return Number(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); } 1629 | }, 1630 | "ca": { 1631 | "name": "Catalan", 1632 | "numbers": [ 1633 | 1, 1634 | 2 1635 | ], 1636 | "plurals": function(n) { return Number(n != 1); } 1637 | }, 1638 | "cgg": { 1639 | "name": "Chiga", 1640 | "numbers": [ 1641 | 1 1642 | ], 1643 | "plurals": function(n) { return 0; } 1644 | }, 1645 | "cs": { 1646 | "name": "Czech", 1647 | "numbers": [ 1648 | 1, 1649 | 2, 1650 | 5 1651 | ], 1652 | "plurals": function(n) { return Number((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2); } 1653 | }, 1654 | "csb": { 1655 | "name": "Kashubian", 1656 | "numbers": [ 1657 | 1, 1658 | 2, 1659 | 5 1660 | ], 1661 | "plurals": function(n) { return Number(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); } 1662 | }, 1663 | "cy": { 1664 | "name": "Welsh", 1665 | "numbers": [ 1666 | 1, 1667 | 2, 1668 | 3, 1669 | 8 1670 | ], 1671 | "plurals": function(n) { return Number((n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3); } 1672 | }, 1673 | "da": { 1674 | "name": "Danish", 1675 | "numbers": [ 1676 | 1, 1677 | 2 1678 | ], 1679 | "plurals": function(n) { return Number(n != 1); } 1680 | }, 1681 | "de": { 1682 | "name": "German", 1683 | "numbers": [ 1684 | 1, 1685 | 2 1686 | ], 1687 | "plurals": function(n) { return Number(n != 1); } 1688 | }, 1689 | "dz": { 1690 | "name": "Dzongkha", 1691 | "numbers": [ 1692 | 1 1693 | ], 1694 | "plurals": function(n) { return 0; } 1695 | }, 1696 | "el": { 1697 | "name": "Greek", 1698 | "numbers": [ 1699 | 1, 1700 | 2 1701 | ], 1702 | "plurals": function(n) { return Number(n != 1); } 1703 | }, 1704 | "en": { 1705 | "name": "English", 1706 | "numbers": [ 1707 | 1, 1708 | 2 1709 | ], 1710 | "plurals": function(n) { return Number(n != 1); } 1711 | }, 1712 | "eo": { 1713 | "name": "Esperanto", 1714 | "numbers": [ 1715 | 1, 1716 | 2 1717 | ], 1718 | "plurals": function(n) { return Number(n != 1); } 1719 | }, 1720 | "es": { 1721 | "name": "Spanish", 1722 | "numbers": [ 1723 | 1, 1724 | 2 1725 | ], 1726 | "plurals": function(n) { return Number(n != 1); } 1727 | }, 1728 | "es_ar": { 1729 | "name": "Argentinean Spanish", 1730 | "numbers": [ 1731 | 1, 1732 | 2 1733 | ], 1734 | "plurals": function(n) { return Number(n != 1); } 1735 | }, 1736 | "et": { 1737 | "name": "Estonian", 1738 | "numbers": [ 1739 | 1, 1740 | 2 1741 | ], 1742 | "plurals": function(n) { return Number(n != 1); } 1743 | }, 1744 | "eu": { 1745 | "name": "Basque", 1746 | "numbers": [ 1747 | 1, 1748 | 2 1749 | ], 1750 | "plurals": function(n) { return Number(n != 1); } 1751 | }, 1752 | "fa": { 1753 | "name": "Persian", 1754 | "numbers": [ 1755 | 1 1756 | ], 1757 | "plurals": function(n) { return 0; } 1758 | }, 1759 | "fi": { 1760 | "name": "Finnish", 1761 | "numbers": [ 1762 | 1, 1763 | 2 1764 | ], 1765 | "plurals": function(n) { return Number(n != 1); } 1766 | }, 1767 | "fil": { 1768 | "name": "Filipino", 1769 | "numbers": [ 1770 | 1, 1771 | 2 1772 | ], 1773 | "plurals": function(n) { return Number(n > 1); } 1774 | }, 1775 | "fo": { 1776 | "name": "Faroese", 1777 | "numbers": [ 1778 | 1, 1779 | 2 1780 | ], 1781 | "plurals": function(n) { return Number(n != 1); } 1782 | }, 1783 | "fr": { 1784 | "name": "French", 1785 | "numbers": [ 1786 | 1, 1787 | 2 1788 | ], 1789 | "plurals": function(n) { return Number(n > 1); } 1790 | }, 1791 | "fur": { 1792 | "name": "Friulian", 1793 | "numbers": [ 1794 | 1, 1795 | 2 1796 | ], 1797 | "plurals": function(n) { return Number(n != 1); } 1798 | }, 1799 | "fy": { 1800 | "name": "Frisian", 1801 | "numbers": [ 1802 | 1, 1803 | 2 1804 | ], 1805 | "plurals": function(n) { return Number(n != 1); } 1806 | }, 1807 | "ga": { 1808 | "name": "Irish", 1809 | "numbers": [ 1810 | 1, 1811 | 2, 1812 | 3, 1813 | 7, 1814 | 11 1815 | ], 1816 | "plurals": function(n) { return Number(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4) ;} 1817 | }, 1818 | "gd": { 1819 | "name": "Scottish Gaelic", 1820 | "numbers": [ 1821 | 1, 1822 | 2, 1823 | 3, 1824 | 20 1825 | ], 1826 | "plurals": function(n) { return Number((n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3); } 1827 | }, 1828 | "gl": { 1829 | "name": "Galician", 1830 | "numbers": [ 1831 | 1, 1832 | 2 1833 | ], 1834 | "plurals": function(n) { return Number(n != 1); } 1835 | }, 1836 | "gu": { 1837 | "name": "Gujarati", 1838 | "numbers": [ 1839 | 1, 1840 | 2 1841 | ], 1842 | "plurals": function(n) { return Number(n != 1); } 1843 | }, 1844 | "gun": { 1845 | "name": "Gun", 1846 | "numbers": [ 1847 | 1, 1848 | 2 1849 | ], 1850 | "plurals": function(n) { return Number(n > 1); } 1851 | }, 1852 | "ha": { 1853 | "name": "Hausa", 1854 | "numbers": [ 1855 | 1, 1856 | 2 1857 | ], 1858 | "plurals": function(n) { return Number(n != 1); } 1859 | }, 1860 | "he": { 1861 | "name": "Hebrew", 1862 | "numbers": [ 1863 | 1, 1864 | 2 1865 | ], 1866 | "plurals": function(n) { return Number(n != 1); } 1867 | }, 1868 | "hi": { 1869 | "name": "Hindi", 1870 | "numbers": [ 1871 | 1, 1872 | 2 1873 | ], 1874 | "plurals": function(n) { return Number(n != 1); } 1875 | }, 1876 | "hr": { 1877 | "name": "Croatian", 1878 | "numbers": [ 1879 | 1, 1880 | 2, 1881 | 5 1882 | ], 1883 | "plurals": function(n) { return Number(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); } 1884 | }, 1885 | "hu": { 1886 | "name": "Hungarian", 1887 | "numbers": [ 1888 | 1, 1889 | 2 1890 | ], 1891 | "plurals": function(n) { return Number(n != 1); } 1892 | }, 1893 | "hy": { 1894 | "name": "Armenian", 1895 | "numbers": [ 1896 | 1, 1897 | 2 1898 | ], 1899 | "plurals": function(n) { return Number(n != 1); } 1900 | }, 1901 | "ia": { 1902 | "name": "Interlingua", 1903 | "numbers": [ 1904 | 1, 1905 | 2 1906 | ], 1907 | "plurals": function(n) { return Number(n != 1); } 1908 | }, 1909 | "id": { 1910 | "name": "Indonesian", 1911 | "numbers": [ 1912 | 1 1913 | ], 1914 | "plurals": function(n) { return 0; } 1915 | }, 1916 | "is": { 1917 | "name": "Icelandic", 1918 | "numbers": [ 1919 | 1, 1920 | 2 1921 | ], 1922 | "plurals": function(n) { return Number(n%10!=1 || n%100==11); } 1923 | }, 1924 | "it": { 1925 | "name": "Italian", 1926 | "numbers": [ 1927 | 1, 1928 | 2 1929 | ], 1930 | "plurals": function(n) { return Number(n != 1); } 1931 | }, 1932 | "ja": { 1933 | "name": "Japanese", 1934 | "numbers": [ 1935 | 1 1936 | ], 1937 | "plurals": function(n) { return 0; } 1938 | }, 1939 | "jbo": { 1940 | "name": "Lojban", 1941 | "numbers": [ 1942 | 1 1943 | ], 1944 | "plurals": function(n) { return 0; } 1945 | }, 1946 | "jv": { 1947 | "name": "Javanese", 1948 | "numbers": [ 1949 | 0, 1950 | 1 1951 | ], 1952 | "plurals": function(n) { return Number(n !== 0); } 1953 | }, 1954 | "ka": { 1955 | "name": "Georgian", 1956 | "numbers": [ 1957 | 1 1958 | ], 1959 | "plurals": function(n) { return 0; } 1960 | }, 1961 | "kk": { 1962 | "name": "Kazakh", 1963 | "numbers": [ 1964 | 1 1965 | ], 1966 | "plurals": function(n) { return 0; } 1967 | }, 1968 | "km": { 1969 | "name": "Khmer", 1970 | "numbers": [ 1971 | 1 1972 | ], 1973 | "plurals": function(n) { return 0; } 1974 | }, 1975 | "kn": { 1976 | "name": "Kannada", 1977 | "numbers": [ 1978 | 1, 1979 | 2 1980 | ], 1981 | "plurals": function(n) { return Number(n != 1); } 1982 | }, 1983 | "ko": { 1984 | "name": "Korean", 1985 | "numbers": [ 1986 | 1 1987 | ], 1988 | "plurals": function(n) { return 0; } 1989 | }, 1990 | "ku": { 1991 | "name": "Kurdish", 1992 | "numbers": [ 1993 | 1, 1994 | 2 1995 | ], 1996 | "plurals": function(n) { return Number(n != 1); } 1997 | }, 1998 | "kw": { 1999 | "name": "Cornish", 2000 | "numbers": [ 2001 | 1, 2002 | 2, 2003 | 3, 2004 | 4 2005 | ], 2006 | "plurals": function(n) { return Number((n==1) ? 0 : (n==2) ? 1 : (n == 3) ? 2 : 3); } 2007 | }, 2008 | "ky": { 2009 | "name": "Kyrgyz", 2010 | "numbers": [ 2011 | 1 2012 | ], 2013 | "plurals": function(n) { return 0; } 2014 | }, 2015 | "lb": { 2016 | "name": "Letzeburgesch", 2017 | "numbers": [ 2018 | 1, 2019 | 2 2020 | ], 2021 | "plurals": function(n) { return Number(n != 1); } 2022 | }, 2023 | "ln": { 2024 | "name": "Lingala", 2025 | "numbers": [ 2026 | 1, 2027 | 2 2028 | ], 2029 | "plurals": function(n) { return Number(n > 1); } 2030 | }, 2031 | "lo": { 2032 | "name": "Lao", 2033 | "numbers": [ 2034 | 1 2035 | ], 2036 | "plurals": function(n) { return 0; } 2037 | }, 2038 | "lt": { 2039 | "name": "Lithuanian", 2040 | "numbers": [ 2041 | 1, 2042 | 2, 2043 | 10 2044 | ], 2045 | "plurals": function(n) { return Number(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2); } 2046 | }, 2047 | "lv": { 2048 | "name": "Latvian", 2049 | "numbers": [ 2050 | 1, 2051 | 2, 2052 | 0 2053 | ], 2054 | "plurals": function(n) { return Number(n%10==1 && n%100!=11 ? 0 : n !== 0 ? 1 : 2); } 2055 | }, 2056 | "mai": { 2057 | "name": "Maithili", 2058 | "numbers": [ 2059 | 1, 2060 | 2 2061 | ], 2062 | "plurals": function(n) { return Number(n != 1); } 2063 | }, 2064 | "mfe": { 2065 | "name": "Mauritian Creole", 2066 | "numbers": [ 2067 | 1, 2068 | 2 2069 | ], 2070 | "plurals": function(n) { return Number(n > 1); } 2071 | }, 2072 | "mg": { 2073 | "name": "Malagasy", 2074 | "numbers": [ 2075 | 1, 2076 | 2 2077 | ], 2078 | "plurals": function(n) { return Number(n > 1); } 2079 | }, 2080 | "mi": { 2081 | "name": "Maori", 2082 | "numbers": [ 2083 | 1, 2084 | 2 2085 | ], 2086 | "plurals": function(n) { return Number(n > 1); } 2087 | }, 2088 | "mk": { 2089 | "name": "Macedonian", 2090 | "numbers": [ 2091 | 1, 2092 | 2 2093 | ], 2094 | "plurals": function(n) { return Number(n==1 || n%10==1 ? 0 : 1); } 2095 | }, 2096 | "ml": { 2097 | "name": "Malayalam", 2098 | "numbers": [ 2099 | 1, 2100 | 2 2101 | ], 2102 | "plurals": function(n) { return Number(n != 1); } 2103 | }, 2104 | "mn": { 2105 | "name": "Mongolian", 2106 | "numbers": [ 2107 | 1, 2108 | 2 2109 | ], 2110 | "plurals": function(n) { return Number(n != 1); } 2111 | }, 2112 | "mnk": { 2113 | "name": "Mandinka", 2114 | "numbers": [ 2115 | 0, 2116 | 1, 2117 | 2 2118 | ], 2119 | "plurals": function(n) { return Number(n == 0 ? 0 : n==1 ? 1 : 2); } 2120 | }, 2121 | "mr": { 2122 | "name": "Marathi", 2123 | "numbers": [ 2124 | 1, 2125 | 2 2126 | ], 2127 | "plurals": function(n) { return Number(n != 1); } 2128 | }, 2129 | "ms": { 2130 | "name": "Malay", 2131 | "numbers": [ 2132 | 1 2133 | ], 2134 | "plurals": function(n) { return 0; } 2135 | }, 2136 | "mt": { 2137 | "name": "Maltese", 2138 | "numbers": [ 2139 | 1, 2140 | 2, 2141 | 11, 2142 | 20 2143 | ], 2144 | "plurals": function(n) { return Number(n==1 ? 0 : n===0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3); } 2145 | }, 2146 | "nah": { 2147 | "name": "Nahuatl", 2148 | "numbers": [ 2149 | 1, 2150 | 2 2151 | ], 2152 | "plurals": function(n) { return Number(n != 1); } 2153 | }, 2154 | "nap": { 2155 | "name": "Neapolitan", 2156 | "numbers": [ 2157 | 1, 2158 | 2 2159 | ], 2160 | "plurals": function(n) { return Number(n != 1); } 2161 | }, 2162 | "nb": { 2163 | "name": "Norwegian Bokmal", 2164 | "numbers": [ 2165 | 1, 2166 | 2 2167 | ], 2168 | "plurals": function(n) { return Number(n != 1); } 2169 | }, 2170 | "ne": { 2171 | "name": "Nepali", 2172 | "numbers": [ 2173 | 1, 2174 | 2 2175 | ], 2176 | "plurals": function(n) { return Number(n != 1); } 2177 | }, 2178 | "nl": { 2179 | "name": "Dutch", 2180 | "numbers": [ 2181 | 1, 2182 | 2 2183 | ], 2184 | "plurals": function(n) { return Number(n != 1); } 2185 | }, 2186 | "nn": { 2187 | "name": "Norwegian Nynorsk", 2188 | "numbers": [ 2189 | 1, 2190 | 2 2191 | ], 2192 | "plurals": function(n) { return Number(n != 1); } 2193 | }, 2194 | "no": { 2195 | "name": "Norwegian", 2196 | "numbers": [ 2197 | 1, 2198 | 2 2199 | ], 2200 | "plurals": function(n) { return Number(n != 1); } 2201 | }, 2202 | "nso": { 2203 | "name": "Northern Sotho", 2204 | "numbers": [ 2205 | 1, 2206 | 2 2207 | ], 2208 | "plurals": function(n) { return Number(n != 1); } 2209 | }, 2210 | "oc": { 2211 | "name": "Occitan", 2212 | "numbers": [ 2213 | 1, 2214 | 2 2215 | ], 2216 | "plurals": function(n) { return Number(n > 1); } 2217 | }, 2218 | "or": { 2219 | "name": "Oriya", 2220 | "numbers": [ 2221 | 2, 2222 | 1 2223 | ], 2224 | "plurals": function(n) { return Number(n != 1); } 2225 | }, 2226 | "pa": { 2227 | "name": "Punjabi", 2228 | "numbers": [ 2229 | 1, 2230 | 2 2231 | ], 2232 | "plurals": function(n) { return Number(n != 1); } 2233 | }, 2234 | "pap": { 2235 | "name": "Papiamento", 2236 | "numbers": [ 2237 | 1, 2238 | 2 2239 | ], 2240 | "plurals": function(n) { return Number(n != 1); } 2241 | }, 2242 | "pl": { 2243 | "name": "Polish", 2244 | "numbers": [ 2245 | 1, 2246 | 2, 2247 | 5 2248 | ], 2249 | "plurals": function(n) { return Number(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); } 2250 | }, 2251 | "pms": { 2252 | "name": "Piemontese", 2253 | "numbers": [ 2254 | 1, 2255 | 2 2256 | ], 2257 | "plurals": function(n) { return Number(n != 1); } 2258 | }, 2259 | "ps": { 2260 | "name": "Pashto", 2261 | "numbers": [ 2262 | 1, 2263 | 2 2264 | ], 2265 | "plurals": function(n) { return Number(n != 1); } 2266 | }, 2267 | "pt": { 2268 | "name": "Portuguese", 2269 | "numbers": [ 2270 | 1, 2271 | 2 2272 | ], 2273 | "plurals": function(n) { return Number(n != 1); } 2274 | }, 2275 | "pt_br": { 2276 | "name": "Brazilian Portuguese", 2277 | "numbers": [ 2278 | 1, 2279 | 2 2280 | ], 2281 | "plurals": function(n) { return Number(n != 1); } 2282 | }, 2283 | "rm": { 2284 | "name": "Romansh", 2285 | "numbers": [ 2286 | 1, 2287 | 2 2288 | ], 2289 | "plurals": function(n) { return Number(n != 1); } 2290 | }, 2291 | "ro": { 2292 | "name": "Romanian", 2293 | "numbers": [ 2294 | 1, 2295 | 2, 2296 | 20 2297 | ], 2298 | "plurals": function(n) { return Number(n==1 ? 0 : (n===0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2); } 2299 | }, 2300 | "ru": { 2301 | "name": "Russian", 2302 | "numbers": [ 2303 | 1, 2304 | 2, 2305 | 5 2306 | ], 2307 | "plurals": function(n) { return Number(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); } 2308 | }, 2309 | "sah": { 2310 | "name": "Yakut", 2311 | "numbers": [ 2312 | 1 2313 | ], 2314 | "plurals": function(n) { return 0; } 2315 | }, 2316 | "sco": { 2317 | "name": "Scots", 2318 | "numbers": [ 2319 | 1, 2320 | 2 2321 | ], 2322 | "plurals": function(n) { return Number(n != 1); } 2323 | }, 2324 | "se": { 2325 | "name": "Northern Sami", 2326 | "numbers": [ 2327 | 1, 2328 | 2 2329 | ], 2330 | "plurals": function(n) { return Number(n != 1); } 2331 | }, 2332 | "si": { 2333 | "name": "Sinhala", 2334 | "numbers": [ 2335 | 1, 2336 | 2 2337 | ], 2338 | "plurals": function(n) { return Number(n != 1); } 2339 | }, 2340 | "sk": { 2341 | "name": "Slovak", 2342 | "numbers": [ 2343 | 1, 2344 | 2, 2345 | 5 2346 | ], 2347 | "plurals": function(n) { return Number((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2); } 2348 | }, 2349 | "sl": { 2350 | "name": "Slovenian", 2351 | "numbers": [ 2352 | 5, 2353 | 1, 2354 | 2, 2355 | 3 2356 | ], 2357 | "plurals": function(n) { return Number(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0); } 2358 | }, 2359 | "so": { 2360 | "name": "Somali", 2361 | "numbers": [ 2362 | 1, 2363 | 2 2364 | ], 2365 | "plurals": function(n) { return Number(n != 1); } 2366 | }, 2367 | "son": { 2368 | "name": "Songhay", 2369 | "numbers": [ 2370 | 1, 2371 | 2 2372 | ], 2373 | "plurals": function(n) { return Number(n != 1); } 2374 | }, 2375 | "sq": { 2376 | "name": "Albanian", 2377 | "numbers": [ 2378 | 1, 2379 | 2 2380 | ], 2381 | "plurals": function(n) { return Number(n != 1); } 2382 | }, 2383 | "sr": { 2384 | "name": "Serbian", 2385 | "numbers": [ 2386 | 1, 2387 | 2, 2388 | 5 2389 | ], 2390 | "plurals": function(n) { return Number(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); } 2391 | }, 2392 | "su": { 2393 | "name": "Sundanese", 2394 | "numbers": [ 2395 | 1 2396 | ], 2397 | "plurals": function(n) { return 0; } 2398 | }, 2399 | "sv": { 2400 | "name": "Swedish", 2401 | "numbers": [ 2402 | 1, 2403 | 2 2404 | ], 2405 | "plurals": function(n) { return Number(n != 1); } 2406 | }, 2407 | "sw": { 2408 | "name": "Swahili", 2409 | "numbers": [ 2410 | 1, 2411 | 2 2412 | ], 2413 | "plurals": function(n) { return Number(n != 1); } 2414 | }, 2415 | "ta": { 2416 | "name": "Tamil", 2417 | "numbers": [ 2418 | 1, 2419 | 2 2420 | ], 2421 | "plurals": function(n) { return Number(n != 1); } 2422 | }, 2423 | "te": { 2424 | "name": "Telugu", 2425 | "numbers": [ 2426 | 1, 2427 | 2 2428 | ], 2429 | "plurals": function(n) { return Number(n != 1); } 2430 | }, 2431 | "tg": { 2432 | "name": "Tajik", 2433 | "numbers": [ 2434 | 1, 2435 | 2 2436 | ], 2437 | "plurals": function(n) { return Number(n > 1); } 2438 | }, 2439 | "th": { 2440 | "name": "Thai", 2441 | "numbers": [ 2442 | 1 2443 | ], 2444 | "plurals": function(n) { return 0; } 2445 | }, 2446 | "ti": { 2447 | "name": "Tigrinya", 2448 | "numbers": [ 2449 | 1, 2450 | 2 2451 | ], 2452 | "plurals": function(n) { return Number(n > 1); } 2453 | }, 2454 | "tj": { 2455 | "name": "Tajik", 2456 | "numbers": [ 2457 | 1, 2458 | 2 2459 | ], 2460 | "plurals": function(n) { return Number(n > 1); } 2461 | }, 2462 | "tk": { 2463 | "name": "Turkmen", 2464 | "numbers": [ 2465 | 1, 2466 | 2 2467 | ], 2468 | "plurals": function(n) { return Number(n != 1); } 2469 | }, 2470 | "tr": { 2471 | "name": "Turkish", 2472 | "numbers": [ 2473 | 1, 2474 | 2 2475 | ], 2476 | "plurals": function(n) { return Number(n > 1); } 2477 | }, 2478 | "tt": { 2479 | "name": "Tatar", 2480 | "numbers": [ 2481 | 1 2482 | ], 2483 | "plurals": function(n) { return 0; } 2484 | }, 2485 | "ug": { 2486 | "name": "Uyghur", 2487 | "numbers": [ 2488 | 1 2489 | ], 2490 | "plurals": function(n) { return 0; } 2491 | }, 2492 | "uk": { 2493 | "name": "Ukrainian", 2494 | "numbers": [ 2495 | 1, 2496 | 2, 2497 | 5 2498 | ], 2499 | "plurals": function(n) { return Number(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); } 2500 | }, 2501 | "ur": { 2502 | "name": "Urdu", 2503 | "numbers": [ 2504 | 1, 2505 | 2 2506 | ], 2507 | "plurals": function(n) { return Number(n != 1); } 2508 | }, 2509 | "uz": { 2510 | "name": "Uzbek", 2511 | "numbers": [ 2512 | 1, 2513 | 2 2514 | ], 2515 | "plurals": function(n) { return Number(n > 1); } 2516 | }, 2517 | "vi": { 2518 | "name": "Vietnamese", 2519 | "numbers": [ 2520 | 1 2521 | ], 2522 | "plurals": function(n) { return 0; } 2523 | }, 2524 | "wa": { 2525 | "name": "Walloon", 2526 | "numbers": [ 2527 | 1, 2528 | 2 2529 | ], 2530 | "plurals": function(n) { return Number(n > 1); } 2531 | }, 2532 | "wo": { 2533 | "name": "Wolof", 2534 | "numbers": [ 2535 | 1 2536 | ], 2537 | "plurals": function(n) { return 0; } 2538 | }, 2539 | "yo": { 2540 | "name": "Yoruba", 2541 | "numbers": [ 2542 | 1, 2543 | 2 2544 | ], 2545 | "plurals": function(n) { return Number(n != 1); } 2546 | }, 2547 | "zh": { 2548 | "name": "Chinese", 2549 | "numbers": [ 2550 | 1 2551 | ], 2552 | "plurals": function(n) { return 0; } 2553 | } 2554 | }, 2555 | 2556 | // for demonstration only sl and ar is added but you can add your own pluralExtensions 2557 | addRule: function(lng, obj) { 2558 | pluralExtensions.rules[lng] = obj; 2559 | }, 2560 | 2561 | setCurrentLng: function(lng) { 2562 | if (!pluralExtensions.currentRule || pluralExtensions.currentRule.lng !== lng) { 2563 | var parts = lng.split('-'); 2564 | 2565 | pluralExtensions.currentRule = { 2566 | lng: lng, 2567 | rule: pluralExtensions.rules[parts[0]] 2568 | }; 2569 | } 2570 | }, 2571 | 2572 | get: function(lng, count) { 2573 | var parts = lng.split('-'); 2574 | 2575 | function getResult(l, c) { 2576 | var ext; 2577 | if (pluralExtensions.currentRule && pluralExtensions.currentRule.lng === lng) { 2578 | ext = pluralExtensions.currentRule.rule; 2579 | } else { 2580 | ext = pluralExtensions.rules[l]; 2581 | } 2582 | if (ext) { 2583 | var i = ext.plurals(c); 2584 | var number = ext.numbers[i]; 2585 | if (ext.numbers.length === 2 && ext.numbers[0] === 1) { 2586 | if (number === 2) { 2587 | number = -1; // regular plural 2588 | } else if (number === 1) { 2589 | number = 1; // singular 2590 | } 2591 | }//console.log(count + '-' + number); 2592 | return number; 2593 | } else { 2594 | return c === 1 ? '1' : '-1'; 2595 | } 2596 | } 2597 | 2598 | return getResult(parts[0], count); 2599 | } 2600 | 2601 | }; 2602 | var postProcessors = {}; 2603 | var addPostProcessor = function(name, fc) { 2604 | postProcessors[name] = fc; 2605 | }; 2606 | // sprintf support 2607 | var sprintf = (function() { 2608 | function get_type(variable) { 2609 | return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase(); 2610 | } 2611 | function str_repeat(input, multiplier) { 2612 | for (var output = []; multiplier > 0; output[--multiplier] = input) {/* do nothing */} 2613 | return output.join(''); 2614 | } 2615 | 2616 | var str_format = function() { 2617 | if (!str_format.cache.hasOwnProperty(arguments[0])) { 2618 | str_format.cache[arguments[0]] = str_format.parse(arguments[0]); 2619 | } 2620 | return str_format.format.call(null, str_format.cache[arguments[0]], arguments); 2621 | }; 2622 | 2623 | str_format.format = function(parse_tree, argv) { 2624 | var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length; 2625 | for (i = 0; i < tree_length; i++) { 2626 | node_type = get_type(parse_tree[i]); 2627 | if (node_type === 'string') { 2628 | output.push(parse_tree[i]); 2629 | } 2630 | else if (node_type === 'array') { 2631 | match = parse_tree[i]; // convenience purposes only 2632 | if (match[2]) { // keyword argument 2633 | arg = argv[cursor]; 2634 | for (k = 0; k < match[2].length; k++) { 2635 | if (!arg.hasOwnProperty(match[2][k])) { 2636 | throw(sprintf('[sprintf] property "%s" does not exist', match[2][k])); 2637 | } 2638 | arg = arg[match[2][k]]; 2639 | } 2640 | } 2641 | else if (match[1]) { // positional argument (explicit) 2642 | arg = argv[match[1]]; 2643 | } 2644 | else { // positional argument (implicit) 2645 | arg = argv[cursor++]; 2646 | } 2647 | 2648 | if (/[^s]/.test(match[8]) && (get_type(arg) != 'number')) { 2649 | throw(sprintf('[sprintf] expecting number but found %s', get_type(arg))); 2650 | } 2651 | switch (match[8]) { 2652 | case 'b': arg = arg.toString(2); break; 2653 | case 'c': arg = String.fromCharCode(arg); break; 2654 | case 'd': arg = parseInt(arg, 10); break; 2655 | case 'e': arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential(); break; 2656 | case 'f': arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg); break; 2657 | case 'o': arg = arg.toString(8); break; 2658 | case 's': arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg); break; 2659 | case 'u': arg = Math.abs(arg); break; 2660 | case 'x': arg = arg.toString(16); break; 2661 | case 'X': arg = arg.toString(16).toUpperCase(); break; 2662 | } 2663 | arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+'+ arg : arg); 2664 | pad_character = match[4] ? match[4] == '0' ? '0' : match[4].charAt(1) : ' '; 2665 | pad_length = match[6] - String(arg).length; 2666 | pad = match[6] ? str_repeat(pad_character, pad_length) : ''; 2667 | output.push(match[5] ? arg + pad : pad + arg); 2668 | } 2669 | } 2670 | return output.join(''); 2671 | }; 2672 | 2673 | str_format.cache = {}; 2674 | 2675 | str_format.parse = function(fmt) { 2676 | var _fmt = fmt, match = [], parse_tree = [], arg_names = 0; 2677 | while (_fmt) { 2678 | if ((match = /^[^\x25]+/.exec(_fmt)) !== null) { 2679 | parse_tree.push(match[0]); 2680 | } 2681 | else if ((match = /^\x25{2}/.exec(_fmt)) !== null) { 2682 | parse_tree.push('%'); 2683 | } 2684 | else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) { 2685 | if (match[2]) { 2686 | arg_names |= 1; 2687 | var field_list = [], replacement_field = match[2], field_match = []; 2688 | if ((field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) { 2689 | field_list.push(field_match[1]); 2690 | while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') { 2691 | if ((field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) { 2692 | field_list.push(field_match[1]); 2693 | } 2694 | else if ((field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) { 2695 | field_list.push(field_match[1]); 2696 | } 2697 | else { 2698 | throw('[sprintf] huh?'); 2699 | } 2700 | } 2701 | } 2702 | else { 2703 | throw('[sprintf] huh?'); 2704 | } 2705 | match[2] = field_list; 2706 | } 2707 | else { 2708 | arg_names |= 2; 2709 | } 2710 | if (arg_names === 3) { 2711 | throw('[sprintf] mixing positional and named placeholders is not (yet) supported'); 2712 | } 2713 | parse_tree.push(match); 2714 | } 2715 | else { 2716 | throw('[sprintf] huh?'); 2717 | } 2718 | _fmt = _fmt.substring(match[0].length); 2719 | } 2720 | return parse_tree; 2721 | }; 2722 | 2723 | return str_format; 2724 | })(); 2725 | 2726 | var vsprintf = function(fmt, argv) { 2727 | argv.unshift(fmt); 2728 | return sprintf.apply(null, argv); 2729 | }; 2730 | 2731 | addPostProcessor("sprintf", function(val, key, opts) { 2732 | if (!opts.sprintf) return val; 2733 | 2734 | if (Object.prototype.toString.apply(opts.sprintf) === '[object Array]') { 2735 | return vsprintf(val, opts.sprintf); 2736 | } else if (typeof opts.sprintf === 'object') { 2737 | return sprintf(val, opts.sprintf); 2738 | } 2739 | 2740 | return val; 2741 | }); 2742 | // public api interface 2743 | TAPi18next.init = init; 2744 | TAPi18next.setLng = setLng; 2745 | TAPi18next.preload = preload; 2746 | TAPi18next.addResourceBundle = addResourceBundle; 2747 | TAPi18next.removeResourceBundle = removeResourceBundle; 2748 | TAPi18next.loadNamespace = loadNamespace; 2749 | TAPi18next.loadNamespaces = loadNamespaces; 2750 | TAPi18next.setDefaultNamespace = setDefaultNamespace; 2751 | TAPi18next.t = translate; 2752 | TAPi18next.translate = translate; 2753 | TAPi18next.exists = exists; 2754 | TAPi18next.detectLanguage = f.detectLanguage; 2755 | TAPi18next.pluralExtensions = pluralExtensions; 2756 | TAPi18next.sync = sync; 2757 | TAPi18next.functions = f; 2758 | TAPi18next.lng = lng; 2759 | TAPi18next.addPostProcessor = addPostProcessor; 2760 | TAPi18next.options = o; 2761 | })(); 2762 | --------------------------------------------------------------------------------