From c18c84b946638bec3c3c215d62e8bab851e79454 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 27 Apr 2023 16:24:53 -0300 Subject: [PATCH 01/70] Initial implementation of deasync on outgoing integrations --- .../integrations/server/lib/triggerHandler.js | 62 ++++++++++++------- apps/meteor/package.json | 2 + yarn.lock | 35 +++++++++++ 3 files changed, 75 insertions(+), 24 deletions(-) diff --git a/apps/meteor/app/integrations/server/lib/triggerHandler.js b/apps/meteor/app/integrations/server/lib/triggerHandler.js index d0a0f59d3e167..250237940c221 100644 --- a/apps/meteor/app/integrations/server/lib/triggerHandler.js +++ b/apps/meteor/app/integrations/server/lib/triggerHandler.js @@ -3,8 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { Random } from '@rocket.chat/random'; import _ from 'underscore'; import moment from 'moment'; -import Fiber from 'fibers'; -import Future from 'fibers/future'; +import deasync from 'deasync'; import { Integrations, IntegrationHistory, Users, Rooms, Messages } from '@rocket.chat/models'; import * as Models from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; @@ -19,6 +18,17 @@ import { omit } from '../../../../lib/utils/omit'; import { forbiddenModelMethods } from '../api/api'; import { httpCall } from '../../../../server/lib/http/call'; +deasync.promise = function (promise) { + return deasync((cb) => { + promise + .then((r) => { + cb(null, r); + }) + .catch((e) => { + cb(e); + }); + })(); +}; class RocketChatIntegrationHandler { constructor() { this.successResults = [200, 201, 202]; @@ -229,6 +239,16 @@ class RocketChatIntegrationHandler { } buildSandbox(store = {}) { + const httpAsync = async (method, url, options) => { + try { + return { + result: await httpCall(method, url, options), + }; + } catch (error) { + return { error }; + } + }; + const sandbox = { scriptTimeout(reject) { return setTimeout(() => reject('timed out'), 3000); @@ -237,7 +257,6 @@ class RocketChatIntegrationHandler { s, console, moment, - Fiber, Promise, Store: { set: (key, val) => { @@ -246,14 +265,7 @@ class RocketChatIntegrationHandler { get: (key) => store[key], }, HTTP: (method, url, options) => { - try { - // Need to review how we will handle this, possible breaking change on removing fibers - return { - result: Promise.await(httpCall(method, url, options)), - }; - } catch (error) { - return { error }; - } + return deasync.promise(httpAsync(method, url, options)); }, }; @@ -357,20 +369,22 @@ class RocketChatIntegrationHandler { sandbox, }); - const scriptResult = vm.run(` - new Promise((resolve, reject) => { - Fiber(() => { - scriptTimeout(reject); - try { - resolve(script[method](params)) - } catch(e) { - reject(e); - } - }).run(); - }).catch((error) => { throw new Error(error); }); - `); + const result = await new Promise((resolve) => { + process.nextTick(async () => { + const scriptResult = await vm.run(` + new Promise((resolve, reject) => { + scriptTimeout(reject); + try { + resolve(script[method](params)) + } catch(e) { + reject(e); + } + }).catch((error) => { throw new Error(error); }); + `); - const result = Future.fromPromise(scriptResult).wait(); + resolve(scriptResult); + }); + }); outgoingLogger.debug({ msg: `Script method "${method}" result of the Integration "${integration.name}" is:`, diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 55d35ae764d06..258a7864aa403 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -101,6 +101,7 @@ "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/cssom": "^0.4.1", + "@types/deasync": "^0", "@types/dompurify": "^2.3.3", "@types/ejson": "^2.2.0", "@types/emojione": "^2.2.6", @@ -302,6 +303,7 @@ "csv-parse": "^5.2.0", "date-fns": "^2.28.0", "date.js": "~0.3.3", + "deasync": "^0.1.28", "debug": "~4.1.1", "dompurify": "^2.3.8", "ejson": "^2.2.2", diff --git a/yarn.lock b/yarn.lock index e0cec8b941205..59f572b5d21f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6962,6 +6962,7 @@ __metadata: "@types/cookie-parser": ^1.4.3 "@types/cors": ^2.8.12 "@types/cssom": ^0.4.1 + "@types/deasync": ^0 "@types/dompurify": ^2.3.3 "@types/ejson": ^2.2.0 "@types/emojione": ^2.2.6 @@ -7061,6 +7062,7 @@ __metadata: csv-parse: ^5.2.0 date-fns: ^2.28.0 date.js: ~0.3.3 + deasync: ^0.1.28 debug: ~4.1.1 dompurify: ^2.3.8 ejson: ^2.2.2 @@ -10283,6 +10285,13 @@ __metadata: languageName: node linkType: hard +"@types/deasync@npm:^0": + version: 0.1.2 + resolution: "@types/deasync@npm:0.1.2" + checksum: 28a49e42c0d459afabdab02f0464a0231e24ef79bf800f15f779897d9564fb2b95d887e6f338a9bc5fa67186594dc1977dd5565fea3c7e0c9b282032c7b20c51 + languageName: node + linkType: hard + "@types/debug@npm:^4": version: 4.1.7 resolution: "@types/debug@npm:4.1.7" @@ -13741,6 +13750,13 @@ __metadata: languageName: node linkType: hard +"bluebird@npm:^2.10.0": + version: 2.11.0 + resolution: "bluebird@npm:2.11.0" + checksum: f1c6cbec64100bca65c88e5de0d9ee9bbb435f7c74c68a16a9466a8b40daf64346805c2fe04af821564ce6d4199085e7855d2e272282a065eb723344815cc354 + languageName: node + linkType: hard + "blueimp-md5@npm:^2.16.0": version: 2.19.0 resolution: "blueimp-md5@npm:2.19.0" @@ -16616,6 +16632,16 @@ __metadata: languageName: node linkType: hard +"deasync@npm:^0.1.28": + version: 0.1.28 + resolution: "deasync@npm:0.1.28" + dependencies: + bindings: ^1.5.0 + node-addon-api: ^1.7.1 + checksum: e0c1ef427875c897e0d903a08410df1d0a3dfd0d2a0a1e43fb6c2824dfbc504b810bd08a0d30653117259316e1aa65409c96dbed40101c934f75bac7499e1265 + languageName: node + linkType: hard + "debug@npm:2.6.9, debug@npm:^2.2.0, debug@npm:^2.3.3, debug@npm:^2.6.0, debug@npm:^2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -26868,6 +26894,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^1.7.1": + version: 1.7.2 + resolution: "node-addon-api@npm:1.7.2" + dependencies: + node-gyp: latest + checksum: 938922b3d7cb34ee137c5ec39df6289a3965e8cab9061c6848863324c21a778a81ae3bc955554c56b6b86962f6ccab2043dd5fa3f33deab633636bd28039333f + languageName: node + linkType: hard + "node-addon-api@npm:^3.1.0": version: 3.2.1 resolution: "node-addon-api@npm:3.2.1" From e02504dbd9646fa9e9a735b974df7a287c3ba58b Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Sat, 29 Apr 2023 16:07:23 -0300 Subject: [PATCH 02/70] Remove fibers from incoming integration --- .../meteor/app/integrations/server/api/api.js | 63 ++++++++++--------- .../integrations/server/lib/triggerHandler.js | 47 +++++++------- apps/meteor/server/deasync/deasync.ts | 15 +++++ 3 files changed, 71 insertions(+), 54 deletions(-) create mode 100644 apps/meteor/server/deasync/deasync.ts diff --git a/apps/meteor/app/integrations/server/api/api.js b/apps/meteor/app/integrations/server/api/api.js index 96e130fcc9138..efd9e9cd0a63e 100644 --- a/apps/meteor/app/integrations/server/api/api.js +++ b/apps/meteor/app/integrations/server/api/api.js @@ -2,8 +2,6 @@ import { VM, VMScript } from 'vm2'; import { Meteor } from 'meteor/meteor'; import { Random } from '@rocket.chat/random'; import { Livechat } from 'meteor/rocketchat:livechat'; -import Fiber from 'fibers'; -import Future from 'fibers/future'; import _ from 'underscore'; import moment from 'moment'; import { Integrations, Users } from '@rocket.chat/models'; @@ -16,11 +14,22 @@ import { API, APIClass, defaultRateLimiterOptions } from '../../../api/server'; import { settings } from '../../../settings/server'; import { httpCall } from '../../../../server/lib/http/call'; import { deleteOutgoingIntegration } from '../methods/outgoing/deleteOutgoingIntegration'; +import { deasyncPromise } from '../../../../server/deasync/deasync'; export const forbiddenModelMethods = ['registerModel', 'getCollectionName']; const compiledScripts = {}; function buildSandbox(store = {}) { + const httpAsync = async (method, url, options) => { + try { + return { + result: await httpCall(method, url, options), + }; + } catch (error) { + return { error }; + } + }; + const sandbox = { scriptTimeout(reject) { return setTimeout(() => reject('timed out'), 3000); @@ -29,7 +38,6 @@ function buildSandbox(store = {}) { s, console, moment, - Fiber, Promise, Livechat, Store: { @@ -41,18 +49,11 @@ function buildSandbox(store = {}) { return store[key]; }, }, - HTTP(method, url, options) { - try { - // Need to review how we will handle this, possible breaking change on removing fibers - return { - result: Promise.await(httpCall(method, url, options)), - }; - } catch (error) { - return { - error, - }; - } + HTTP: (method, url, options) => { + // TODO: deprecate, track and alert + return deasyncPromise(httpAsync(method, url, options)); }, + // TODO: Export fetch as the non deprecated method }; Object.keys(Models) .filter((k) => !forbiddenModelMethods.includes(k)) @@ -216,20 +217,26 @@ async function executeIntegrationRest() { sandbox, }); - const scriptResult = vm.run(` - new Promise((resolve, reject) => { - Fiber(() => { - scriptTimeout(reject); - try { - resolve(script.process_incoming_request({ request: request })); - } catch(e) { - reject(e); - } - }).run(); - }).catch((error) => { throw new Error(error); }); - `); - - const result = Future.fromPromise(scriptResult).wait(); + const result = await new Promise((resolve, reject) => { + process.nextTick(async () => { + try { + const scriptResult = await vm.run(` + new Promise((resolve, reject) => { + scriptTimeout(reject); + try { + resolve(script.process_incoming_request({ request: request })); + } catch(e) { + reject(e); + } + }).catch((error) => { throw new Error(error); }); + `); + + resolve(scriptResult); + } catch (e) { + reject(e); + } + }); + }); if (!result) { incomingLogger.debug({ diff --git a/apps/meteor/app/integrations/server/lib/triggerHandler.js b/apps/meteor/app/integrations/server/lib/triggerHandler.js index 250237940c221..5fe002e212702 100644 --- a/apps/meteor/app/integrations/server/lib/triggerHandler.js +++ b/apps/meteor/app/integrations/server/lib/triggerHandler.js @@ -3,7 +3,6 @@ import { Meteor } from 'meteor/meteor'; import { Random } from '@rocket.chat/random'; import _ from 'underscore'; import moment from 'moment'; -import deasync from 'deasync'; import { Integrations, IntegrationHistory, Users, Rooms, Messages } from '@rocket.chat/models'; import * as Models from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; @@ -17,18 +16,8 @@ import { outgoingEvents } from '../../lib/outgoingEvents'; import { omit } from '../../../../lib/utils/omit'; import { forbiddenModelMethods } from '../api/api'; import { httpCall } from '../../../../server/lib/http/call'; +import { deasyncPromise } from '../../../../server/deasync/deasync'; -deasync.promise = function (promise) { - return deasync((cb) => { - promise - .then((r) => { - cb(null, r); - }) - .catch((e) => { - cb(e); - }); - })(); -}; class RocketChatIntegrationHandler { constructor() { this.successResults = [200, 201, 202]; @@ -265,8 +254,10 @@ class RocketChatIntegrationHandler { get: (key) => store[key], }, HTTP: (method, url, options) => { - return deasync.promise(httpAsync(method, url, options)); + // TODO: deprecate, track and alert + return deasyncPromise(httpAsync(method, url, options)); }, + // TODO: Export fetch as the non deprecated method }; Object.keys(Models) @@ -369,20 +360,24 @@ class RocketChatIntegrationHandler { sandbox, }); - const result = await new Promise((resolve) => { + const result = await new Promise((resolve, reject) => { process.nextTick(async () => { - const scriptResult = await vm.run(` - new Promise((resolve, reject) => { - scriptTimeout(reject); - try { - resolve(script[method](params)) - } catch(e) { - reject(e); - } - }).catch((error) => { throw new Error(error); }); - `); - - resolve(scriptResult); + try { + const scriptResult = await vm.run(` + new Promise((resolve, reject) => { + scriptTimeout(reject); + try { + resolve(script[method](params)) + } catch(e) { + reject(e); + } + }).catch((error) => { throw new Error(error); }); + `); + + resolve(scriptResult); + } catch (e) { + reject(e); + } }); }); diff --git a/apps/meteor/server/deasync/deasync.ts b/apps/meteor/server/deasync/deasync.ts new file mode 100644 index 0000000000000..77c45df56e65c --- /dev/null +++ b/apps/meteor/server/deasync/deasync.ts @@ -0,0 +1,15 @@ +import deasync from 'deasync'; + +const deasyncPromise = function (promise: Promise) { + return deasync((cb) => { + promise + .then((r) => { + cb(undefined, r); + }) + .catch((e) => { + cb(e, undefined); + }); + })(); +}; + +export { deasyncPromise, deasync }; From 0392a18f5677944f11ee4d0531071c96b2006de0 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 19 Apr 2023 13:25:13 -0300 Subject: [PATCH 03/70] Internalize tap --- .../packages/rocketchat-i18n/package.js | 2 +- .../packages/rocketchat-tap-i18n/.gitignore | 7 + .../packages/rocketchat-tap-i18n/client.js | 3 + .../rocketchat-tap-i18n/lib/globals.js | 12 + .../lib/plugin/compiler_configuration.coffee | 23 + .../lib/plugin/compilers/i18n.coffee | 18 + .../compilers/i18n.generic_compiler.coffee | 124 + .../lib/plugin/compilers/i18n.json.coffee | 4 + .../lib/plugin/compilers/i18n.yml.coffee | 4 + .../plugin/compilers/package-tap.i18n.coffee | 95 + .../plugin/compilers/project-tap.i18n.coffee | 122 + .../lib/plugin/compilers/share.coffee | 1 + .../lib/plugin/etc/language_names.js | 140 + .../helpers/compile_step_helpers.coffee | 23 + .../lib/plugin/helpers/helpers.coffee | 1 + .../lib/plugin/helpers/load_json.coffee | 24 + .../lib/plugin/helpers/load_yml.coffee | 25 + .../lib/tap_i18n/globals.js | 10 + .../lib/tap_i18n/json-routes.js | 49 + .../lib/tap_i18n/tap_i18n-client.coffee | 201 ++ .../lib/tap_i18n/tap_i18n-common.coffee | 143 + .../lib/tap_i18n/tap_i18n-helpers.coffee | 1 + .../lib/tap_i18n/tap_i18n-init.coffee | 1 + .../lib/tap_i18n/tap_i18n-server.coffee | 90 + .../lib/tap_i18next/tap_i18next-1.7.3.js | 2748 +++++++++++++++++ .../packages/rocketchat-tap-i18n/package.js | 68 + .../packages/rocketchat-tap-i18n/server.js | 3 + 27 files changed, 3941 insertions(+), 1 deletion(-) create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/.gitignore create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/client.js create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/globals.js create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compiler_configuration.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/i18n.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/i18n.generic_compiler.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/i18n.json.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/i18n.yml.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/package-tap.i18n.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/project-tap.i18n.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/share.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/etc/language_names.js create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/helpers/compile_step_helpers.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/helpers/helpers.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/helpers/load_json.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/helpers/load_yml.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/globals.js create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/json-routes.js create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-client.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-common.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-helpers.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-init.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-server.coffee create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18next/tap_i18next-1.7.3.js create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/package.js create mode 100644 apps/meteor/packages/rocketchat-tap-i18n/server.js diff --git a/apps/meteor/packages/rocketchat-i18n/package.js b/apps/meteor/packages/rocketchat-i18n/package.js index c32be61eb5a95..3f64cd58acb13 100644 --- a/apps/meteor/packages/rocketchat-i18n/package.js +++ b/apps/meteor/packages/rocketchat-i18n/package.js @@ -26,7 +26,7 @@ Package.onUse(function (api) { fs.readdirSync(i18nDir).forEach(function (filename) { if (filename.indexOf('.json') > -1 && filename.indexOf('livechat.') === -1 && fs.statSync(`${i18nDir}/${filename}`).size > 16) { - api.addFiles(`i18n/${filename}`); + api.addFiles(`i18n/${filename}`, ['client', 'server']); } }); diff --git a/apps/meteor/packages/rocketchat-tap-i18n/.gitignore b/apps/meteor/packages/rocketchat-tap-i18n/.gitignore new file mode 100644 index 0000000000000..cff4da41fc458 --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/.gitignore @@ -0,0 +1,7 @@ +.build* +.npm +.DS_Store +smart.lock +packages/test-pack-* +public +.test-run-log diff --git a/apps/meteor/packages/rocketchat-tap-i18n/client.js b/apps/meteor/packages/rocketchat-tap-i18n/client.js new file mode 100644 index 0000000000000..0e7aba8ab7ba2 --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/client.js @@ -0,0 +1,3 @@ +import { TAPi18nClient } from './lib/tap_i18n/tap_i18n-client'; + +export const TAPi18n = new TAPi18nClient(); diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/globals.js b/apps/meteor/packages/rocketchat-tap-i18n/lib/globals.js new file mode 100644 index 0000000000000..4b4f38e7cf58e --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/globals.js @@ -0,0 +1,12 @@ +// The globals object will be accessible to the build plugin, the server and +// the client + +/* eslint no-unused-vars: 0 */ + +globals = { + fallback_language: 'en', + langauges_tags_regex: '[a-z]{2,3}(?:-[a-zA-Z]{4})?(?:-[A-Z]{2,3})?', + project_translations_domain: 'project', + browser_path: '/tap-i18n', + debug: false, +}; diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compiler_configuration.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compiler_configuration.coffee new file mode 100644 index 0000000000000..e3677703f876c --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compiler_configuration.coffee @@ -0,0 +1,23 @@ +# Note: same compiler can be used to compile more then one package (at least in v0.9.x) + +share.compiler_configuration = + fallback_language: globals.fallback_language + packages: [] # Each time we compile package-tap.i18n we push "package_name:arch" to this array + templates_registered_for: [] # Each time we register a template we push "package_name:arch" to this array + default_project_conf_inserted_for: [] # Keeps track of the archs we've inserted the default project conf for. + # Default project conf is inserted by the *.i18.json compiler to be used + # in case the project has no project-tap.i18n + project_tap_i18n_loaded_for: [] # Keeps track of the archs we've loaded project_tap_i18n for + + tap_i18n_input_files: [] + registerInputFile: (compileStep) -> + input_file = "#{compileStep.arch}:#{compileStep._fullInputPath}" + if input_file in @tap_i18n_input_files + # A new build cycle + @packages = [] + @templates_registered_for = [] + @default_project_conf_inserted_for = [] + @project_tap_i18n_loaded_for = [] + @tap_i18n_input_files = [] + + @tap_i18n_input_files.push(input_file) diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/i18n.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/i18n.coffee new file mode 100644 index 0000000000000..37d6f30278353 --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/i18n.coffee @@ -0,0 +1,18 @@ +path = Npm.require "path" + +compilers = share.compilers + +Plugin.registerSourceHandler "i18n", (compileStep) -> + # Starting from Meteor v1.2 registerSourceHandler doesn't + # accept filenames as handlers. + # See: https://github.com/meteor/meteor/issues/3985 + # and: https://github.com/TAPevents/tap-i18n/issues/113 + # Below is a workaround until we refactor for v1.2 new + # build plugin API. + file_name = _.last compileStep.inputPath.split(path.sep) + + if file_name == "package-tap.i18n" + compilers.package_tap_i18n(compileStep) + + if file_name == "project-tap.i18n" + compilers.project_tap_i18n(compileStep) diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/i18n.generic_compiler.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/i18n.generic_compiler.coffee new file mode 100644 index 0000000000000..86199b861f628 --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/i18n.generic_compiler.coffee @@ -0,0 +1,124 @@ +path = Npm.require "path" + +helpers = share.helpers +compilers = share.compilers +compiler_configuration = share.compiler_configuration + +compilers.generic_compiler = (extension, helper) -> (compileStep) -> + compiler_configuration.registerInputFile(compileStep) + input_path = compileStep._fullInputPath + + language = path.basename(input_path).split(".").slice(0, -2).pop() + if _.isUndefined(language) or _.isEmpty(language) + compileStep.error + message: "Language-tag is not specified for *.i18n.`#{extension}' file: `#{input_path}'", + sourcePath: input_path + return + + if not RegExp("^#{globals.langauges_tags_regex}$").test(language) + compileStep.error + message: "Can't recognise '#{language}' as a language-tag: `#{input_path}'", + sourcePath: input_path + return + + translations = helper input_path, compileStep + + package_name = if helpers.isPackage(compileStep) then compileStep.packageName else globals.project_translations_domain + output = + """ + var _ = Package.underscore._, + package_name = "#{package_name}", + namespace = "#{package_name}"; + + if (package_name != "#{globals.project_translations_domain}") { + namespace = TAPi18n.packages[package_name].namespace; + } + + """ + + # only for project + if not helpers.isPackage(compileStep) + if /^(client|server)/.test(compileStep.inputPath) + compileStep.error + message: "Languages files should be common to the server and the client. Do not put them under /client or /server .", + sourcePath: input_path + return + + # add the language names to TAPi18n.languages_names + language_name = [language, language] + if language_names[language]? + language_name = language_names[language] + + if language != globals.fallback_language + # the name for the fallback_language is part of the getProjectConfJs()'s output + output += + """ + TAPi18n.languages_names["#{language}"] = #{JSON.stringify language_name}; + + """ + + # If this is a project but project-tap.i18n haven't compiled yet add default project conf + # for case there is no project-tap.i18n defined in this project. + # Reminder: we don't require projects to have project-tap.i18n + if not(helpers.isDefaultProjectConfInserted(compileStep)) and \ + not(helpers.isProjectI18nLoaded(compileStep)) + output += share.getProjectConfJs(share.project_i18n_schema.clean {}) # defined in project-tap.i18n.coffee + + helpers.markDefaultProjectConfInserted(compileStep) + + + # if fallback_language -> integrate, otherwise add to TAPi18n.translations if server arch. + if language == compiler_configuration.fallback_language + output += + """ + // integrate the fallback language translations + translations = {}; + translations[namespace] = #{JSON.stringify translations}; + TAPi18n._loadLangFileObject("#{compiler_configuration.fallback_language}", translations); + + """ + + if compileStep.archMatches "os" + if language != compiler_configuration.fallback_language + output += + """ + if(_.isUndefined(TAPi18n.translations["#{language}"])) { + TAPi18n.translations["#{language}"] = {}; + } + + if(_.isUndefined(TAPi18n.translations["#{language}"][namespace])) { + TAPi18n.translations["#{language}"][namespace] = {}; + } + + _.extend(TAPi18n.translations["#{language}"][namespace], #{JSON.stringify translations}); + + """ + + output += + """ + TAPi18n._registerServerTranslator("#{language}", namespace); + + """ + + # register i18n helper for templates, only once per web arch, only for packages + if helpers.isPackage(compileStep) + if compileStep.archMatches("web") and helpers.getCompileStepArchAndPackage(compileStep) not in compiler_configuration.templates_registered_for + output += + """ + var package_templates = _.difference(_.keys(Template), non_package_templates); + + for (var i = 0; i < package_templates.length; i++) { + var package_template = package_templates[i]; + + registerI18nTemplate(package_template); + } + + """ + compiler_configuration.templates_registered_for.push helpers.getCompileStepArchAndPackage(compileStep) + + output_path = compileStep.rootOutputPath + compileStep.inputPath.replace new RegExp("`#{extension}'$"), "js" + compileStep.addJavaScript + path: output_path, + sourcePath: input_path, + data: output, + bare: false \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/i18n.json.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/i18n.json.coffee new file mode 100644 index 0000000000000..fe65e24a02abc --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/i18n.json.coffee @@ -0,0 +1,4 @@ +helpers = share.helpers +compilers = share.compilers +compilers.i18n_json = compilers.generic_compiler('json', helpers.loadJSON) +Plugin.registerSourceHandler "i18n.json", compilers.i18n_json \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/i18n.yml.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/i18n.yml.coffee new file mode 100644 index 0000000000000..ec7e7f79362f3 --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/i18n.yml.coffee @@ -0,0 +1,4 @@ +helpers = share.helpers +compilers = share.compilers +compilers.i18n_yml = compilers.generic_compiler('yml', helpers.loadYAML) +Plugin.registerSourceHandler "i18n.yml", compilers.i18n_yml \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/package-tap.i18n.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/package-tap.i18n.coffee new file mode 100644 index 0000000000000..45db1a679c60a --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/package-tap.i18n.coffee @@ -0,0 +1,95 @@ +helpers = share.helpers +compilers = share.compilers +compiler_configuration = share.compiler_configuration + +schema = new SimpleSchema + translation_function_name: + type: String + defaultValue: "__" + label: "Translation Function Name" + optional: true + helper_name: + type: String + defaultValue: "_" + label: "Helper Name" + optional: true + namespace: + type: String + defaultValue: null + label: "Translations Namespace" + optional: true + +compilers.package_tap_i18n = (compileStep) -> + compiler_configuration.registerInputFile(compileStep) + input_path = compileStep._fullInputPath + + if helpers.isPackage(compileStep) + compileStep.error + message: "More than one package-tap.i18n found for package: #{compileStep.packageName}", + sourcePath: input_path + return + + if helpers.isProjectI18nLoaded(compileStep) + compileStep.error + message: "Can't compile package-tap.i18n if project-tap.i18n is present", + sourcePath: input_path + return + + if helpers.isDefaultProjectConfInserted(compileStep) + compileStep.error + message: "package-tap.i18n should be loaded before languages files (*.i18n.json)", + sourcePath: input_path + return + + helpers.markAsPackage(compileStep) + + package_tap_i18n = helpers.loadJSON input_path, compileStep + + if not package_tap_i18n? + package_tap_i18n = schema.clean {} + schema.clean package_tap_i18n + + try + check package_tap_i18n, schema + catch error + compileStep.error + message: "File `#{file_path}' is an invalid package-tap.i18n file (#{error})", + sourcePath: input_path + return + + package_name = compileStep.packageName + + if not package_tap_i18n.namespace? + package_tap_i18n.namespace = package_name + + namespace = package_tap_i18n.namespace + + package_i18n_js_file = + """ + TAPi18n.packages["#{package_name}"] = #{JSON.stringify(package_tap_i18n)}; + + // define package's translation function (proxy to the i18next) + #{package_tap_i18n.translation_function_name} = TAPi18n._getPackageI18nextProxy("#{namespace}"); + + """ + + if compileStep.archMatches "web" + package_i18n_js_file += + """ + // define the package's templates registrar + registerI18nTemplate = TAPi18n._getRegisterHelpersProxy("#{package_name}"); + registerTemplate = registerI18nTemplate; // XXX OBSOLETE, kept for backward compatibility will be removed in the future + + // Record the list of templates prior to package load + var _ = Package.underscore._; + non_package_templates = _.keys(Template); + + """ + + compileStep.addJavaScript + path: "package-i18n.js", + sourcePath: input_path, + data: package_i18n_js_file, + bare: false + +Plugin.registerSourceHandler "package-tap.i18n", compilers.package_tap_i18n \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/project-tap.i18n.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/project-tap.i18n.coffee new file mode 100644 index 0000000000000..0187d7161a5ed --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/project-tap.i18n.coffee @@ -0,0 +1,122 @@ +helpers = share.helpers +compilers = share.compilers +compiler_configuration = share.compiler_configuration + +share.project_i18n_schema = schema = new SimpleSchema + helper_name: + type: String + defaultValue: "_" + label: "Helper Name" + optional: true + supported_languages: + type: [String] + label: "Supported Languages" + defaultValue: null + optional: true + i18n_files_route: + type: String + label: "Unified languages files path" + defaultValue: globals.browser_path + optional: true + preloaded_langs: + type: [String] + label: "Preload languages" + defaultValue: [] + optional: true + cdn_path: + type: String + label: "Unified languages files path on CDN" + defaultValue: null + optional: true + +getProjectConfJs = share.getProjectConfJs = (conf) -> + fallback_language_name = language_names[globals.fallback_language] + + project_conf_js = """ + TAPi18n._enable(#{JSON.stringify(conf)}); + TAPi18n.languages_names["#{globals.fallback_language}"] = #{JSON.stringify fallback_language_name}; + + """ + + # If we get a list of supported languages we must make sure that we'll have a + # language name for each one of its languages. + # + # Though languages names are added for every language we find i18n.json file + # for (by the i18n.json compiler). We shouldn't rely on the existence of + # *.i18n.json file for each supported language, because a language might be + # defined as supported even when it has no i18n.json files (it's especially + # true when tap:i18n is used with tap:i18n-db) + if conf.supported_languages? + for lang_tag in conf.supported_languages + if language_names[lang_tag]? + project_conf_js += """ + TAPi18n.languages_names["#{lang_tag}"] = #{JSON.stringify language_names[lang_tag]}; + + """ + + return project_conf_js + +compilers.project_tap_i18n = (compileStep) -> + compiler_configuration.registerInputFile(compileStep) + input_path = compileStep._fullInputPath + + if helpers.isPackage(compileStep) + compileStep.error + message: "Can't load project-tap.i18n in a package: #{compileStep.packageName}", + sourcePath: input_path + return + + if helpers.isProjectI18nLoaded(compileStep) + compileStep.error + message: "Can't have more than one project-tap.i18n", + sourcePath: input_path + return + + project_tap_i18n = helpers.loadJSON input_path, compileStep + + if not project_tap_i18n? + project_tap_i18n = schema.clean {} + schema.clean project_tap_i18n + + try + check project_tap_i18n, schema + catch error + compileStep.error + message: "File `#{file_path}' is an invalid project-tap.i18n file (#{error})", + sourcePath: input_path + return + + project_i18n_js_file = getProjectConfJs project_tap_i18n + + if compileStep.archMatches("web") and not _.isEmpty project_tap_i18n.preloaded_langs + preloaded_langs = "all" + if project_tap_i18n.preloaded_langs[0] != "*" + preloaded_langs = project_tap_i18n.preloaded_langs.join(",") + + project_i18n_js_file += + """ + $.ajax({ + type: 'GET', + url: "#{project_tap_i18n.i18n_files_route}/multi/#{preloaded_langs}.json", + dataType: 'json', + success: function(data) { + for (lang_tag in data) { + TAPi18n._loadLangFileObject(lang_tag, data[lang_tag]); + TAPi18n._loaded_languages.push(lang_tag); + } + }, + data: {}, + async: false + }); + + """ + + helpers.markProjectI18nLoaded(compileStep) + + compileStep.addJavaScript + path: "project-i18n.js", + sourcePath: input_path, + data: project_i18n_js_file, + bare: false + +Plugin.registerSourceHandler "project-tap.i18n", compilers.project_tap_i18n \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/share.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/share.coffee new file mode 100644 index 0000000000000..6216e28395017 --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/compilers/share.coffee @@ -0,0 +1 @@ +share.compilers = {} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/etc/language_names.js b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/etc/language_names.js new file mode 100644 index 0000000000000..7c47e6c0f7dd4 --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/etc/language_names.js @@ -0,0 +1,140 @@ +/* eslint no-unused-vars: 0 */ + +language_names = { + af: ['Afrikaans', 'Afrikaans'], + ak: ['Akan', 'Akan'], + sq: ['Albanian', 'Shqip'], + am: ['Amharic', 'አማርኛ'], + ar: ['Arabic', 'العربية'], + hy: ['Armenian', 'Հայերեն'], + rup: ['Aromanian', 'Armãneashce'], + as: ['Assamese', 'অসমীয়া'], + az: ['Azerbaijani', 'Azərbaycan dili'], + 'az-TR': ['Azerbaijani (Turkey)', 'Azərbaycan Türkcəsi'], + ba: ['Bashkir', 'башҡорт теле'], + eu: ['Basque', 'Euskara'], + 'be-BY': ['Belarusian', 'Беларуская мова'], + bn: ['Bengali', 'বাংলা'], + bs: ['Bosnian', 'Bosanski'], + bg: ['Bulgarian', 'Български'], + mya: ['Burmese', 'ဗမာစာ'], + ca: ['Catalan', 'Català'], + bal: ['Catalan (Balear)', 'Català (Balear)'], + zh: ['Chinese', '中文'], + 'zh-CN': ['Chinese (China)', '简体中文'], + 'zh-HK': ['Chinese (Hong Kong)', '繁體中文(香港)'], + 'zh-TW': ['Chinese (Taiwan)', '繁體中文(台灣)'], + co: ['Corsican', 'corsu'], + hr: ['Croatian', 'Hrvatski'], + cs: ['Czech', 'čeština‎'], + da: ['Danish', 'Dansk'], + nl: ['Dutch', 'Nederlands'], + 'nl-BE': ['Dutch (Belgium)', 'Nederlands (België)'], + en: ['English', 'English'], + 'en-AU': ['English (Australia)', 'English (Australia)'], + 'en-CA': ['English (Canada)', 'English (Canada)'], + 'en-GB': ['English (UK)', 'English (UK)'], + eo: ['Esperanto', 'Esperanto'], + et: ['Estonian', 'Eesti'], + fo: ['Faroese', 'føroyskt'], + fi: ['Finnish', 'Suomi'], + 'fr-BE': ['French (Belgium)', 'Français de Belgique'], + fr: ['French (France)', 'Français'], + fy: ['Frisian', 'Frysk'], + fuc: ['Fulah', 'Pulaar'], + gl: ['Galician', 'Galego'], + ka: ['Georgian', 'ქართული'], + de: ['German', 'Deutsch'], + 'de-AT': ['German (Austria)', 'Östereichisches Deutsch'], + 'de-IN': ['German (informal)', 'Deutsch (informell)'], + el: ['Greek', 'Ελληνικά'], + gn: ['Guaraní', 'Avañe\'ẽ'], + haw: ['Hawaiian', 'Ōlelo Hawaiʻi'], + haz: ['Hazaragi', 'هزاره گی'], + he: ['Hebrew', 'עברית'], + hi: ['Hindi', 'हिन्दी'], + hu: ['Hungarian', 'Magyar'], + is: ['Icelandic', 'Íslenska'], + id: ['Indonesian', 'Bahasa Indonesia'], + ga: ['Irish', 'Gaelige'], + it: ['Italian', 'Italiano'], + ja: ['Japanese', '日本語'], + jv: ['Javanese', 'Basa Jawa'], + kn: ['Kannada', 'ಕನ್ನಡ'], + kk: ['Kazakh', 'Қазақ тілі'], + km: ['Khmer', 'ភាសាខ្មែរ'], + rw: ['Kinyarwanda', 'Kinyarwanda'], + ky: ['Kirghiz', 'кыргыз тили'], + ko: ['Korean', '한국어'], + ku: ['Kurdish (Sorani)', 'كوردی‎'], + lo: ['Lao', 'ພາສາລາວ'], + lv: ['Latvian', 'latviešu valoda'], + li: ['Limburgish', 'Limburgs'], + lt: ['Lithuanian', 'Lietuvių kalba'], + lb: ['Luxembourgish', 'Lëtzebuergesch'], + mk: ['Macedonian', 'македонски јазик'], + mg: ['Malagasy', 'Malagasy'], + 'ms-MY': ['Malay', 'Bahasa Melayu'], + ml: ['Malayalam', 'മലയാളം'], + mr: ['Marathi', 'मराठी'], + xmf: ['Mingrelian', 'მარგალური ნინა'], + mn: ['Mongolian', 'Монгол'], + me: ['Montenegrin', 'Crnogorski jezik'], + ne: ['Nepali', 'नेपाली'], + no: ['Norwegian', 'Norsk'], + os: ['Ossetic', 'Ирон'], + ps: ['Pashto', 'پښتو'], + fa: ['Persian', 'فارسی'], + 'fa-AF': ['Persian (Afghanistan)', 'فارسی (افغانستان'], + pl: ['Polish', 'Polski'], + 'pt-BR': ['Portuguese (Brazil)', 'Português do Brasil'], + pt: ['Portuguese (Portugal)', 'Português'], + pa: ['Punjabi', 'ਪੰਜਾਬੀ'], + rhg: ['Rohingya', 'Rohingya'], + ro: ['Romanian', 'Română'], + ru: ['Russian', 'Русский'], + rue: ['Rusyn', 'Русиньскый'], + sah: ['Sakha', 'Sakha'], + 'sa-IN': ['Sanskrit', 'भारतम्'], + srd: ['Sardinian', 'sardu'], + gd: ['Scottish Gaelic', 'Gàidhlig'], + sr: ['Serbian', 'Српски језик'], + sd: ['Sindhi', 'سندھ'], + si: ['Sinhala', 'සිංහල'], + 'sk-SK': ['Slovak', 'Slovenčina'], + 'sl-SI': ['Slovenian', 'slovenščina'], + so: ['Somali', 'Afsoomaali'], + azb: ['South Azerbaijani', 'گؤنئی آذربایجان'], + 'es-AR': ['Spanish (Argentina)', 'Español de Argentina'], + 'es-CL': ['Spanish (Chile)', 'Español de Chile'], + 'es-CO': ['Spanish (Colombia)', 'Español de Colombia'], + 'es-MX': ['Spanish (Mexico)', 'Español de México'], + 'es-PE': ['Spanish (Peru)', 'Español de Perú'], + 'es-PR': ['Spanish (Puerto Rico)', 'Español de Puerto Rico'], + es: ['Spanish (Spain)', 'Español'], + 'es-VE': ['Spanish (Venezuela)', 'Español de Venezuela'], + su: ['Sundanese', 'Basa Sunda'], + sw: ['Swahili', 'Kiswahili'], + sv: ['Swedish', 'Svenska'], + gsw: ['Swiss German', 'Schwyzerdütsch'], + tl: ['Tagalog', 'Tagalog'], + tg: ['Tajik', 'тоҷикӣ'], + 'ta-IN': ['Tamil', 'தமிழ்'], + 'ta-LK': ['Tamil (Sri Lanka)', 'தமிழ்'], + tt: ['Tatar', 'Татар теле'], + te: ['Telugu', 'తెలుగు'], + 'th-TH': ['Thai', 'ไทย'], + bo: ['Tibetan', 'བོད་སྐད'], + tir: ['Tigrinya', 'ትግርኛ'], + tr: ['Turkish', 'Türkçe'], + tuk: ['Turkmen', 'Türkmençe'], + ua: ['Ukrainian', 'Українська'], + ug: ['Uighur', 'Uyƣurqə'], + uk: ['Ukrainian', 'Українська'], + ur: ['Urdu', 'اردو'], + uz: ['Uzbek', 'O‘zbekcha'], + 'vi-VN': ['Vietnamese', 'Tiếng Việt'], + wa: ['Walloon', 'Walon'], + cy: ['Welsh', 'Cymraeg'], + 'bas-CM': ['Basa (Cameroon)', 'Bassa (Cameroon)'], +}; diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/helpers/compile_step_helpers.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/helpers/compile_step_helpers.coffee new file mode 100644 index 0000000000000..0ab5a6f58a105 --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/helpers/compile_step_helpers.coffee @@ -0,0 +1,23 @@ +compiler_configuration = share.compiler_configuration + +_.extend share.helpers, + getCompileStepArchAndPackage: (compileStep) -> + "#{compileStep.packageName}:#{compileStep.arch}" + + markAsPackage: (compileStep) -> + compiler_configuration.packages.push @.getCompileStepArchAndPackage(compileStep) + + isPackage: (compileStep) -> + @.getCompileStepArchAndPackage(compileStep) in compiler_configuration.packages + + markProjectI18nLoaded: (compileStep) -> + compiler_configuration.project_tap_i18n_loaded_for.push @.getCompileStepArchAndPackage(compileStep) + + isProjectI18nLoaded: (compileStep) -> + @.getCompileStepArchAndPackage(compileStep) in compiler_configuration.project_tap_i18n_loaded_for + + markDefaultProjectConfInserted: (compileStep) -> + compiler_configuration.default_project_conf_inserted_for.push @.getCompileStepArchAndPackage(compileStep) + + isDefaultProjectConfInserted: (compileStep) -> + @.getCompileStepArchAndPackage(compileStep) in compiler_configuration.default_project_conf_inserted_for diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/helpers/helpers.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/helpers/helpers.coffee new file mode 100644 index 0000000000000..74aff3ad70079 --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/helpers/helpers.coffee @@ -0,0 +1 @@ +share.helpers = {} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/helpers/load_json.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/helpers/load_json.coffee new file mode 100644 index 0000000000000..243fba61b7152 --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/helpers/load_json.coffee @@ -0,0 +1,24 @@ +fs = Npm.require 'fs' + +# loads a json from file_path +# +# returns undefined if file doesn't exist null if file is empty, parsed content otherwise +_.extend share.helpers, + loadJSON: (file_path, compileStep=null) -> + try # use try/catch to avoid the additional syscall to fs.existsSync + fstats = fs.statSync file_path + catch + return undefined + + if fstats.size == 0 + return null + + try + content = JSON.parse(fs.readFileSync(file_path, "utf8")) + catch error + if compileStep? + compileStep.error + message: "Can't load `#{file_path}' JSON "+error.message, + sourcePath: compileStep._fullInputPath + + throw new Error "Can't load `#{file_path}' JSON" diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/helpers/load_yml.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/helpers/load_yml.coffee new file mode 100644 index 0000000000000..de40b8cacfe6c --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/plugin/helpers/load_yml.coffee @@ -0,0 +1,25 @@ +fs = Npm.require 'fs' +YAML = Npm.require 'yamljs' + +# loads a yml from file_path +# +# returns undefined if file doesn't exist null if file is empty, parsed content otherwise +_.extend share.helpers, + loadYAML: (file_path, compileStep=null) -> + try # use try/catch to avoid the additional syscall to fs.existsSync + fstats = fs.statSync file_path + catch + return undefined + + if fstats.size == 0 + return null + + try + content = YAML.load(file_path) + catch error + if compileStep? + compileStep.error + message: "Can't load `#{file_path}' YAML", + sourcePath: compileStep._fullInputPath + + throw new Error "Can't load `#{file_path}' YAML" diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/globals.js b/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/globals.js new file mode 100644 index 0000000000000..1ae38a02316a2 --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/globals.js @@ -0,0 +1,10 @@ +// The globals object will be accessible to the build plugin, the server and +// the client + +export const globals = { + fallback_language: 'en', + langauges_tags_regex: '[a-z]{2,3}(?:-[a-zA-Z]{4})?(?:-[A-Z]{2,3})?', + project_translations_domain: 'project', + browser_path: '/tap-i18n', + debug: false, +}; diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/json-routes.js b/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/json-routes.js new file mode 100644 index 0000000000000..573b6a1b5e8ea --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/json-routes.js @@ -0,0 +1,49 @@ +import { WebApp } from 'meteor/webapp'; + +const connect = Npm.require('connect'); +const connectRoute = Npm.require('connect-route'); + +WebApp.connectHandlers.use(connect.urlencoded({ limit: '50mb' })); // Override default request size +WebApp.connectHandlers.use(connect.json({ limit: '50mb' })); // Override default request size +WebApp.connectHandlers.use(connect.query()); + +// Save reference to router for later +let connectRouter; + +const responseHeaders = { + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', +}; + +// Register as a middleware +WebApp.connectHandlers.use( + connectRoute(function (router) { + connectRouter = router; + }), +); + +function setHeaders(res, headers) { + Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value); + }); +} + +export const JsonRoutes = { + add(method, path, handler) { + // Make sure path starts with a slash + if (path[0] !== '/') { + path = `/${path}`; + } + + connectRouter[method.toLowerCase()](path, async (req, res, next) => { + // Set headers on response + setHeaders(res, responseHeaders); + + try { + await handler(req, res, next); + } catch (error) { + next(error); + } + }); + }, +}; diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-client.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-client.coffee new file mode 100644 index 0000000000000..58f943f833c78 --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-client.coffee @@ -0,0 +1,201 @@ +import { TAPi18nBase } from './tap_i18n-common'; +import { globals } from './globals'; + +export class TAPi18nClient extends TAPi18nBase + _languageSpecificTranslators: null + _languageSpecificTranslatorsTrackers: null + + _getLanguageFilePath: (lang_tag) -> + if not @_enabled() + return null + + path = if @.conf.cdn_path? then @.conf.cdn_path else @.conf.i18n_files_route + path = path.replace /\/$/, "" + if Meteor.isCordova and path[0] == "/" + path = Meteor.absoluteUrl().replace(/\/+$/, "") + path + + "#{path}/#{lang_tag}.json" + + _loadLanguage: (languageTag) -> + # Load languageTag and its dependencies languages to TAPi18next if we + # haven't loaded them already. + # + # languageTag dependencies languages are: + # * The base language if languageTag is a dialect. + # * The fallback language (en) if we haven't loaded it already. + # + # Returns a deferred object that resolves with no arguments if all files + # loaded successfully to TAPi18next and rejects with array of error + # messages otherwise + # + # Example: + # TAPi18n._loadLanguage("pt-BR") + # .done(function () { + # console.log("languageLoaded successfully"); + # }) + # .fail(function (messages) { + # console.log("Couldn't load languageTag", messages); + # }) + # + # The above example will attempt to load pt-BR, pt and en + + dfd = new $.Deferred() + + if not @_enabled() + return dfd.reject "tap-i18n is not enabled in the project level, check tap-i18n README" + + project_languages = @_getProjectLanguages() + + if languageTag in project_languages + if languageTag not in @_loaded_languages + loadLanguageTag = => + jqXHR = $.getJSON(@_getLanguageFilePath(languageTag)) + + jqXHR.done (data) => + @_loadLangFileObject(languageTag, data) + + @_loaded_languages.push languageTag + + dfd.resolve() + + jqXHR.fail (xhr, error_code) => + dfd.reject("Couldn't load language '#{languageTag}' JSON: #{error_code}") + + directDependencyLanguageTag = if "-" in languageTag then languageTag.replace(/-.*/, "") else @._fallback_language + + # load dependency language if it is part of the project and not the fallback language + if languageTag != @._fallback_language and directDependencyLanguageTag in project_languages + dependencyLoadDfd = @_loadLanguage directDependencyLanguageTag + + dependencyLoadDfd.done => + # All dependencies loaded successfully + loadLanguageTag() + + dependencyLoadDfd.fail (message) => + dfd.reject("Loading process failed since dependency language + '#{directDependencyLanguageTag}' failed to load: " + message) + else + loadLanguageTag() + else + # languageTag loaded already + dfd.resolve() + else + dfd.reject(["Language #{languageTag} is not supported"]) + + return dfd.promise() + + _registerHelpers: (package_name, template) -> + if package_name != globals.project_translations_domain + tapI18nextProxy = @_getPackageI18nextProxy(@packages[package_name].namespace) + else + tapI18nextProxy = @_getPackageI18nextProxy(globals.project_translations_domain) + + underscore_helper = (key, args...) -> + options = (args.pop()).hash + if not _.isEmpty(args) + options.sprintf = args + + tapI18nextProxy(key, options) + + # template specific helpers + if package_name != globals.project_translations_domain + # {{_ }} + if Template[template]? and Template[template].helpers? + helpers = {} + helpers[@packages[package_name].helper_name] = underscore_helper + Template[template].helpers(helpers) + + # global helpers + else + # {{_ }} + UI.registerHelper @conf.helper_name, underscore_helper + + # {{languageTag}} + UI.registerHelper "languageTag", () => @getLanguage() + + return + + _getRegisterHelpersProxy: (package_name) -> + # A proxy to _registerHelpers where the package_name is fixed to package_name + (template) => + @_registerHelpers(package_name, template) + + _prepareLanguageSpecificTranslator: (lang_tag) -> + dfd = (new $.Deferred()).resolve().promise() + + if lang_tag of @_languageSpecificTranslatorsTrackers + return dfd + + @_languageSpecificTranslatorsTrackers[lang_tag] = new Tracker.Dependency + + if not(lang_tag of @_languageSpecificTranslators) + dfd = @_loadLanguage(lang_tag) + .done => + @_languageSpecificTranslators[lang_tag] = @_getSpecificLangTranslator(lang_tag) + + @_languageSpecificTranslatorsTrackers[lang_tag].changed() + + return dfd + + _getPackageI18nextProxy: (package_name) -> + # A proxy to TAPi18next.t where the namespace is preset to the package's + + (key, options, lang_tag=null) => + # Devs get confused and use lang option instead of lng option, make lang + # alias of lng + if options?.lang? and not options?.lng? + options.lng = options.lang + + if options?.lng? and not lang_tag? + lang_tag = options.lng + # Remove options.lng so we won't pass it to the regular TAPi18next + # before the language specific translator is ready to keep behavior + # consistent. + # + # If lang is actually ready before the language specifc translator is + # ready, TAPi18next will translate to lang_tag if we won't remove + # options.lng. + delete options.lng + + if lang_tag? + @_prepareLanguageSpecificTranslator(lang_tag) + + @_languageSpecificTranslatorsTrackers[lang_tag].depend() + + if lang_tag of @_languageSpecificTranslators + return @_languageSpecificTranslators[lang_tag] "#{@_getPackageDomain(package_name)}:#{key}", options + else + return TAPi18next.t "#{@_getPackageDomain(package_name)}:#{key}", options + + # If inside a reactive computation, we want to invalidate the computation if the client lang changes + @_language_changed_tracker.depend() + + + TAPi18next.t "#{@_getPackageDomain(package_name)}:#{key}", options + + _onceEnabled: () -> + @_registerHelpers globals.project_translations_domain + + _abortPreviousSetLang: null + setLanguage: (lang_tag) -> + self = @ + + @_abortPreviousSetLang?() + + isAborted = false + @_abortPreviousSetLang = -> isAborted = true + + @_loadLanguage(lang_tag).then => + if not isAborted + TAPi18next.setLng(lang_tag) + + @_language_changed_tracker.changed() + Session.set @_loaded_lang_session_key, lang_tag + + getLanguage: -> + if not @._enabled() + return null + + session_lang = Session.get @_loaded_lang_session_key + + if session_lang? then session_lang else @._fallback_language diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-common.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-common.coffee new file mode 100644 index 0000000000000..f98d92bd0ae43 --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-common.coffee @@ -0,0 +1,143 @@ +import TAPi18next from '../tap_i18next/tap_i18next-1.7.3.js' +import { globals } from './globals' + +fallback_language = globals.fallback_language + +TAPi18next.init({ resStore: {}, fallbackLng: globals.fallback_language, useCookie: false }); + +export class TAPi18nBase extends EventEmitter + _loaded_lang_session_key: "TAPi18n::loaded_lang" + + constructor: -> + super() + @_fallback_language = fallback_language + + @_language_changed_tracker = new Tracker.Dependency + + @_loaded_languages = [fallback_language] # stores the loaded languages, the fallback language is loaded automatically + + @conf = null # If conf isn't null we assume that tap:i18n is enabled for the project. + # We assume conf is valid, we sterilize and validate it during the build process. + + @packages = {} # Stores the packages' package-tap.i18n jsons + + @languages_names = {} # Stores languages that we've found languages files for in the project dir. + # format: + # { + # lang_tag: [lang_name_in_english, lang_name_in_local_language] + # } + + @translations = {} # Stores the packages/project translations - Server side only + # fallback_language translations are not stored here + + + if Meteor.isClient + Session.set @_loaded_lang_session_key, null + + @_languageSpecificTranslators = {} + @_languageSpecificTranslatorsTrackers = {} + + if Meteor.isServer + @server_translators = {} + + Meteor.startup => + # If tap-i18n is enabled for that project + if @_enabled() + @_registerHTTPMethod() + + @__ = @_getPackageI18nextProxy(globals.project_translations_domain) + + TAPi18next.setLng fallback_language + + _enable: (conf) -> + # tap:i18n gets enabled for a project once a conf file is set for it. + # It can be either a conf object that was set by project-tap.i18n file or + # a default conf, which is being added if the project has lang files + # (*.i18n.json) but not project-tap.i18n + @conf = conf + + @._onceEnabled() + + _onceEnabled: () -> + # The arch specific code can use this for procedures that should be performed once + # tap:i18n gets enabled (project conf file is being set) + return + + _enabled: -> + # read the comment of @conf + @conf? + + _getPackageDomain: (package_name) -> + package_name.replace(/:/g, "-") + + addResourceBundle: (lang_tag, package_name, translations) -> + TAPi18next.addResourceBundle(lang_tag, @_getPackageDomain(package_name), translations) + + _getSpecificLangTranslator: (lang) -> + current_lang = TAPi18next.lng() + + translator = null + TAPi18next.setLng lang, {fixLng: true}, (lang_translator) => + translator = lang_translator + + # Restore i18next lang that had been changed in the process of generating + # lang specific translator + TAPi18next.setLng current_lang + + return translator + + _getProjectLanguages: () -> + # Return an array of languages available for the current project + if @._enabled() + if _.isArray @.conf.supported_languages + return _.union([@._fallback_language], @.conf.supported_languages) + else + # If supported_languages is null, all the languages we found + # translations files to in the project level are considered supported. + # We use the @.languages_names array to tell which languages we found + # since for every i18n.json file we found in the project level we add + # an entry for its language to @.languages_names in the build process. + # + # We also know for certain that when tap-i18n is enabled the fallback + # lang is in @.languages_names + return _.keys @.languages_names + else + return [@._fallback_language] + + getLanguages: -> + if not @._enabled() + return null + + languages = {} + for lang_tag in @._getProjectLanguages() + languages[lang_tag] = + name: @.languages_names[lang_tag][1] + en: @.languages_names[lang_tag][0] + + languages + + _loadLangFileObject: (language_tag, data) -> + for package_name, package_keys of data + # Translations that are added by loadTranslations() have higher priority + package_keys = _.extend({}, package_keys, @_loadTranslations_cache[language_tag]?[package_name] or {}) + + @addResourceBundle(language_tag, package_name, package_keys) + + _loadTranslations_cache: {} + loadTranslations: (translations, namespace) -> + project_languages = @_getProjectLanguages() + + for language_tag, translation_keys of translations + if not @_loadTranslations_cache[language_tag]? + @_loadTranslations_cache[language_tag] = {} + + if not @_loadTranslations_cache[language_tag][namespace]? + @_loadTranslations_cache[language_tag][namespace] = {} + + _.extend(@_loadTranslations_cache[language_tag][namespace], translation_keys) + + @addResourceBundle(language_tag, namespace, translation_keys) + + if Meteor.isClient and @getLanguage() == language_tag + # Retranslate if session language updated + @_language_changed_tracker.changed() diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-helpers.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-helpers.coffee new file mode 100644 index 0000000000000..74aff3ad70079 --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-helpers.coffee @@ -0,0 +1 @@ +share.helpers = {} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-init.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-init.coffee new file mode 100644 index 0000000000000..f51365a4e7a0c --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-init.coffee @@ -0,0 +1 @@ +TAPi18n = new TAPi18n() \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-server.coffee b/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-server.coffee new file mode 100644 index 0000000000000..ba5031ce17b2a --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18n/tap_i18n-server.coffee @@ -0,0 +1,90 @@ +import { TAPi18nBase } from './tap_i18n-common'; +import { globals } from './globals'; +import { JsonRoutes } from './json-routes'; + +export class TAPi18nServer extends TAPi18nBase + server_translators: null + + _registerServerTranslator: (lang_tag, package_name) -> + if @_enabled() + if not(lang_tag of @server_translators) + @server_translators[lang_tag] = @_getSpecificLangTranslator(lang_tag) + + # fallback language is integrated, and isn't part of @translations + if lang_tag != @_fallback_language + @addResourceBundle(lang_tag, package_name, @translations[lang_tag][package_name]) + + if not(@_fallback_language of @server_translators) + @server_translators[@_fallback_language] = @_getSpecificLangTranslator(@_fallback_language) + + _registerAllServerTranslators: () -> + for lang_tag in @_getProjectLanguages() + for package_name of @translations[lang_tag] + @_registerServerTranslator(lang_tag, package_name) + + _getPackageI18nextProxy: (package_name) -> + # A proxy to TAPi18next.t where the namespace is preset to the package's + (key, options, lang_tag=null) => + if not lang_tag? + # translate to fallback_language + return @server_translators[@_fallback_language] "#{@_getPackageDomain(package_name)}:#{key}", options + else if not(lang_tag of @server_translators) + console.log "Warning: language #{lang_tag} is not supported in this project, fallback language (#{@_fallback_language})" + return @server_translators[@_fallback_language] "#{@_getPackageDomain(package_name)}:#{key}", options + else + return @server_translators[lang_tag] "#{@_getPackageDomain(package_name)}:#{key}", options + + _registerHTTPMethod: -> + self = @ + + methods = {} + + if not self._enabled() + throw new Meteor.Error 500, "tap-i18n has to be enabled in order to register the HTTP method" + + JsonRoutes.add 'get', "#{self.conf.i18n_files_route.replace(/\/$/, "")}/multi/:langs", (req, res, next) -> + if not RegExp("^((#{globals.langauges_tags_regex},)*#{globals.langauges_tags_regex}|all).json$").test(req.params.langs) + return JsonRoutes.sendResult res, + code: 401 + + langs = req.params.langs.replace ".json", "" + + if langs == "all" + output = self.translations + else + output = {} + + langs = langs.split(",") + for lang_tag in langs + if lang_tag in self._getProjectLanguages() and \ + lang_tag != self._fallback_language # fallback language is integrated to the bundle + language_translations = self.translations[lang_tag] + + if language_translations? + output[lang_tag] = language_translations + + return JsonRoutes.sendResult res, + data: output + + JsonRoutes.add 'get', "#{self.conf.i18n_files_route.replace(/\/$/, "")}/:lang", (req, res, next) -> + if not RegExp("^#{globals.langauges_tags_regex}.json$").test(req.params.lang) + return JsonRoutes.sendResult res, + code: 401 + + lang_tag = req.params.lang.replace ".json", "" + + if lang_tag not in self._getProjectLanguages() or \ + lang_tag == self._fallback_language # fallback language is integrated to the bundle + return JsonRoutes.sendResult res, + code: 404 # not found + + language_translations = self.translations[lang_tag] + # returning {} if lang_tag is not in translations allows the project + # developer to force a language supporte with project-tap.i18n's + # supported_languages property, even if that language has no lang + # files. + return JsonRoutes.sendResult res, + data: (if language_translations? then language_translations else {}) + + _onceEnabled: -> + @_registerAllServerTranslators() diff --git a/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18next/tap_i18next-1.7.3.js b/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18next/tap_i18next-1.7.3.js new file mode 100644 index 0000000000000..dbf4f810bd68a --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/lib/tap_i18next/tap_i18next-1.7.3.js @@ -0,0 +1,2748 @@ +// tap_i18next is a copy of i18next that expose i18next to the global namespace +// under the name name TAPi18next instead of i18n to (1) avoid interfering with other +// Meteor packages that might use i18n with different configurations than we do +// or worse - (2) using a different version of i18next +// +// setJqueryExt is disabled by default in TAPi18next +// sprintf is a default postProcess in TAPi18next +// +// TAPi18next is set outside of the singleton builder to make it available in the +// package level + +// i18next, v1.7.3 +// Copyright (c)2014 Jan Mühlemann (jamuhl). +// Distributed under MIT license +// http://i18next.com + +// set TAPi18next outside of the singleton builder to make it available in the package level +TAPi18next = {}; +(function() { + + // add indexOf to non ECMA-262 standard compliant browsers + if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { + "use strict"; + if (this == null) { + throw new TypeError(); + } + var t = Object(this); + var len = t.length >>> 0; + if (len === 0) { + return -1; + } + var n = 0; + if (arguments.length > 0) { + n = Number(arguments[1]); + if (n != n) { // shortcut for verifying if it's NaN + n = 0; + } else if (n != 0 && n != Infinity && n != -Infinity) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + if (n >= len) { + return -1; + } + var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + return -1; + } + } + + // add lastIndexOf to non ECMA-262 standard compliant browsers + if (!Array.prototype.lastIndexOf) { + Array.prototype.lastIndexOf = function(searchElement /*, fromIndex*/) { + "use strict"; + if (this == null) { + throw new TypeError(); + } + var t = Object(this); + var len = t.length >>> 0; + if (len === 0) { + return -1; + } + var n = len; + if (arguments.length > 1) { + n = Number(arguments[1]); + if (n != n) { + n = 0; + } else if (n != 0 && n != (1 / 0) && n != -(1 / 0)) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + var k = n >= 0 ? Math.min(n, len - 1) : len - Math.abs(n); + for (; k >= 0; k--) { + if (k in t && t[k] === searchElement) { + return k; + } + } + return -1; + }; + } + + // Add string trim for IE8. + if (typeof String.prototype.trim !== 'function') { + String.prototype.trim = function() { + return this.replace(/^\s+|\s+$/g, ''); + } + } + + var root = this + , $ = root.jQuery || root.Zepto + , resStore = {} + , currentLng + , replacementCounter = 0 + , languages = [] + , initialized = false; + + + // Export the i18next object for **CommonJS**. + // If we're not in CommonJS, add `i18n` to the + // global object or to jquery. + if (typeof module !== 'undefined' && module.exports) { + module.exports = TAPi18next; + } else { + if ($) { + $.TAPi18next = $.TAPi18next || TAPi18next; + } + + root.TAPi18next = root.TAPi18next || TAPi18next; + } + // defaults + var o = { + lng: undefined, + load: 'all', + preload: [], + lowerCaseLng: false, + returnObjectTrees: false, + fallbackLng: ['dev'], + fallbackNS: [], + detectLngQS: 'setLng', + ns: 'translation', + fallbackOnNull: true, + fallbackOnEmpty: false, + fallbackToDefaultNS: false, + nsseparator: ':', + keyseparator: '.', + selectorAttr: 'data-i18n', + debug: false, + + resGetPath: 'locales/__lng__/__ns__.json', + resPostPath: 'locales/add/__lng__/__ns__', + + getAsync: true, + postAsync: true, + + resStore: undefined, + useLocalStorage: false, + localStorageExpirationTime: 7*24*60*60*1000, + + dynamicLoad: false, + sendMissing: false, + sendMissingTo: 'fallback', // current | all + sendType: 'POST', + + interpolationPrefix: '__', + interpolationSuffix: '__', + reusePrefix: '$t(', + reuseSuffix: ')', + pluralSuffix: '_plural', + pluralNotFound: ['plural_not_found', Math.random()].join(''), + contextNotFound: ['context_not_found', Math.random()].join(''), + escapeInterpolation: false, + + setJqueryExt: false, + defaultValueFromContent: true, + useDataAttrOptions: false, + cookieExpirationTime: undefined, + useCookie: true, + cookieName: 'TAPi18next', + cookieDomain: undefined, + + objectTreeKeyHandler: undefined, + postProcess: ["sprintf"], + parseMissingKey: undefined, + + shortcutFunction: 'sprintf' // or: defaultValue + }; + function _extend(target, source) { + if (!source || typeof source === 'function') { + return target; + } + + for (var attr in source) { target[attr] = source[attr]; } + return target; + } + + function _each(object, callback, args) { + var name, i = 0, + length = object.length, + isObj = length === undefined || Object.prototype.toString.apply(object) !== '[object Array]' || typeof object === "function"; + + if (args) { + if (isObj) { + for (name in object) { + if (callback.apply(object[name], args) === false) { + break; + } + } + } else { + for ( ; i < length; ) { + if (callback.apply(object[i++], args) === false) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if (isObj) { + for (name in object) { + if (callback.call(object[name], name, object[name]) === false) { + break; + } + } + } else { + for ( ; i < length; ) { + if (callback.call(object[i], i, object[i++]) === false) { + break; + } + } + } + } + + return object; + } + + var _entityMap = { + "&": "&", + "<": "<", + ">": ">", + '"': '"', + "'": ''', + "/": '/' + }; + + function _escape(data) { + if (typeof data === 'string') { + return data.replace(/[&<>"'\/]/g, function (s) { + return _entityMap[s]; + }); + }else{ + return data; + } + } + + function _ajax(options) { + + // v0.5.0 of https://github.com/goloroden/http.js + var getXhr = function (callback) { + // Use the native XHR object if the browser supports it. + if (window.XMLHttpRequest) { + return callback(null, new XMLHttpRequest()); + } else if (window.ActiveXObject) { + // In Internet Explorer check for ActiveX versions of the XHR object. + try { + return callback(null, new ActiveXObject("Msxml2.XMLHTTP")); + } catch (e) { + return callback(null, new ActiveXObject("Microsoft.XMLHTTP")); + } + } + + // If no XHR support was found, throw an error. + return callback(new Error()); + }; + + var encodeUsingUrlEncoding = function (data) { + if(typeof data === 'string') { + return data; + } + + var result = []; + for(var dataItem in data) { + if(data.hasOwnProperty(dataItem)) { + result.push(encodeURIComponent(dataItem) + '=' + encodeURIComponent(data[dataItem])); + } + } + + return result.join('&'); + }; + + var utf8 = function (text) { + text = text.replace(/\r\n/g, '\n'); + var result = ''; + + for(var i = 0; i < text.length; i++) { + var c = text.charCodeAt(i); + + if(c < 128) { + result += String.fromCharCode(c); + } else if((c > 127) && (c < 2048)) { + result += String.fromCharCode((c >> 6) | 192); + result += String.fromCharCode((c & 63) | 128); + } else { + result += String.fromCharCode((c >> 12) | 224); + result += String.fromCharCode(((c >> 6) & 63) | 128); + result += String.fromCharCode((c & 63) | 128); + } + } + + return result; + }; + + var base64 = function (text) { + var keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + + text = utf8(text); + var result = '', + chr1, chr2, chr3, + enc1, enc2, enc3, enc4, + i = 0; + + do { + chr1 = text.charCodeAt(i++); + chr2 = text.charCodeAt(i++); + chr3 = text.charCodeAt(i++); + + enc1 = chr1 >> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); + enc4 = chr3 & 63; + + if(isNaN(chr2)) { + enc3 = enc4 = 64; + } else if(isNaN(chr3)) { + enc4 = 64; + } + + result += + keyStr.charAt(enc1) + + keyStr.charAt(enc2) + + keyStr.charAt(enc3) + + keyStr.charAt(enc4); + chr1 = chr2 = chr3 = ''; + enc1 = enc2 = enc3 = enc4 = ''; + } while(i < text.length); + + return result; + }; + + var mergeHeaders = function () { + // Use the first header object as base. + var result = arguments[0]; + + // Iterate through the remaining header objects and add them. + for(var i = 1; i < arguments.length; i++) { + var currentHeaders = arguments[i]; + for(var header in currentHeaders) { + if(currentHeaders.hasOwnProperty(header)) { + result[header] = currentHeaders[header]; + } + } + } + + // Return the merged headers. + return result; + }; + + var ajax = function (method, url, options, callback) { + // Adjust parameters. + if(typeof options === 'function') { + callback = options; + options = {}; + } + + // Set default parameter values. + options.cache = options.cache || false; + options.data = options.data || {}; + options.headers = options.headers || {}; + options.jsonp = options.jsonp || false; + options.async = options.async === undefined ? true : options.async; + + // Merge the various header objects. + var headers = mergeHeaders({ + 'accept': '*/*', + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8' + }, ajax.headers, options.headers); + + // Encode the data according to the content-type. + var payload; + if (headers['content-type'] === 'application/json') { + payload = JSON.stringify(options.data); + } else { + payload = encodeUsingUrlEncoding(options.data); + } + + // Specially prepare GET requests: Setup the query string, handle caching and make a JSONP call + // if neccessary. + if(method === 'GET') { + // Setup the query string. + var queryString = []; + if(payload) { + queryString.push(payload); + payload = null; + } + + // Handle caching. + if(!options.cache) { + queryString.push('_=' + (new Date()).getTime()); + } + + // If neccessary prepare the query string for a JSONP call. + if(options.jsonp) { + queryString.push('callback=' + options.jsonp); + queryString.push('jsonp=' + options.jsonp); + } + + // Merge the query string and attach it to the url. + queryString = queryString.join('&'); + if (queryString.length > 1) { + if (url.indexOf('?') > -1) { + url += '&' + queryString; + } else { + url += '?' + queryString; + } + } + + // Make a JSONP call if neccessary. + if(options.jsonp) { + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = url; + head.appendChild(script); + return; + } + } + + // Since we got here, it is no JSONP request, so make a normal XHR request. + getXhr(function (err, xhr) { + if(err) return callback(err); + + // Open the request. + xhr.open(method, url, options.async); + + // Set the request headers. + for(var header in headers) { + if(headers.hasOwnProperty(header)) { + xhr.setRequestHeader(header, headers[header]); + } + } + + // Handle the request events. + xhr.onreadystatechange = function () { + if(xhr.readyState === 4) { + var data = xhr.responseText || ''; + + // If no callback is given, return. + if(!callback) { + return; + } + + // Return an object that provides access to the data as text and JSON. + callback(xhr.status, { + text: function () { + return data; + }, + + json: function () { + return JSON.parse(data); + } + }); + } + }; + + // Actually send the XHR request. + xhr.send(payload); + }); + }; + + // Define the external interface. + var http = { + authBasic: function (username, password) { + ajax.headers['Authorization'] = 'Basic ' + base64(username + ':' + password); + }, + + connect: function (url, options, callback) { + return ajax('CONNECT', url, options, callback); + }, + + del: function (url, options, callback) { + return ajax('DELETE', url, options, callback); + }, + + get: function (url, options, callback) { + return ajax('GET', url, options, callback); + }, + + head: function (url, options, callback) { + return ajax('HEAD', url, options, callback); + }, + + headers: function (headers) { + ajax.headers = headers || {}; + }, + + isAllowed: function (url, verb, callback) { + this.options(url, function (status, data) { + callback(data.text().indexOf(verb) !== -1); + }); + }, + + options: function (url, options, callback) { + return ajax('OPTIONS', url, options, callback); + }, + + patch: function (url, options, callback) { + return ajax('PATCH', url, options, callback); + }, + + post: function (url, options, callback) { + return ajax('POST', url, options, callback); + }, + + put: function (url, options, callback) { + return ajax('PUT', url, options, callback); + }, + + trace: function (url, options, callback) { + return ajax('TRACE', url, options, callback); + } + }; + + + var methode = options.type ? options.type.toLowerCase() : 'get'; + + http[methode](options.url, options, function (status, data) { + if (status === 200) { + options.success(data.json(), status, null); + } else { + options.error(data.text(), status, null); + } + }); + } + + var _cookie = { + create: function(name,value,minutes,domain) { + var expires; + if (minutes) { + var date = new Date(); + date.setTime(date.getTime()+(minutes*60*1000)); + expires = "; expires="+date.toGMTString(); + } + else expires = ""; + domain = (domain)? "domain="+domain+";" : ""; + document.cookie = name+"="+value+expires+";"+domain+"path=/"; + }, + + read: function(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length,c.length); + } + return null; + }, + + remove: function(name) { + this.create(name,"",-1); + } + }; + + var cookie_noop = { + create: function(name,value,minutes,domain) {}, + read: function(name) { return null; }, + remove: function(name) {} + }; + + + + // move dependent functions to a container so that + // they can be overriden easier in no jquery environment (node.js) + var f = { + extend: $ ? $.extend : _extend, + each: $ ? $.each : _each, + ajax: $ ? $.ajax : (typeof document !== 'undefined' ? _ajax : function() {}), + cookie: typeof document !== 'undefined' ? _cookie : cookie_noop, + detectLanguage: detectLanguage, + escape: _escape, + log: function(str) { + if (o.debug && typeof console !== "undefined") console.log(str); + }, + toLanguages: function(lng) { + var languages = []; + if (typeof lng === 'string' && lng.indexOf('-') > -1) { + var parts = lng.split('-'); + + lng = o.lowerCaseLng ? + parts[0].toLowerCase() + '-' + parts[1].toLowerCase() : + parts[0].toLowerCase() + '-' + parts[1].toUpperCase(); + + if (o.load !== 'unspecific') languages.push(lng); + if (o.load !== 'current') languages.push(parts[0]); + } else { + languages.push(lng); + } + + for (var i = 0; i < o.fallbackLng.length; i++) { + if (languages.indexOf(o.fallbackLng[i]) === -1 && o.fallbackLng[i]) languages.push(o.fallbackLng[i]); + } + + return languages; + }, + regexEscape: function(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + } + }; + function init(options, cb) { + + if (typeof options === 'function') { + cb = options; + options = {}; + } + options = options || {}; + + // override defaults with passed in options + f.extend(o, options); + delete o.fixLng; /* passed in each time */ + + // create namespace object if namespace is passed in as string + if (typeof o.ns == 'string') { + o.ns = { namespaces: [o.ns], defaultNs: o.ns}; + } + + // fallback namespaces + if (typeof o.fallbackNS == 'string') { + o.fallbackNS = [o.fallbackNS]; + } + + // fallback languages + if (typeof o.fallbackLng == 'string' || typeof o.fallbackLng == 'boolean') { + o.fallbackLng = [o.fallbackLng]; + } + + // escape prefix/suffix + o.interpolationPrefixEscaped = f.regexEscape(o.interpolationPrefix); + o.interpolationSuffixEscaped = f.regexEscape(o.interpolationSuffix); + + if (!o.lng) o.lng = f.detectLanguage(); + if (o.lng) { + // set cookie with lng set (as detectLanguage will set cookie on need) + if (o.useCookie) f.cookie.create(o.cookieName, o.lng, o.cookieExpirationTime, o.cookieDomain); + } else { + o.lng = o.fallbackLng[0]; + if (o.useCookie) f.cookie.remove(o.cookieName); + } + + languages = f.toLanguages(o.lng); + currentLng = languages[0]; + f.log('currentLng set to: ' + currentLng); + + var lngTranslate = translate; + if (options.fixLng) { + lngTranslate = function(key, options) { + options = options || {}; + options.lng = options.lng || lngTranslate.lng; + return translate(key, options); + }; + lngTranslate.lng = currentLng; + } + + pluralExtensions.setCurrentLng(currentLng); + + // add JQuery extensions + if ($ && o.setJqueryExt) addJqueryFunct(); + + // jQuery deferred + var deferred; + if ($ && $.Deferred) { + deferred = $.Deferred(); + } + + // return immidiatly if res are passed in + if (o.resStore) { + resStore = o.resStore; + initialized = true; + if (cb) cb(lngTranslate); + if (deferred) deferred.resolve(lngTranslate); + if (deferred) return deferred.promise(); + return; + } + + // languages to load + var lngsToLoad = f.toLanguages(o.lng); + if (typeof o.preload === 'string') o.preload = [o.preload]; + for (var i = 0, l = o.preload.length; i < l; i++) { + var pres = f.toLanguages(o.preload[i]); + for (var y = 0, len = pres.length; y < len; y++) { + if (lngsToLoad.indexOf(pres[y]) < 0) { + lngsToLoad.push(pres[y]); + } + } + } + + // else load them + TAPi18next.sync.load(lngsToLoad, o, function(err, store) { + resStore = store; + initialized = true; + + if (cb) cb(lngTranslate); + if (deferred) deferred.resolve(lngTranslate); + }); + + if (deferred) return deferred.promise(); + } + function preload(lngs, cb) { + if (typeof lngs === 'string') lngs = [lngs]; + for (var i = 0, l = lngs.length; i < l; i++) { + if (o.preload.indexOf(lngs[i]) < 0) { + o.preload.push(lngs[i]); + } + } + return init(cb); + } + + function addResourceBundle(lng, ns, resources) { + if (typeof ns !== 'string') { + resources = ns; + ns = o.ns.defaultNs; + } else if (o.ns.namespaces.indexOf(ns) < 0) { + o.ns.namespaces.push(ns); + } + + resStore[lng] = resStore[lng] || {}; + resStore[lng][ns] = resStore[lng][ns] || {}; + + f.extend(resStore[lng][ns], resources); + } + + function removeResourceBundle(lng, ns) { + if (typeof ns !== 'string') { + ns = o.ns.defaultNs; + } + + resStore[lng] = resStore[lng] || {}; + resStore[lng][ns] = {}; + } + + function setDefaultNamespace(ns) { + o.ns.defaultNs = ns; + } + + function loadNamespace(namespace, cb) { + loadNamespaces([namespace], cb); + } + + function loadNamespaces(namespaces, cb) { + var opts = { + dynamicLoad: o.dynamicLoad, + resGetPath: o.resGetPath, + getAsync: o.getAsync, + customLoad: o.customLoad, + ns: { namespaces: namespaces, defaultNs: ''} /* new namespaces to load */ + }; + + // languages to load + var lngsToLoad = f.toLanguages(o.lng); + if (typeof o.preload === 'string') o.preload = [o.preload]; + for (var i = 0, l = o.preload.length; i < l; i++) { + var pres = f.toLanguages(o.preload[i]); + for (var y = 0, len = pres.length; y < len; y++) { + if (lngsToLoad.indexOf(pres[y]) < 0) { + lngsToLoad.push(pres[y]); + } + } + } + + // check if we have to load + var lngNeedLoad = []; + for (var a = 0, lenA = lngsToLoad.length; a < lenA; a++) { + var needLoad = false; + var resSet = resStore[lngsToLoad[a]]; + if (resSet) { + for (var b = 0, lenB = namespaces.length; b < lenB; b++) { + if (!resSet[namespaces[b]]) needLoad = true; + } + } else { + needLoad = true; + } + + if (needLoad) lngNeedLoad.push(lngsToLoad[a]); + } + + if (lngNeedLoad.length) { + TAPi18next.sync._fetch(lngNeedLoad, opts, function(err, store) { + var todo = namespaces.length * lngNeedLoad.length; + + // load each file individual + f.each(namespaces, function(nsIndex, nsValue) { + + // append namespace to namespace array + if (o.ns.namespaces.indexOf(nsValue) < 0) { + o.ns.namespaces.push(nsValue); + } + + f.each(lngNeedLoad, function(lngIndex, lngValue) { + resStore[lngValue] = resStore[lngValue] || {}; + resStore[lngValue][nsValue] = store[lngValue][nsValue]; + + todo--; // wait for all done befor callback + if (todo === 0 && cb) { + if (o.useLocalStorage) TAPi18next.sync._storeLocal(resStore); + cb(); + } + }); + }); + }); + } else { + if (cb) cb(); + } + } + + function setLng(lng, options, cb) { + if (typeof options === 'function') { + cb = options; + options = {}; + } else if (!options) { + options = {}; + } + + options.lng = lng; + return init(options, cb); + } + + function lng() { + return currentLng; + } + function addJqueryFunct() { + // $.t shortcut + $.t = $.t || translate; + + function parse(ele, key, options) { + if (key.length === 0) return; + + var attr = 'text'; + + if (key.indexOf('[') === 0) { + var parts = key.split(']'); + key = parts[1]; + attr = parts[0].substr(1, parts[0].length-1); + } + + if (key.indexOf(';') === key.length-1) { + key = key.substr(0, key.length-2); + } + + var optionsToUse; + if (attr === 'html') { + optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.html() }, options) : options; + ele.html($.t(key, optionsToUse)); + } else if (attr === 'text') { + optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.text() }, options) : options; + ele.text($.t(key, optionsToUse)); + } else if (attr === 'prepend') { + optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.html() }, options) : options; + ele.prepend($.t(key, optionsToUse)); + } else if (attr === 'append') { + optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.html() }, options) : options; + ele.append($.t(key, optionsToUse)); + } else if (attr.indexOf("data-") === 0) { + var dataAttr = attr.substr(("data-").length); + optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.data(dataAttr) }, options) : options; + var translated = $.t(key, optionsToUse); + //we change into the data cache + ele.data(dataAttr, translated); + //we change into the dom + ele.attr(attr, translated); + } else { + optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.attr(attr) }, options) : options; + ele.attr(attr, $.t(key, optionsToUse)); + } + } + + function localize(ele, options) { + var key = ele.attr(o.selectorAttr); + if (!key && typeof key !== 'undefined' && key !== false) key = ele.text() || ele.val(); + if (!key) return; + + var target = ele + , targetSelector = ele.data("i18n-target"); + if (targetSelector) { + target = ele.find(targetSelector) || ele; + } + + if (!options && o.useDataAttrOptions === true) { + options = ele.data("i18n-options"); + } + options = options || {}; + + if (key.indexOf(';') >= 0) { + var keys = key.split(';'); + + $.each(keys, function(m, k) { + if (k !== '') parse(target, k, options); + }); + + } else { + parse(target, key, options); + } + + if (o.useDataAttrOptions === true) ele.data("i18n-options", options); + } + + // fn + $.fn.TAPi18next = function (options) { + return this.each(function() { + // localize element itself + localize($(this), options); + + // localize childs + var elements = $(this).find('[' + o.selectorAttr + ']'); + elements.each(function() { + localize($(this), options); + }); + }); + }; + } + function applyReplacement(str, replacementHash, nestedKey, options) { + if (!str) return str; + + options = options || replacementHash; // first call uses replacement hash combined with options + if (str.indexOf(options.interpolationPrefix || o.interpolationPrefix) < 0) return str; + + var prefix = options.interpolationPrefix ? f.regexEscape(options.interpolationPrefix) : o.interpolationPrefixEscaped + , suffix = options.interpolationSuffix ? f.regexEscape(options.interpolationSuffix) : o.interpolationSuffixEscaped + , unEscapingSuffix = 'HTML'+suffix; + + f.each(replacementHash, function(key, value) { + var nextKey = nestedKey ? nestedKey + o.keyseparator + key : key; + if (typeof value === 'object' && value !== null) { + str = applyReplacement(str, value, nextKey, options); + } else { + if (options.escapeInterpolation || o.escapeInterpolation) { + str = str.replace(new RegExp([prefix, nextKey, unEscapingSuffix].join(''), 'g'), value); + str = str.replace(new RegExp([prefix, nextKey, suffix].join(''), 'g'), f.escape(value)); + } else { + str = str.replace(new RegExp([prefix, nextKey, suffix].join(''), 'g'), value); + } + // str = options.escapeInterpolation; + } + }); + return str; + } + + // append it to functions + f.applyReplacement = applyReplacement; + + function applyReuse(translated, options) { + var comma = ','; + var options_open = '{'; + var options_close = '}'; + + var opts = f.extend({}, options); + delete opts.postProcess; + + while (translated.indexOf(o.reusePrefix) != -1) { + replacementCounter++; + if (replacementCounter > o.maxRecursion) { break; } // safety net for too much recursion + var index_of_opening = translated.lastIndexOf(o.reusePrefix); + var index_of_end_of_closing = translated.indexOf(o.reuseSuffix, index_of_opening) + o.reuseSuffix.length; + var token = translated.substring(index_of_opening, index_of_end_of_closing); + var token_without_symbols = token.replace(o.reusePrefix, '').replace(o.reuseSuffix, ''); + + + if (token_without_symbols.indexOf(comma) != -1) { + var index_of_token_end_of_closing = token_without_symbols.indexOf(comma); + 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) { + var index_of_opts_opening = token_without_symbols.indexOf(options_open, index_of_token_end_of_closing); + var index_of_opts_end_of_closing = token_without_symbols.indexOf(options_close, index_of_opts_opening) + options_close.length; + try { + opts = f.extend(opts, JSON.parse(token_without_symbols.substring(index_of_opts_opening, index_of_opts_end_of_closing))); + token_without_symbols = token_without_symbols.substring(0, index_of_token_end_of_closing); + } catch (e) { + } + } + } + + var translated_token = _translate(token_without_symbols, opts); + translated = translated.replace(token, translated_token); + } + return translated; + } + + function hasContext(options) { + return (options.context && (typeof options.context == 'string' || typeof options.context == 'number')); + } + + function needsPlural(options) { + return (options.count !== undefined && typeof options.count != 'string' && options.count !== 1); + } + + function exists(key, options) { + options = options || {}; + + var notFound = _getDefaultValue(key, options) + , found = _find(key, options); + + return found !== undefined || found === notFound; + } + + function translate(key, options) { + if (typeof options === 'undefined') { + options = {}; + } + + if (!initialized) { + f.log('i18next not finished initialization. you might have called t function before loading resources finished.') + return options.defaultValue || ''; + }; + replacementCounter = 0; + return _translate.apply(null, arguments); + } + + function _getDefaultValue(key, options) { + return (options.defaultValue !== undefined) ? options.defaultValue : key; + } + + function _injectSprintfProcessor() { + + var values = []; + + // mh: build array from second argument onwards + for (var i = 1; i < arguments.length; i++) { + values.push(arguments[i]); + } + + return { + postProcess: 'sprintf', + sprintf: values + }; + } + + function _translate(potentialKeys, options) { + if (typeof options !== "undefined" && options !== null && typeof options !== 'object') { + if (o.shortcutFunction === 'sprintf') { + // mh: gettext like sprintf syntax found, automatically create sprintf processor + options = _injectSprintfProcessor.apply(null, arguments); + } else if (o.shortcutFunction === 'defaultValue') { + options = { + defaultValue: options + } + } + } else { + options = options || {}; + } + + if (potentialKeys === undefined || potentialKeys === null) return ''; + + if (typeof potentialKeys == 'string') { + potentialKeys = [potentialKeys]; + } + + var key = potentialKeys[0]; + + if (potentialKeys.length > 1) { + for (var i = 0; i < potentialKeys.length; i++) { + key = potentialKeys[i]; + if (exists(key, options)) { + break; + } + } + } + + var notFound = _getDefaultValue(key, options) + , found = _find(key, options) + , lngs = options.lng ? f.toLanguages(options.lng) : languages + , ns = options.ns || o.ns.defaultNs + , parts; + + // split ns and key + if (key.indexOf(o.nsseparator) > -1) { + parts = key.split(o.nsseparator); + ns = parts[0]; + key = parts[1]; + } + + if (found === undefined && o.sendMissing) { + if (options.lng) { + sync.postMissing(lngs[0], ns, key, notFound, lngs); + } else { + sync.postMissing(o.lng, ns, key, notFound, lngs); + } + } + + var postProcessor = options.postProcess || o.postProcess; + if (found !== undefined && postProcessor) { + if (postProcessors[postProcessor]) { + found = postProcessors[postProcessor](found, key, options); + } + } + + // process notFound if function exists + var splitNotFound = notFound; + if (notFound.indexOf(o.nsseparator) > -1) { + parts = notFound.split(o.nsseparator); + splitNotFound = parts[1]; + } + if (splitNotFound === key && o.parseMissingKey) { + notFound = o.parseMissingKey(notFound); + } + + if (found === undefined) { + notFound = applyReplacement(notFound, options); + notFound = applyReuse(notFound, options); + + if (postProcessor && postProcessors[postProcessor]) { + var val = _getDefaultValue(key, options); + found = postProcessors[postProcessor](val, key, options); + } + } + + return (found !== undefined) ? found : notFound; + } + + function _find(key, options) { + options = options || {}; + + var optionWithoutCount, translated + , notFound = _getDefaultValue(key, options) + , lngs = languages; + + if (!resStore) { return notFound; } // no resStore to translate from + + // CI mode + if (lngs[0].toLowerCase() === 'cimode') return notFound; + + // passed in lng + if (options.lng) { + lngs = f.toLanguages(options.lng); + + if (!resStore[lngs[0]]) { + var oldAsync = o.getAsync; + o.getAsync = false; + + TAPi18next.sync.load(lngs, o, function(err, store) { + f.extend(resStore, store); + o.getAsync = oldAsync; + }); + } + } + + var ns = options.ns || o.ns.defaultNs; + if (key.indexOf(o.nsseparator) > -1) { + var parts = key.split(o.nsseparator); + ns = parts[0]; + key = parts[1]; + } + + if (hasContext(options)) { + optionWithoutCount = f.extend({}, options); + delete optionWithoutCount.context; + optionWithoutCount.defaultValue = o.contextNotFound; + + var contextKey = ns + o.nsseparator + key + '_' + options.context; + + translated = translate(contextKey, optionWithoutCount); + if (translated != o.contextNotFound) { + return applyReplacement(translated, { context: options.context }); // apply replacement for context only + } // else continue translation with original/nonContext key + } + + if (needsPlural(options)) { + optionWithoutCount = f.extend({}, options); + delete optionWithoutCount.count; + optionWithoutCount.defaultValue = o.pluralNotFound; + + var pluralKey = ns + o.nsseparator + key + o.pluralSuffix; + var pluralExtension = pluralExtensions.get(lngs[0], options.count); + if (pluralExtension >= 0) { + pluralKey = pluralKey + '_' + pluralExtension; + } else if (pluralExtension === 1) { + pluralKey = ns + o.nsseparator + key; // singular + } + + translated = translate(pluralKey, optionWithoutCount); + if (translated != o.pluralNotFound) { + return applyReplacement(translated, { + count: options.count, + interpolationPrefix: options.interpolationPrefix, + interpolationSuffix: options.interpolationSuffix + }); // apply replacement for count only + } // else continue translation with original/singular key + } + + var found; + var keys = key.split(o.keyseparator); + for (var i = 0, len = lngs.length; i < len; i++ ) { + if (found !== undefined) break; + + var l = lngs[i]; + + var x = 0; + var value = resStore[l] && resStore[l][ns]; + while (keys[x]) { + value = value && value[keys[x]]; + x++; + } + if (value !== undefined) { + var valueType = Object.prototype.toString.apply(value); + if (typeof value === 'string') { + value = applyReplacement(value, options); + value = applyReuse(value, options); + } else if (valueType === '[object Array]' && !o.returnObjectTrees && !options.returnObjectTrees) { + value = value.join('\n'); + value = applyReplacement(value, options); + value = applyReuse(value, options); + } else if (value === null && o.fallbackOnNull === true) { + value = undefined; + } else if (value !== null) { + if (!o.returnObjectTrees && !options.returnObjectTrees) { + if (o.objectTreeKeyHandler && typeof o.objectTreeKeyHandler == 'function') { + value = o.objectTreeKeyHandler(key, value, l, ns, options); + } else { + value = 'key \'' + ns + ':' + key + ' (' + l + ')\' ' + + 'returned an object instead of string.'; + f.log(value); + } + } else if (valueType !== '[object Number]' && valueType !== '[object Function]' && valueType !== '[object RegExp]') { + var copy = (valueType === '[object Array]') ? [] : {}; // apply child translation on a copy + f.each(value, function(m) { + copy[m] = _translate(ns + o.nsseparator + key + o.keyseparator + m, options); + }); + value = copy; + } + } + + if (typeof value === 'string' && value.trim() === '' && o.fallbackOnEmpty === true) + value = undefined; + + found = value; + } + } + + if (found === undefined && !options.isFallbackLookup && (o.fallbackToDefaultNS === true || (o.fallbackNS && o.fallbackNS.length > 0))) { + // set flag for fallback lookup - avoid recursion + options.isFallbackLookup = true; + + if (o.fallbackNS.length) { + + for (var y = 0, lenY = o.fallbackNS.length; y < lenY; y++) { + found = _find(o.fallbackNS[y] + o.nsseparator + key, options); + + if (found) { + /* compare value without namespace */ + var foundValue = found.indexOf(o.nsseparator) > -1 ? found.split(o.nsseparator)[1] : found + , notFoundValue = notFound.indexOf(o.nsseparator) > -1 ? notFound.split(o.nsseparator)[1] : notFound; + + if (foundValue !== notFoundValue) break; + } + } + } else { + found = _find(key, options); // fallback to default NS + } + } + + return found; + } + function detectLanguage() { + var detectedLng; + + // get from qs + var qsParm = []; + if (typeof window !== 'undefined') { + (function() { + var query = window.location.search.substring(1); + var parms = query.split('&'); + for (var i=0; i 0) { + var key = parms[i].substring(0,pos); + var val = parms[i].substring(pos+1); + qsParm[key] = val; + } + } + })(); + if (qsParm[o.detectLngQS]) { + detectedLng = qsParm[o.detectLngQS]; + } + } + + // get from cookie + if (!detectedLng && typeof document !== 'undefined' && o.useCookie ) { + var c = f.cookie.read(o.cookieName); + if (c) detectedLng = c; + } + + // get from navigator + if (!detectedLng && typeof navigator !== 'undefined') { + detectedLng = (navigator.language) ? navigator.language : navigator.userLanguage; + } + + return detectedLng; + } + var sync = { + + load: function(lngs, options, cb) { + if (options.useLocalStorage) { + sync._loadLocal(lngs, options, function(err, store) { + var missingLngs = []; + for (var i = 0, len = lngs.length; i < len; i++) { + if (!store[lngs[i]]) missingLngs.push(lngs[i]); + } + + if (missingLngs.length > 0) { + sync._fetch(missingLngs, options, function(err, fetched) { + f.extend(store, fetched); + sync._storeLocal(fetched); + + cb(null, store); + }); + } else { + cb(null, store); + } + }); + } else { + sync._fetch(lngs, options, function(err, store){ + cb(null, store); + }); + } + }, + + _loadLocal: function(lngs, options, cb) { + var store = {} + , nowMS = new Date().getTime(); + + if(window.localStorage) { + + var todo = lngs.length; + + f.each(lngs, function(key, lng) { + var local = window.localStorage.getItem('res_' + lng); + + if (local) { + local = JSON.parse(local); + + if (local.i18nStamp && local.i18nStamp + options.localStorageExpirationTime > nowMS) { + store[lng] = local; + } + } + + todo--; // wait for all done befor callback + if (todo === 0) cb(null, store); + }); + } + }, + + _storeLocal: function(store) { + if(window.localStorage) { + for (var m in store) { + store[m].i18nStamp = new Date().getTime(); + window.localStorage.setItem('res_' + m, JSON.stringify(store[m])); + } + } + return; + }, + + _fetch: function(lngs, options, cb) { + var ns = options.ns + , store = {}; + + if (!options.dynamicLoad) { + var todo = ns.namespaces.length * lngs.length + , errors; + + // load each file individual + f.each(ns.namespaces, function(nsIndex, nsValue) { + f.each(lngs, function(lngIndex, lngValue) { + + // Call this once our translation has returned. + var loadComplete = function(err, data) { + if (err) { + errors = errors || []; + errors.push(err); + } + store[lngValue] = store[lngValue] || {}; + store[lngValue][nsValue] = data; + + todo--; // wait for all done befor callback + if (todo === 0) cb(errors, store); + }; + + if(typeof options.customLoad == 'function'){ + // Use the specified custom callback. + options.customLoad(lngValue, nsValue, options, loadComplete); + } else { + //~ // Use our inbuilt sync. + sync._fetchOne(lngValue, nsValue, options, loadComplete); + } + }); + }); + } else { + // Call this once our translation has returned. + var loadComplete = function(err, data) { + cb(null, data); + }; + + if(typeof options.customLoad == 'function'){ + // Use the specified custom callback. + options.customLoad(lngs, ns.namespaces, options, loadComplete); + } else { + var url = applyReplacement(options.resGetPath, { lng: lngs.join('+'), ns: ns.namespaces.join('+') }); + // load all needed stuff once + f.ajax({ + url: url, + success: function(data, status, xhr) { + f.log('loaded: ' + url); + loadComplete(null, data); + }, + error : function(xhr, status, error) { + f.log('failed loading: ' + url); + loadComplete('failed loading resource.json error: ' + error); + }, + dataType: "json", + async : options.getAsync + }); + } + } + }, + + _fetchOne: function(lng, ns, options, done) { + var url = applyReplacement(options.resGetPath, { lng: lng, ns: ns }); + f.ajax({ + url: url, + success: function(data, status, xhr) { + f.log('loaded: ' + url); + done(null, data); + }, + error : function(xhr, status, error) { + if ((status && status == 200) || (xhr && xhr.status && xhr.status == 200)) { + // file loaded but invalid json, stop waste time ! + f.log('There is a typo in: ' + url); + } else if ((status && status == 404) || (xhr && xhr.status && xhr.status == 404)) { + f.log('Does not exist: ' + url); + } else { + var theStatus = status ? status : ((xhr && xhr.status) ? xhr.status : null); + f.log(theStatus + ' when loading ' + url); + } + + done(error, {}); + }, + dataType: "json", + async : options.getAsync + }); + }, + + postMissing: function(lng, ns, key, defaultValue, lngs) { + var payload = {}; + payload[key] = defaultValue; + + var urls = []; + + if (o.sendMissingTo === 'fallback' && o.fallbackLng[0] !== false) { + for (var i = 0; i < o.fallbackLng.length; i++) { + urls.push({lng: o.fallbackLng[i], url: applyReplacement(o.resPostPath, { lng: o.fallbackLng[i], ns: ns })}); + } + } else if (o.sendMissingTo === 'current' || (o.sendMissingTo === 'fallback' && o.fallbackLng[0] === false) ) { + urls.push({lng: lng, url: applyReplacement(o.resPostPath, { lng: lng, ns: ns })}); + } else if (o.sendMissingTo === 'all') { + for (var i = 0, l = lngs.length; i < l; i++) { + urls.push({lng: lngs[i], url: applyReplacement(o.resPostPath, { lng: lngs[i], ns: ns })}); + } + } + + for (var y = 0, len = urls.length; y < len; y++) { + var item = urls[y]; + f.ajax({ + url: item.url, + type: o.sendType, + data: payload, + success: function(data, status, xhr) { + f.log('posted missing key \'' + key + '\' to: ' + item.url); + + // add key to resStore + var keys = key.split('.'); + var x = 0; + var value = resStore[item.lng][ns]; + while (keys[x]) { + if (x === keys.length - 1) { + value = value[keys[x]] = defaultValue; + } else { + value = value[keys[x]] = value[keys[x]] || {}; + } + x++; + } + }, + error : function(xhr, status, error) { + f.log('failed posting missing key \'' + key + '\' to: ' + item.url); + }, + dataType: "json", + async : o.postAsync + }); + } + } + }; + // definition http://translate.sourceforge.net/wiki/l10n/pluralforms + var pluralExtensions = { + + rules: { + "ach": { + "name": "Acholi", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "af": { + "name": "Afrikaans", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "ak": { + "name": "Akan", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "am": { + "name": "Amharic", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "an": { + "name": "Aragonese", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "ar": { + "name": "Arabic", + "numbers": [ + 0, + 1, + 2, + 3, + 11, + 100 + ], + "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); } + }, + "arn": { + "name": "Mapudungun", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "ast": { + "name": "Asturian", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "ay": { + "name": "Aymar\u00e1", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "az": { + "name": "Azerbaijani", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "be": { + "name": "Belarusian", + "numbers": [ + 1, + 2, + 5 + ], + "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); } + }, + "bg": { + "name": "Bulgarian", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "bn": { + "name": "Bengali", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "bo": { + "name": "Tibetan", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "br": { + "name": "Breton", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "bs": { + "name": "Bosnian", + "numbers": [ + 1, + 2, + 5 + ], + "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); } + }, + "ca": { + "name": "Catalan", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "cgg": { + "name": "Chiga", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "cs": { + "name": "Czech", + "numbers": [ + 1, + 2, + 5 + ], + "plurals": function(n) { return Number((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2); } + }, + "csb": { + "name": "Kashubian", + "numbers": [ + 1, + 2, + 5 + ], + "plurals": function(n) { return Number(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); } + }, + "cy": { + "name": "Welsh", + "numbers": [ + 1, + 2, + 3, + 8 + ], + "plurals": function(n) { return Number((n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3); } + }, + "da": { + "name": "Danish", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "de": { + "name": "German", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "dz": { + "name": "Dzongkha", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "el": { + "name": "Greek", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "en": { + "name": "English", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "eo": { + "name": "Esperanto", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "es": { + "name": "Spanish", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "es_ar": { + "name": "Argentinean Spanish", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "et": { + "name": "Estonian", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "eu": { + "name": "Basque", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "fa": { + "name": "Persian", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "fi": { + "name": "Finnish", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "fil": { + "name": "Filipino", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "fo": { + "name": "Faroese", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "fr": { + "name": "French", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "fur": { + "name": "Friulian", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "fy": { + "name": "Frisian", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "ga": { + "name": "Irish", + "numbers": [ + 1, + 2, + 3, + 7, + 11 + ], + "plurals": function(n) { return Number(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4) ;} + }, + "gd": { + "name": "Scottish Gaelic", + "numbers": [ + 1, + 2, + 3, + 20 + ], + "plurals": function(n) { return Number((n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3); } + }, + "gl": { + "name": "Galician", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "gu": { + "name": "Gujarati", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "gun": { + "name": "Gun", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "ha": { + "name": "Hausa", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "he": { + "name": "Hebrew", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "hi": { + "name": "Hindi", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "hr": { + "name": "Croatian", + "numbers": [ + 1, + 2, + 5 + ], + "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); } + }, + "hu": { + "name": "Hungarian", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "hy": { + "name": "Armenian", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "ia": { + "name": "Interlingua", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "id": { + "name": "Indonesian", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "is": { + "name": "Icelandic", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n%10!=1 || n%100==11); } + }, + "it": { + "name": "Italian", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "ja": { + "name": "Japanese", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "jbo": { + "name": "Lojban", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "jv": { + "name": "Javanese", + "numbers": [ + 0, + 1 + ], + "plurals": function(n) { return Number(n !== 0); } + }, + "ka": { + "name": "Georgian", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "kk": { + "name": "Kazakh", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "km": { + "name": "Khmer", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "kn": { + "name": "Kannada", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "ko": { + "name": "Korean", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "ku": { + "name": "Kurdish", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "kw": { + "name": "Cornish", + "numbers": [ + 1, + 2, + 3, + 4 + ], + "plurals": function(n) { return Number((n==1) ? 0 : (n==2) ? 1 : (n == 3) ? 2 : 3); } + }, + "ky": { + "name": "Kyrgyz", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "lb": { + "name": "Letzeburgesch", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "ln": { + "name": "Lingala", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "lo": { + "name": "Lao", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "lt": { + "name": "Lithuanian", + "numbers": [ + 1, + 2, + 10 + ], + "plurals": function(n) { return Number(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2); } + }, + "lv": { + "name": "Latvian", + "numbers": [ + 1, + 2, + 0 + ], + "plurals": function(n) { return Number(n%10==1 && n%100!=11 ? 0 : n !== 0 ? 1 : 2); } + }, + "mai": { + "name": "Maithili", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "mfe": { + "name": "Mauritian Creole", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "mg": { + "name": "Malagasy", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "mi": { + "name": "Maori", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "mk": { + "name": "Macedonian", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n==1 || n%10==1 ? 0 : 1); } + }, + "ml": { + "name": "Malayalam", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "mn": { + "name": "Mongolian", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "mnk": { + "name": "Mandinka", + "numbers": [ + 0, + 1, + 2 + ], + "plurals": function(n) { return Number(n == 0 ? 0 : n==1 ? 1 : 2); } + }, + "mr": { + "name": "Marathi", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "ms": { + "name": "Malay", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "mt": { + "name": "Maltese", + "numbers": [ + 1, + 2, + 11, + 20 + ], + "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); } + }, + "nah": { + "name": "Nahuatl", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "nap": { + "name": "Neapolitan", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "nb": { + "name": "Norwegian Bokmal", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "ne": { + "name": "Nepali", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "nl": { + "name": "Dutch", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "nn": { + "name": "Norwegian Nynorsk", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "no": { + "name": "Norwegian", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "nso": { + "name": "Northern Sotho", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "oc": { + "name": "Occitan", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "or": { + "name": "Oriya", + "numbers": [ + 2, + 1 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "pa": { + "name": "Punjabi", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "pap": { + "name": "Papiamento", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "pl": { + "name": "Polish", + "numbers": [ + 1, + 2, + 5 + ], + "plurals": function(n) { return Number(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); } + }, + "pms": { + "name": "Piemontese", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "ps": { + "name": "Pashto", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "pt": { + "name": "Portuguese", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "pt_br": { + "name": "Brazilian Portuguese", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "rm": { + "name": "Romansh", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "ro": { + "name": "Romanian", + "numbers": [ + 1, + 2, + 20 + ], + "plurals": function(n) { return Number(n==1 ? 0 : (n===0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2); } + }, + "ru": { + "name": "Russian", + "numbers": [ + 1, + 2, + 5 + ], + "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); } + }, + "sah": { + "name": "Yakut", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "sco": { + "name": "Scots", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "se": { + "name": "Northern Sami", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "si": { + "name": "Sinhala", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "sk": { + "name": "Slovak", + "numbers": [ + 1, + 2, + 5 + ], + "plurals": function(n) { return Number((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2); } + }, + "sl": { + "name": "Slovenian", + "numbers": [ + 5, + 1, + 2, + 3 + ], + "plurals": function(n) { return Number(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0); } + }, + "so": { + "name": "Somali", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "son": { + "name": "Songhay", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "sq": { + "name": "Albanian", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "sr": { + "name": "Serbian", + "numbers": [ + 1, + 2, + 5 + ], + "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); } + }, + "su": { + "name": "Sundanese", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "sv": { + "name": "Swedish", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "sw": { + "name": "Swahili", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "ta": { + "name": "Tamil", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "te": { + "name": "Telugu", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "tg": { + "name": "Tajik", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "th": { + "name": "Thai", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "ti": { + "name": "Tigrinya", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "tk": { + "name": "Turkmen", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "tr": { + "name": "Turkish", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "tt": { + "name": "Tatar", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "ug": { + "name": "Uyghur", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "uk": { + "name": "Ukrainian", + "numbers": [ + 1, + 2, + 5 + ], + "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); } + }, + "ur": { + "name": "Urdu", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "uz": { + "name": "Uzbek", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "vi": { + "name": "Vietnamese", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "wa": { + "name": "Walloon", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n > 1); } + }, + "wo": { + "name": "Wolof", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + }, + "yo": { + "name": "Yoruba", + "numbers": [ + 1, + 2 + ], + "plurals": function(n) { return Number(n != 1); } + }, + "zh": { + "name": "Chinese", + "numbers": [ + 1 + ], + "plurals": function(n) { return 0; } + } + }, + + // for demonstration only sl and ar is added but you can add your own pluralExtensions + addRule: function(lng, obj) { + pluralExtensions.rules[lng] = obj; + }, + + setCurrentLng: function(lng) { + if (!pluralExtensions.currentRule || pluralExtensions.currentRule.lng !== lng) { + var parts = lng.split('-'); + + pluralExtensions.currentRule = { + lng: lng, + rule: pluralExtensions.rules[parts[0]] + }; + } + }, + + get: function(lng, count) { + var parts = lng.split('-'); + + function getResult(l, c) { + var ext; + if (pluralExtensions.currentRule && pluralExtensions.currentRule.lng === lng) { + ext = pluralExtensions.currentRule.rule; + } else { + ext = pluralExtensions.rules[l]; + } + if (ext) { + var i = ext.plurals(c); + var number = ext.numbers[i]; + if (ext.numbers.length === 2 && ext.numbers[0] === 1) { + if (number === 2) { + number = -1; // regular plural + } else if (number === 1) { + number = 1; // singular + } + }//console.log(count + '-' + number); + return number; + } else { + return c === 1 ? '1' : '-1'; + } + } + + return getResult(parts[0], count); + } + + }; + var postProcessors = {}; + var addPostProcessor = function(name, fc) { + postProcessors[name] = fc; + }; + // sprintf support + var sprintf = (function() { + function get_type(variable) { + return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase(); + } + function str_repeat(input, multiplier) { + for (var output = []; multiplier > 0; output[--multiplier] = input) {/* do nothing */} + return output.join(''); + } + + var str_format = function() { + if (!str_format.cache.hasOwnProperty(arguments[0])) { + str_format.cache[arguments[0]] = str_format.parse(arguments[0]); + } + return str_format.format.call(null, str_format.cache[arguments[0]], arguments); + }; + + str_format.format = function(parse_tree, argv) { + var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length; + for (i = 0; i < tree_length; i++) { + node_type = get_type(parse_tree[i]); + if (node_type === 'string') { + output.push(parse_tree[i]); + } + else if (node_type === 'array') { + match = parse_tree[i]; // convenience purposes only + if (match[2]) { // keyword argument + arg = argv[cursor]; + for (k = 0; k < match[2].length; k++) { + if (!arg.hasOwnProperty(match[2][k])) { + throw(sprintf('[sprintf] property "%s" does not exist', match[2][k])); + } + arg = arg[match[2][k]]; + } + } + else if (match[1]) { // positional argument (explicit) + arg = argv[match[1]]; + } + else { // positional argument (implicit) + arg = argv[cursor++]; + } + + if (/[^s]/.test(match[8]) && (get_type(arg) != 'number')) { + throw(sprintf('[sprintf] expecting number but found %s', get_type(arg))); + } + switch (match[8]) { + case 'b': arg = arg.toString(2); break; + case 'c': arg = String.fromCharCode(arg); break; + case 'd': arg = parseInt(arg, 10); break; + case 'e': arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential(); break; + case 'f': arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg); break; + case 'o': arg = arg.toString(8); break; + case 's': arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg); break; + case 'u': arg = Math.abs(arg); break; + case 'x': arg = arg.toString(16); break; + case 'X': arg = arg.toString(16).toUpperCase(); break; + } + arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+'+ arg : arg); + pad_character = match[4] ? match[4] == '0' ? '0' : match[4].charAt(1) : ' '; + pad_length = match[6] - String(arg).length; + pad = match[6] ? str_repeat(pad_character, pad_length) : ''; + output.push(match[5] ? arg + pad : pad + arg); + } + } + return output.join(''); + }; + + str_format.cache = {}; + + str_format.parse = function(fmt) { + var _fmt = fmt, match = [], parse_tree = [], arg_names = 0; + while (_fmt) { + if ((match = /^[^\x25]+/.exec(_fmt)) !== null) { + parse_tree.push(match[0]); + } + else if ((match = /^\x25{2}/.exec(_fmt)) !== null) { + parse_tree.push('%'); + } + else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) { + if (match[2]) { + arg_names |= 1; + var field_list = [], replacement_field = match[2], field_match = []; + if ((field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) { + field_list.push(field_match[1]); + while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') { + if ((field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) { + field_list.push(field_match[1]); + } + else if ((field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) { + field_list.push(field_match[1]); + } + else { + throw('[sprintf] huh?'); + } + } + } + else { + throw('[sprintf] huh?'); + } + match[2] = field_list; + } + else { + arg_names |= 2; + } + if (arg_names === 3) { + throw('[sprintf] mixing positional and named placeholders is not (yet) supported'); + } + parse_tree.push(match); + } + else { + throw('[sprintf] huh?'); + } + _fmt = _fmt.substring(match[0].length); + } + return parse_tree; + }; + + return str_format; + })(); + + var vsprintf = function(fmt, argv) { + argv.unshift(fmt); + return sprintf.apply(null, argv); + }; + + addPostProcessor("sprintf", function(val, key, opts) { + if (!opts.sprintf) return val; + + if (Object.prototype.toString.apply(opts.sprintf) === '[object Array]') { + return vsprintf(val, opts.sprintf); + } else if (typeof opts.sprintf === 'object') { + return sprintf(val, opts.sprintf); + } + + return val; + }); + // public api interface + TAPi18next.init = init; + TAPi18next.setLng = setLng; + TAPi18next.preload = preload; + TAPi18next.addResourceBundle = addResourceBundle; + TAPi18next.removeResourceBundle = removeResourceBundle; + TAPi18next.loadNamespace = loadNamespace; + TAPi18next.loadNamespaces = loadNamespaces; + TAPi18next.setDefaultNamespace = setDefaultNamespace; + TAPi18next.t = translate; + TAPi18next.translate = translate; + TAPi18next.exists = exists; + TAPi18next.detectLanguage = f.detectLanguage; + TAPi18next.pluralExtensions = pluralExtensions; + TAPi18next.sync = sync; + TAPi18next.functions = f; + TAPi18next.lng = lng; + TAPi18next.addPostProcessor = addPostProcessor; + TAPi18next.options = o; +})(); diff --git a/apps/meteor/packages/rocketchat-tap-i18n/package.js b/apps/meteor/packages/rocketchat-tap-i18n/package.js new file mode 100644 index 0000000000000..b99a68e8e004e --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/package.js @@ -0,0 +1,68 @@ +Package.describe({ + name: 'rocketchat:tap-i18n', + summary: 'A comprehensive internationalization solution for Meteor', + version: '3.0.0', + git: 'https://github.com/TAPevents/tap-i18n', +}); + +const both = ['server', 'client']; +const server = 'server'; +const client = 'client'; + +Package.onUse(function (api) { + api.versionsFrom('2.5'); + + api.use('coffeescript', both); + api.use('underscore', both); + api.use('ecmascript', both); + api.use('meteor', both); + api.use('webapp', both); + api.use('tracker', both); + api.use('session', client); + api.use('jquery@3.0.0', client); + api.use('templating@1.4.1', client); + + api.use('raix:eventemitter@1.0.0', both); + + // load and init TAPi18next + api.addFiles('lib/tap_i18next/tap_i18next-1.7.3.js', both); + api.export('TAPi18next'); + + api.mainModule('client.js', client); + api.mainModule('server.js', server); + + api.export('TAPi18n'); +}); + +Npm.depends({ + 'connect': '2.30.2', + 'connect-route': '0.1.5', +}); + +Package.registerBuildPlugin({ + name: 'tap-i18n-compiler', + use: ['coffeescript', 'underscore', 'mdg:validation-error@0.5.1', 'aldeed:simple-schema@1.5.4', 'check@1.3.1', 'templating@1.4.1'], + npmDependencies: { + yamljs: '0.3.0', + }, + sources: [ + 'lib/globals.js', + + 'lib/plugin/etc/language_names.js', + + 'lib/plugin/compiler_configuration.coffee', + + 'lib/plugin/helpers/helpers.coffee', + 'lib/plugin/helpers/load_json.coffee', + 'lib/plugin/helpers/load_yml.coffee', + 'lib/plugin/helpers/compile_step_helpers.coffee', + + 'lib/plugin/compilers/share.coffee', + 'lib/plugin/compilers/i18n.coffee', + 'lib/plugin/compilers/project-tap.i18n.coffee', + 'lib/plugin/compilers/package-tap.i18n.coffee', + 'lib/plugin/compilers/i18n.generic_compiler.coffee', + 'lib/plugin/compilers/i18n.json.coffee', + 'lib/plugin/compilers/i18n.yml.coffee', + ], +}); diff --git a/apps/meteor/packages/rocketchat-tap-i18n/server.js b/apps/meteor/packages/rocketchat-tap-i18n/server.js new file mode 100644 index 0000000000000..ca94c805bb5f8 --- /dev/null +++ b/apps/meteor/packages/rocketchat-tap-i18n/server.js @@ -0,0 +1,3 @@ +import { TAPi18nServer } from './lib/tap_i18n/tap_i18n-server.coffee'; + +export const TAPi18n = new TAPi18nServer(); From 19762936d79a38d6aaaa74ed307dccce6ac637fb Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 2 May 2023 09:50:28 -0600 Subject: [PATCH 04/70] replay release-3.0 branch --- RELEASE-3.0.md | 7 + apps/meteor/.meteor/packages | 1 + .../app/2fa/client/overrideMeteorCall.ts | 4 +- .../authentication/server/startup/index.js | 6 +- .../app/file-upload/server/lib/proxy.ts | 181 +++++++++--------- .../client/models/CachedCollection.ts | 20 +- apps/meteor/app/ui-master/server/index.js | 6 +- .../ui-utils/client/lib/RoomHistoryManager.ts | 2 +- .../ui/client/lib/codeMirror/codeMirror.ts | 4 +- apps/meteor/client/lib/userData.ts | 18 +- apps/meteor/client/startup/startup.ts | 2 +- .../settings/inputs/CodeMirror/CodeMirror.tsx | 2 +- .../rocketchat-mongo-config/package.js | 2 +- .../rocketchat-mongo-config/server/index.js | 56 +++--- .../server/lib/dataExport/sendViaEmail.ts | 2 +- apps/meteor/server/lib/getMomentLocale.ts | 2 +- apps/meteor/server/routes/i18n.ts | 14 +- apps/meteor/server/services/meteor/service.ts | 19 +- apps/meteor/server/startup/initialData.js | 2 +- apps/meteor/server/ufs/index.ts | 61 +----- apps/meteor/server/ufs/ufs-gridfs.ts | 2 +- apps/meteor/server/ufs/ufs-local.ts | 2 +- apps/meteor/server/ufs/ufs-methods.ts | 2 +- apps/meteor/server/ufs/ufs-server.ts | 2 +- apps/meteor/server/ufs/ufs.ts | 51 +++++ .../src/lib/asyncLocalStorage.ts | 6 +- 26 files changed, 242 insertions(+), 234 deletions(-) create mode 100644 RELEASE-3.0.md create mode 100644 apps/meteor/server/ufs/ufs.ts diff --git a/RELEASE-3.0.md b/RELEASE-3.0.md new file mode 100644 index 0000000000000..59db0ebcef5a3 --- /dev/null +++ b/RELEASE-3.0.md @@ -0,0 +1,7 @@ +# Instructions to run the meteor development branch of 3.0 version + +1. Clone the meteor project locally following the instructions at https://github.com/meteor/meteor/blob/devel/DEVELOPMENT.md +2. Change to the branch `release-3.0` +3. Add a alias for the cloned meteor project named `md` (example `alias md=/Users/rocket.chat/Projects/meteor/meteor`). Instructions on link above as well. +4. Checkout the Rocket.Chat's branch named `meteor-3.0` +5. Run the project `NODE_OPTIONS="--trace-warnings --unhandled-rejections=warn" DISABLE_FIBERS=1 IGNORE_ASYNC_PLUGIN=1 md` diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index 5ff7af0271aca..e17b6eca2cbee 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -71,3 +71,4 @@ rocketchat:oauth2-server rocketchat:i18n facts-base@1.0.1 standard-minifier-css@1.9.0 +jquery \ No newline at end of file diff --git a/apps/meteor/app/2fa/client/overrideMeteorCall.ts b/apps/meteor/app/2fa/client/overrideMeteorCall.ts index a814897263902..3741ab1e623ac 100644 --- a/apps/meteor/app/2fa/client/overrideMeteorCall.ts +++ b/apps/meteor/app/2fa/client/overrideMeteorCall.ts @@ -39,7 +39,7 @@ const callAsyncWithTotp = (methodName: string, args: unknown[]) => async (twoFactorCode: string, twoFactorMethod: string): Promise => { try { - const result = await callAsync(methodName, ...args, { twoFactorCode, twoFactorMethod }); + const result = await callAsync(methodName, { returnServerPromise: true }, ...args, { twoFactorCode, twoFactorMethod }); return result; } catch (error: unknown) { @@ -59,7 +59,7 @@ Meteor.call = function (methodName: string, ...args: unknown[]): unknown { }; Meteor.callAsync = async function _callAsyncWithTotp(methodName: string, ...args: unknown[]): Promise { - const promise = callAsync(methodName, ...args); + const promise = callAsync(methodName, { returnServerPromise: true }, ...args); return process2faAsyncReturn({ promise, diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index 4947d4bec3f3b..7474a6f702310 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -228,7 +228,7 @@ const onCreateUserAsync = async function (options, user = {}) { Accounts.onCreateUser(function (...args) { // Depends on meteor support for Async - return Promise.await(onCreateUserAsync.call(this, ...args)); + return onCreateUserAsync.call(this, ...args); }); const { insertUserDoc } = Accounts; @@ -263,7 +263,7 @@ const insertUserDocAsync = async function (options, user) { }; } - const _id = insertUserDoc.call(Accounts, options, user); + const _id = await insertUserDoc.call(Accounts, options, user); user = await Users.findOne({ _id, @@ -313,7 +313,7 @@ const insertUserDocAsync = async function (options, user) { Accounts.insertUserDoc = function (...args) { // Depends on meteor support for Async - return Promise.await(insertUserDocAsync.call(this, ...args)); + return insertUserDocAsync.call(this, ...args); }; const validateLoginAttemptAsync = async function (login) { diff --git a/apps/meteor/app/file-upload/server/lib/proxy.ts b/apps/meteor/app/file-upload/server/lib/proxy.ts index 16a5855ddf6a7..90843c687627d 100644 --- a/apps/meteor/app/file-upload/server/lib/proxy.ts +++ b/apps/meteor/app/file-upload/server/lib/proxy.ts @@ -13,92 +13,99 @@ import { isDocker } from '../../../utils/server'; const logger = new Logger('UploadProxy'); -WebApp.connectHandlers.stack.unshift({ - route: '', - // eslint-disable-next-line @typescript-eslint/no-misused-promises - async handle(req: createServer.IncomingMessage, res: http.ServerResponse, next: NextFunction) { - // Quick check to see if request should be catch - if (!req.url?.includes(`/${UploadFS.config.storesPath}/`)) { - return next(); - } - - logger.debug({ msg: 'Upload URL:', url: req.url }); - - if (req.method !== 'POST') { - return next(); - } - - // Remove store path - const parsedUrl = URL.parse(req.url); - const path = parsedUrl.pathname?.substr(UploadFS.config.storesPath.length + 1) || ''; - - // Get store - const regExp = new RegExp('^/([^/?]+)/([^/?]+)$'); - const match = regExp.exec(path); - - // Request is not valid - if (match === null) { - res.writeHead(400); - res.end(); - return; - } - - // Get store - const store = UploadFS.getStore(match[1]); - if (!store) { - res.writeHead(404); - res.end(); - return; - } - - // Get file - const fileId = match[2]; - const file = await store.getCollection().findOne({ _id: fileId }); - if (!file) { - res.writeHead(404); - res.end(); - return; - } - - if (!file.instanceId || file.instanceId === InstanceStatus.id()) { - logger.debug('Correct instance'); - return next(); - } - - // Proxy to other instance - const instance = await InstanceStatusModel.findOneById(file.instanceId); - - if (instance == null) { - res.writeHead(404); - res.end(); - return; - } - - if (instance.extraInformation.host === process.env.INSTANCE_IP && isDocker() === false) { - instance.extraInformation.host = 'localhost'; - } - - logger.debug(`Wrong instance, proxing to ${instance.extraInformation.host}:${instance.extraInformation.port}`); - - const options = { - hostname: instance.extraInformation.host, - port: instance.extraInformation.port, - path: req.originalUrl, - method: 'POST', - }; - - logger.warn( - 'UFS proxy middleware is deprecated as this upload method is not being used by Web/Mobile Clients. See this: https://docs.rocket.chat/api/rest-api/methods/rooms/upload', - ); - // eslint-disable-next-line @typescript-eslint/naming-convention - const proxy = http.request(options, function (proxy_res) { - proxy_res.pipe(res, { - end: true, - }); - }); - - req.pipe(proxy, { +// @ts-expect-error - l +const dummyRouter = WebApp.connectHandlers._router; + +// Create a dummy route +dummyRouter.route('*'); +// fetch the newly created "layer" +const stackedRoute = dummyRouter.stack.pop(); +stackedRoute.handle = async (req: createServer.IncomingMessage, res: http.ServerResponse, next: NextFunction) => { + // Quick check to see if request should be catch + if (!req.url?.includes(`/${UploadFS.config.storesPath}/`)) { + return next(); + } + + logger.debug({ msg: 'Upload URL:', url: req.url }); + + if (req.method !== 'POST') { + return next(); + } + + // Remove store path + const parsedUrl = URL.parse(req.url); + const path = parsedUrl.pathname?.substr(UploadFS.config.storesPath.length + 1) || ''; + + // Get store + const regExp = new RegExp('^/([^/?]+)/([^/?]+)$'); + const match = regExp.exec(path); + + // Request is not valid + if (match === null) { + res.writeHead(400); + res.end(); + return; + } + + // Get store + const store = UploadFS.getStore(match[1]); + if (!store) { + res.writeHead(404); + res.end(); + return; + } + + // Get file + const fileId = match[2]; + const file = await store.getCollection().findOne({ _id: fileId }); + if (!file) { + res.writeHead(404); + res.end(); + return; + } + + if (!file.instanceId || file.instanceId === InstanceStatus.id()) { + logger.debug('Correct instance'); + return next(); + } + + // Proxy to other instance + const instance = await InstanceStatusModel.findOneById(file.instanceId); + + if (instance == null) { + res.writeHead(404); + res.end(); + return; + } + + if (instance.extraInformation.host === process.env.INSTANCE_IP && isDocker() === false) { + instance.extraInformation.host = 'localhost'; + } + + logger.debug(`Wrong instance, proxing to ${instance.extraInformation.host}:${instance.extraInformation.port}`); + + const options = { + hostname: instance.extraInformation.host, + port: instance.extraInformation.port, + path: req.originalUrl, + method: 'POST', + }; + + logger.warn( + 'UFS proxy middleware is deprecated as this upload method is not being used by Web/Mobile Clients. See this: https://docs.rocket.chat/api/rest-api/methods/rooms/upload', + ); + // eslint-disable-next-line @typescript-eslint/naming-convention + const proxy = http.request(options, function (proxy_res) { + proxy_res.pipe(res, { end: true, }); - }, -}); + }); + + req.pipe(proxy, { + end: true, + }); +}; + +// Move the layer to the top :) +// @ts-expect-error - l +WebApp.connectHandlers._router.stack.unshift(stackedRoute); diff --git a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts index 6e5720131a9fb..49b0b0c489a88 100644 --- a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts +++ b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.ts @@ -162,20 +162,20 @@ export class CachedCollection extends Emitter<{ changed const data = await this.callLoad(); this.log(`${data.length} records loaded from server`); - data.forEach((record) => { + for await (const record of data) { const newRecord = this.handleLoadFromServer(record); if (!hasId(newRecord)) { return; } const { _id } = newRecord; - this.collection.upsert({ _id } as Mongo.Selector, newRecord); + await this.collection.upsertAsync({ _id } as Mongo.Selector, newRecord); this.emit('changed', newRecord as any); // TODO: investigate why this is needed if (hasUpdatedAt(newRecord) && newRecord._updatedAt > this.updatedAt) { this.updatedAt = newRecord._updatedAt; } - }); + } this.updatedAt = this.updatedAt === lastTime ? startTime : this.updatedAt; } @@ -230,10 +230,10 @@ export class CachedCollection extends Emitter<{ changed } if (action === 'removed') { - this.collection.remove(newRecord._id); + await this.collection.removeAsync(newRecord._id); } else { const { _id } = newRecord; - this.collection.upsert({ _id } as Mongo.Selector, newRecord); + await this.collection.upsertAsync({ _id } as Mongo.Selector, newRecord); } await this.save(); }); @@ -275,9 +275,9 @@ export class CachedCollection extends Emitter<{ changed const actionTime = hasUpdatedAt(newRecord) ? newRecord._updatedAt : startTime; changes.push({ - action: () => { + action: async () => { const { _id } = newRecord; - this.collection.upsert({ _id } as Mongo.Selector, newRecord); + await this.collection.upsertAsync({ _id } as Mongo.Selector, newRecord); if (actionTime > this.updatedAt) { this.updatedAt = actionTime; } @@ -300,9 +300,9 @@ export class CachedCollection extends Emitter<{ changed const actionTime = newRecord._deletedAt; changes.push({ - action: () => { + action: async () => { const { _id } = newRecord; - this.collection.remove({ _id } as Mongo.Selector); + await this.collection.removeAsync({ _id } as Mongo.Selector); if (actionTime > this.updatedAt) { this.updatedAt = actionTime; } @@ -316,7 +316,7 @@ export class CachedCollection extends Emitter<{ changed changes .sort((a, b) => a.timestamp - b.timestamp) .forEach((c) => { - c.action(); + void c.action(); }); this.updatedAt = this.updatedAt === lastTime ? startTime : this.updatedAt; diff --git a/apps/meteor/app/ui-master/server/index.js b/apps/meteor/app/ui-master/server/index.js index 84e402045c2bf..ffa0f894c6a06 100644 --- a/apps/meteor/app/ui-master/server/index.js +++ b/apps/meteor/app/ui-master/server/index.js @@ -136,14 +136,14 @@ const renderDynamicCssList = withDebouncing({ wait: 500 })(async () => { return `--${_id.replace('theme-color-', '')}: ${value};`; }) .join('\n'); - injectIntoBody('dynamic-variables', ``); + await injectIntoBody('dynamic-variables', ``); }); renderDynamicCssList(); settings.watchByRegex(/theme-color-rc/i, renderDynamicCssList); -injectIntoBody( +await injectIntoBody( 'react-root', `