diff --git a/locales/en/email.json b/locales/en/email.json new file mode 100644 index 00000000..b86690a3 --- /dev/null +++ b/locales/en/email.json @@ -0,0 +1,15 @@ +{ + "template": { + "subject": "Your report is ready", + "btnText": "Download Report", + "fileName": "File name", + "unauth": "Your file could not be completed, please try again", + "readyForDownload": "The report you request is ready for download", + "ifDidNotInitAction": "If you did not initiate this action and you suspect that your account may be compromised, please", + "freezeAccount": "freeze your account", + "contactSupport": "and contact support", + "youCan": "You can", + "download": "download", + "pgpSignature": "a PGP digital signature file" + } +} diff --git a/locales/en/pdf.json b/locales/en/pdf.json new file mode 100644 index 00000000..37fdc566 --- /dev/null +++ b/locales/en/pdf.json @@ -0,0 +1,16 @@ +{ + "template": { + "title": "Report", + "statementDetails": "Statement Details", + "statementDate": "Statement date", + "snapshotAt": "Snapshot at", + "period": "Period", + "username": "Username", + "email": "Email", + "errorMessage": "Your file could not be completed, please try again", + "reportGenAt": "Report generated at", + "copyright": "Copyright © 2013-2024 iFinex Inc. All rights reserved.", + "page": "Page", + "from": "from" + } +} diff --git a/locales/es-EM/email.json b/locales/es-EM/email.json new file mode 100644 index 00000000..01e10af2 --- /dev/null +++ b/locales/es-EM/email.json @@ -0,0 +1,15 @@ +{ + "template": { + "subject": "Tu reporte esta listo", + "btnText": "Descargar el Informe", + "fileName": "Nombre del archivo", + "unauth": "Tu archivo no se pudo completar, intente de nuevo por favor.", + "readyForDownload": "Tu reporte esta listo para ser descargado", + "ifDidNotInitAction": "Si no realizaste esta acción y sospechas que tu cuenta puede estar comprometida, por favor", + "freezeAccount": "congela tu cuenta", + "contactSupport": "y contacta soporte", + "youCan": "Puedes", + "download": "descargar", + "pgpSignature": "un archivo con firma digital PGP" + } +} diff --git a/locales/es-EM/pdf.json b/locales/es-EM/pdf.json new file mode 100644 index 00000000..7764d527 --- /dev/null +++ b/locales/es-EM/pdf.json @@ -0,0 +1,16 @@ +{ + "template": { + "title": "Reporte", + "statementDetails": "Detalles del Estado", + "statementDate": "Fecha del Estado", + "snapshotAt": "Captura al", + "period": "Periodo", + "username": "Nombre de Usuario", + "email": "Correo", + "errorMessage": "Tu archivo no pudo ser completado, por favor inténtalo de nuevo", + "reportGenAt": "Reporte generado al", + "copyright": "Copyright © 2013-2024 iFinex Inc. Todos los derechos reservados.", + "page": "Página", + "from": "de" + } +} diff --git a/locales/pt-BR/email.json b/locales/pt-BR/email.json new file mode 100644 index 00000000..383c132f --- /dev/null +++ b/locales/pt-BR/email.json @@ -0,0 +1,15 @@ +{ + "template": { + "subject": "Seu relatório está pronto", + "btnText": "Baixar Relatório", + "fileName": "Nome do arquivo", + "unauth": "Não foi possível concluir seu arquivo, tente novamente", + "readyForDownload": "O relatório solicitado está pronto para download", + "ifDidNotInitAction": "Se você não iniciou esta ação e suspeita que sua conta pode estar comprometida", + "freezeAccount": "por favor, congele sua conta", + "contactSupport": "e entre em contato com o suporte", + "youCan": "Você pode", + "download": "baixar", + "pgpSignature": "um arquivo de assinatura digital PGP" + } +} diff --git a/locales/pt-BR/pdf.json b/locales/pt-BR/pdf.json new file mode 100644 index 00000000..f33b377e --- /dev/null +++ b/locales/pt-BR/pdf.json @@ -0,0 +1,16 @@ +{ + "template": { + "title": "Informes", + "statementDetails": "Detalhes da declaração", + "statementDate": "Data da declaração", + "snapshotAt": "Captura de", + "period": "Período", + "username": "Nome de Usuário", + "email": "e-mail", + "errorMessage": "Seu arquivo não pode ser completado, por favor tente novamente", + "reportGenAt": "Informe gerado em ", + "copyright": "Copyright © 2013-2024 iFinex Inc. Todos os direitos reservados.", + "page": "Página", + "from": "de" + } +} diff --git a/locales/ru/email.json b/locales/ru/email.json new file mode 100644 index 00000000..e53c1326 --- /dev/null +++ b/locales/ru/email.json @@ -0,0 +1,15 @@ +{ + "template": { + "subject": "Ваш отчет готов", + "btnText": "Скачать Отчет", + "fileName": "Имя файла", + "unauth": "Ваш файл не может быть завершен, пожалуйста, попробуйте еще раз", + "readyForDownload": "Запрашиваемый вами отчет готов к загрузке", + "ifDidNotInitAction": "Если вы не инициировали это действие и подозреваете, что ваша учетная запись может быть взломана, пожалуйста,", + "freezeAccount": "заблокируйте ее", + "contactSupport": "и обратитесь в службу поддержки", + "youCan": "Вы можете", + "download": "скачать", + "pgpSignature": "файл цифровой подписи PGP" + } +} diff --git a/locales/ru/pdf.json b/locales/ru/pdf.json new file mode 100644 index 00000000..e04d9f2b --- /dev/null +++ b/locales/ru/pdf.json @@ -0,0 +1,16 @@ +{ + "template": { + "title": "Отчет", + "statementDetails": "Детали ведомости", + "statementDate": "Дата ведомости", + "snapshotAt": "Снимок в", + "period": "Период", + "username": "Имя пользователя", + "email": "Эл. почта", + "errorMessage": "Ваш файл не может быть завершен, пожалуйста, попробуйте еще раз", + "reportGenAt": "Отчет сгенерирован в", + "copyright": "Авторское право © 2013-2024 iFinex Inc. Все права защищены.", + "page": "Стр.", + "from": "из" + } +} diff --git a/locales/tr/email.json b/locales/tr/email.json new file mode 100644 index 00000000..d9237f34 --- /dev/null +++ b/locales/tr/email.json @@ -0,0 +1,15 @@ +{ + "template": { + "subject": "Raporunuz hazır", + "btnText": "Raporu İndir", + "fileName": "Dosya adı", + "unauth": "Dosyanız tamamlanamadı, lütfen tekrar deneyin", + "readyForDownload": "İstediğiniz rapor indirilmeye hazır", + "ifDidNotInitAction": "Bu işlemi siz başlatmadıysanız ve hesabınızın güvenliğinin ihlal edilmiş olabileceğinden şüpheleniyorsanız, lütfen", + "freezeAccount": "hesabınızı dondurun", + "contactSupport": "ve destek ekibiyle iletişime geçin", + "youCan": "Yapabilirsiniz", + "download": "indir", + "pgpSignature": "bir PGP dijital imza dosyası" + } +} diff --git a/locales/tr/pdf.json b/locales/tr/pdf.json new file mode 100644 index 00000000..2813bc70 --- /dev/null +++ b/locales/tr/pdf.json @@ -0,0 +1,16 @@ +{ + "template": { + "title": "Rapor", + "statementDetails": "Beyanname Detayları", + "statementDate": "Beyanname Tarihi", + "snapshotAt": "Anlık Görüntü:", + "period": "Dönem", + "username": "Kullanıcı Adı", + "email": "Email", + "errorMessage": "Dosyanız tamamlanamadı, lütfen tekrar deneyin", + "reportGenAt": "Rapor şu tarihte oluşturuldu:", + "copyright": "Telif Hakkı © 2013-2024 iFinex Inc. Tüm hakları saklıdır.", + "page": "Sayfa", + "from": "İtibaren" + } +} diff --git a/locales/zh-CN/email.json b/locales/zh-CN/email.json new file mode 100644 index 00000000..2b18df1b --- /dev/null +++ b/locales/zh-CN/email.json @@ -0,0 +1,15 @@ +{ + "template": { + "subject": "您的报告已备妥", + "btnText": "下载报告档案", + "fileName": "档案名称", + "unauth": "档案未下载成功,请重试一次", + "readyForDownload": "您请求的报告已可下载", + "ifDidNotInitAction": "如果您未进行此操作并且怀疑您的帐户已遭盗用,请", + "freezeAccount": "冻结您的帐户", + "contactSupport": "以及联络客服人员", + "youCan": "您可以", + "download": "下载", + "pgpSignature": "PGP数位签名档" + } +} diff --git a/locales/zh-CN/pdf.json b/locales/zh-CN/pdf.json new file mode 100644 index 00000000..579aca9b --- /dev/null +++ b/locales/zh-CN/pdf.json @@ -0,0 +1,16 @@ +{ + "template": { + "title": "报告", + "statementDetails": "结算明细", + "statementDate": "结算日期", + "snapshotAt": "快照时间", + "period": "期间", + "username": "用户名", + "email": "电子邮箱", + "errorMessage": "无法建立档案,请重试", + "reportGenAt": "报告创建时间", + "copyright": "Copyright © 2013-2024 iFinex Inc. All rights reserved.", + "page": "页数", + "from": "从" + } +} diff --git a/locales/zh-TW/email.json b/locales/zh-TW/email.json new file mode 100644 index 00000000..25956fd4 --- /dev/null +++ b/locales/zh-TW/email.json @@ -0,0 +1,15 @@ +{ + "template": { + "subject": "您的報告已備妥", + "btnText": "下載报告檔案", + "fileName": "檔案名稱", + "unauth": "檔案未下載成功,請重試一次", + "readyForDownload": "您請求的報告已可下載", + "ifDidNotInitAction": "如果您未進行此操作並且懷疑您的帳戶已遭盜用,請", + "freezeAccount": "凍結您的帳戶", + "contactSupport": "以及聯絡客服人員", + "youCan": "您可以", + "download": "下載", + "pgpSignature": "PGP數位簽名檔案" + } +} diff --git a/locales/zh-TW/pdf.json b/locales/zh-TW/pdf.json new file mode 100644 index 00000000..fae8c1f8 --- /dev/null +++ b/locales/zh-TW/pdf.json @@ -0,0 +1,16 @@ +{ + "template": { + "title": "報告", + "statementDetails": "結算明細", + "statementDate": "結算日期", + "snapshotAt": "快照時間:", + "period": "期間", + "username": "用戶名", + "email": "電子郵箱", + "errorMessage": "無法建立檔案,請重試", + "reportGenAt": "報告建立時間:", + "copyright": "Copyright © 2013-2024 iFinex Inc. All rights reserved.", + "page": "頁數", + "from": "從" + } +} diff --git a/package.json b/package.json index 595de9c8..db61a1bb 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,9 @@ "bitfinex-api-node": "6.0.0", "colors": "1.4.0", "csv": "5.5.3", + "i18next": "23.15.1", + "i18next-fs-backend": "2.3.2", "inversify": "6.0.1", - "js-yaml": "4.1.0", "lib-js-util-base": "git+https://github.com/bitfinexcom/lib-js-util-base.git", "lru": "3.1.0", "moment": "2.29.4", diff --git a/workers/api.service.report.wrk.js b/workers/api.service.report.wrk.js index 9ae4ff23..01a2b573 100644 --- a/workers/api.service.report.wrk.js +++ b/workers/api.service.report.wrk.js @@ -46,6 +46,7 @@ const TYPES = require('./loc.api/di/types') const { setLoggerDeps } = require('./loc.api/logger/logger-deps') +const getI18next = require('./loc.api/i18next') const { PDFBufferUnderElectronCreationError } = require('./loc.api/errors') @@ -60,6 +61,7 @@ class WrkReportServiceApi extends WrkApi { this.coreDeps = [] this.appDeps = [] + this.transPaths = [] this.loadDIConfig() this.loadCoreDeps() @@ -70,6 +72,11 @@ class WrkReportServiceApi extends WrkApi { this.start() } + addTransLocation (transPath) { + const _transPath = transPath ?? path.join(__dirname, '../locales') + this.transPaths.push(_transPath) + } + loadDIConfig (cont = container) { const conf = this.conf[this.group] @@ -88,6 +95,15 @@ class WrkReportServiceApi extends WrkApi { this.container.load(...this.appDeps) } + async getI18next (i18nextConfigs) { + const i18next = await getI18next({ + i18nextConfigs, + transPaths: this.transPaths + }) + + return i18next + } + getPluginCtx (type) { const ctx = super.getPluginCtx(type) @@ -124,6 +140,7 @@ class WrkReportServiceApi extends WrkApi { init () { super.init() + this.addTransLocation() const dbPathAbsolute = path.isAbsolute(argv.dbFolder) ? argv.dbFolder @@ -179,6 +196,8 @@ class WrkReportServiceApi extends WrkApi { rService.ctx = rService.caller.getCtx() } + const i18next = await this.getI18next() + this.loadAppDeps({ rService, processorQueue, @@ -186,6 +205,7 @@ class WrkReportServiceApi extends WrkApi { link: this.grc_bfx.link, deflateFac: this.deflate_gzip, grcSlackFac, + i18next, ...deps }) diff --git a/workers/loc.api/di/app.deps.js b/workers/loc.api/di/app.deps.js index c66db1d3..4a57dd8c 100644 --- a/workers/loc.api/di/app.deps.js +++ b/workers/loc.api/di/app.deps.js @@ -40,11 +40,13 @@ module.exports = ({ aggregatorQueue, deflateFac, grcSlackFac, - link + link, + i18next }) => { return new ContainerModule((bind) => { bind(TYPES.RService).toConstantValue(rService) bind(TYPES.RootPath).toConstantValue(rService.ctx.rootPath) + bind(TYPES.I18next).toConstantValue(i18next) bind(TYPES.RServiceDepsSchema).toConstantValue([ ['_responder', TYPES.Responder], ['_getREST', TYPES.GetREST], @@ -52,7 +54,8 @@ module.exports = ({ ['_prepareApiResponse', TYPES.PrepareApiResponse], ['_generateReportFile', TYPES.GenerateReportFile], ['_hasGrcService', TYPES.HasGrcService], - ['_weightedAveragesReport', TYPES.WeightedAveragesReport] + ['_weightedAveragesReport', TYPES.WeightedAveragesReport], + ['_i18next', TYPES.I18next] ]) bind(TYPES.RServiceDepsSchemaAliase) .toDynamicValue((ctx) => { @@ -164,7 +167,10 @@ module.exports = ({ bind(TYPES.SendMail).toConstantValue( bindDepsToFn( sendMail, - [TYPES.GrcBfxReq] + [ + TYPES.GrcBfxReq, + TYPES.I18next + ] ) ) bind(TYPES.Processor) diff --git a/workers/loc.api/di/types.js b/workers/loc.api/di/types.js index 693bb2f6..09b5b67a 100644 --- a/workers/loc.api/di/types.js +++ b/workers/loc.api/di/types.js @@ -8,6 +8,7 @@ module.exports = { Container: Symbol.for('Container'), LoggerFactory: Symbol.for('LoggerFactory'), Logger: Symbol.for('Logger'), + I18next: Symbol.for('I18next'), RService: Symbol.for('RService'), DeflateFac: Symbol.for('DeflateFac'), GrcSlackFac: Symbol.for('GrcSlackFac'), diff --git a/workers/loc.api/generate-report-file/pdf-writer/index.js b/workers/loc.api/generate-report-file/pdf-writer/index.js index a04beaa3..7b062ed1 100644 --- a/workers/loc.api/generate-report-file/pdf-writer/index.js +++ b/workers/loc.api/generate-report-file/pdf-writer/index.js @@ -4,8 +4,6 @@ const { Transform } = require('stream') const path = require('path') const fs = require('fs') const pug = require('pug') -const yaml = require('js-yaml') -const { merge } = require('lib-js-util-base') const getCompleteFileName = require('../../queue/helpers/get-complete-file-name') const getTranslator = require('../../helpers/get-translator') @@ -16,6 +14,9 @@ const { GrcPDFAvailabilityError } = require('../../errors') const TEMPLATE_FILE_NAMES = require('./template-file-names') +const TRANSLATION_NAMESPACES = require( + '../../i18next/translation.namespaces' +) const placeholderPattern = /\$\{[a-zA-Z0-9]+\}/g const pathToFonts = path.join(__dirname, 'templates/fonts') @@ -31,27 +32,27 @@ const { decorateInjectable } = require('../../di/utils') const depsTypes = (TYPES) => [ TYPES.ROOT_FOLDER_PATH, TYPES.HasGrcService, - TYPES.GrcBfxReq + TYPES.GrcBfxReq, + TYPES.I18next ] class PdfWriter { #fonts = this.#renderFontsTemplate(fontsTemplate, base64Fonts) - #translationsArr = [] - #translations = {} #templatePaths = new Map() #templates = new Map() constructor ( rootFolderPath, hasGrcService, - grcBfxReq + grcBfxReq, + i18next ) { this.rootFolderPath = rootFolderPath this.hasGrcService = hasGrcService this.grcBfxReq = grcBfxReq + this.i18next = i18next this.isElectronjsEnv = false - this.addTranslations() this.addTemplates() this.compileTemplate() } @@ -192,29 +193,13 @@ class PdfWriter { } #getTranslator (language) { - return getTranslator({ - language, - translations: this.#translations - }) - } - - addTranslations (trans = this.loadTranslations()) { - const translationsArr = Array.isArray(trans) - ? trans - : [trans] - - this.#translationsArr.push(...translationsArr) - this.#translations = merge({}, ...this.#translationsArr) - } - - loadTranslations ( - pathToTrans = path.join(__dirname, 'translations.yml') - ) { - const translations = yaml.load( - fs.readFileSync(pathToTrans, 'utf8') + return getTranslator( + { i18next: this.i18next }, + { + lng: language, + ns: TRANSLATION_NAMESPACES.PDF + } ) - - return translations } addTemplates (params) { @@ -285,17 +270,36 @@ class PdfWriter { } #getAvailableLanguages () { - const languages = Object.keys(this.#translations) + return this.i18next.options.preload + } - if (languages.every((lang) => lang !== 'en')) { - languages.push('en') - } + #getTemplateKey (templateFileName, language) { + const lng = this.#getTemplateLngKey(language) - return languages + return `${templateFileName}:${lng}` } - #getTemplateKey (templateFileName, language) { - return `${templateFileName}:${language}` + #getTemplateLngKey (language) { + const lngs = this.#getAvailableLanguages() + + if (lngs.some((lng) => lng === language)) { + return language + } + + const lng = lngs.find((lng) => ( + lng.startsWith(language) || + language.startsWith(lng) + )) + + if (lng) { + return lng + } + + const normalizedLng = language.replace(/-\S*/, '') + + return lngs.find( + (lng) => lng.startsWith(normalizedLng) + ) ?? language } #renderFontsTemplate ( diff --git a/workers/loc.api/generate-report-file/pdf-writer/translations.yml b/workers/loc.api/generate-report-file/pdf-writer/translations.yml deleted file mode 100644 index 6969e504..00000000 --- a/workers/loc.api/generate-report-file/pdf-writer/translations.yml +++ /dev/null @@ -1,104 +0,0 @@ -en: - template: - title: Report - statementDetails: Statement Details - statementDate: Statement date - snapshotAt: Snapshot at - period: Period - username: Username - email: Email - errorMessage: Your file could not be completed, please try again - reportGenAt: Report generated at - copyright: Copyright © 2013-2024 iFinex Inc. All rights reserved. - page: Page - from: from - -ru: - template: - title: Отчет - statementDetails: Детали ведомости - statementDate: Дата ведомости - snapshotAt: Снимок в - period: Период - username: Имя пользователя - email: Эл. почта - errorMessage: Ваш файл не может быть завершен, пожалуйста, попробуйте еще раз - reportGenAt: Отчет сгенерирован в - copyright: Авторское право © 2013-2024 iFinex Inc. Все права защищены. - page: Стр. - from: из - -zh-CN: - template: - title: 报告 - statementDetails: 结算明细 - statementDate: 结算日期 - snapshotAt: 快照时间 - period: 期间 - username: 用户名 - email: 电子邮箱 - errorMessage: 无法建立档案,请重试 - reportGenAt: 报告创建时间 - copyright: Copyright © 2013-2024 iFinex Inc. All rights reserved. - page: 页数 - from: 从 - -zh-TW: - template: - title: 報告 - statementDetails: 結算明細 - statementDate: 結算日期 - snapshotAt: 快照時間: - period: 期間 - username: 用戶名 - email: 電子郵箱 - errorMessage: 無法建立檔案,請重試 - reportGenAt: 報告建立時間: - copyright: Copyright © 2013-2024 iFinex Inc. All rights reserved. - page: 頁數 - from: 從 - -es-EM: - template: - title: Reporte - statementDetails: Detalles del Estado - statementDate: Fecha del Estado - snapshotAt: Captura al - period: Periodo - username: Nombre de Usuario - email: Correo - errorMessage: Tu archivo no pudo ser completado, por favor inténtalo de nuevo - reportGenAt: Reporte generado al - copyright: Copyright © 2013-2024 iFinex Inc. Todos los derechos reservados. - page: Página - from: de - -tr: - template: - title: Rapor - statementDetails: Beyanname Detayları - statementDate: Beyanname Tarihi - snapshotAt: 'Anlık Görüntü:' - period: Dönem - username: Kullanıcı Adı - email: Email - errorMessage: Dosyanız tamamlanamadı, lütfen tekrar deneyin - reportGenAt: 'Rapor şu tarihte oluşturuldu:' - copyright: Telif Hakkı © 2013-2024 iFinex Inc. Tüm hakları saklıdır. - page: Sayfa - from: İtibaren - -pt-BR: - template: - title: Informes - statementDetails: Detalhes da declaração - statementDate: Data da declaração - snapshotAt: Captura de - period: Período - username: Nome de Usuário - email: e-mail - errorMessage: Seu arquivo não pode ser completado, por favor tente novamente - reportGenAt: 'Informe gerado em ' - copyright: Copyright © 2013-2024 iFinex Inc. Todos os direitos reservados. - page: Página - from: de diff --git a/workers/loc.api/helpers/get-translator.js b/workers/loc.api/helpers/get-translator.js index 80f5e343..0a1b919d 100644 --- a/workers/loc.api/helpers/get-translator.js +++ b/workers/loc.api/helpers/get-translator.js @@ -1,60 +1,31 @@ 'use strict' -const LANGUAGES = require('./languages') - -const getTranslator = (params) => { +const getTranslator = (deps, commonOpts) => { const { - language = 'en', - translations, - isNotDefaultTranslatorUsed = false - } = params ?? {} - - const normLang = LANGUAGES?.[language] ?? 'en' - const translatorByDefault = ( - !isNotDefaultTranslatorUsed && - getTranslator({ - language: 'en', - translations, - isNotDefaultTranslatorUsed: true - }) - ) + i18next + } = deps ?? {} return (defVal = '', opts) => { const prop = typeof opts === 'string' ? opts : opts?.prop + const options = ( + opts && + typeof opts === 'object' + ) + ? opts + : {} + const defaultValue = defVal ?? + commonOpts?.defaultValue ?? + options?.defaultValue ?? + '' - if ( - !translations?.[normLang] || - typeof translations[normLang] !== 'object' || - Object.keys(translations[normLang]) === 0 || - typeof prop !== 'string' || - !prop - ) { - return translatorByDefault - ? translatorByDefault(defVal, prop) - : defVal - } - - const res = prop.split('.').reduce((accum, curr) => { - if ( - typeof accum[curr] === 'object' || - typeof accum[curr] === 'string' || - Number.isFinite(accum[curr]) - ) { - return accum[curr] - } - - return accum - }, translations[normLang]) - - if (typeof res === 'object') { - return translatorByDefault - ? translatorByDefault(defVal, prop) - : defVal - } + return i18next.t(prop, { + ...commonOpts, + ...options, - return res + defaultValue + }) } } diff --git a/workers/loc.api/helpers/index.js b/workers/loc.api/helpers/index.js index 254743fe..5da352d6 100644 --- a/workers/loc.api/helpers/index.js +++ b/workers/loc.api/helpers/index.js @@ -68,7 +68,6 @@ const getDataFromApi = require('./get-data-from-api') const splitSymbolPairs = require('./split-symbol-pairs') const FOREX_SYMBS = require('./forex.symbs') const getTranslator = require('./get-translator') -const LANGUAGES = require('./languages') module.exports = { getREST, @@ -124,6 +123,5 @@ module.exports = { parsePositionsAuditId, splitSymbolPairs, FOREX_SYMBS, - getTranslator, - LANGUAGES + getTranslator } diff --git a/workers/loc.api/helpers/languages.js b/workers/loc.api/helpers/languages.js deleted file mode 100644 index 12013361..00000000 --- a/workers/loc.api/helpers/languages.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict' - -module.exports = { - en: 'en', - 'en-US': 'en', - ru: 'ru', - 'zh-CN': 'zh-CN', - 'zh-TW': 'zh-TW', - tr: 'tr', - 'tr-TR': 'tr', - es: 'es-EM', - 'es-EM': 'es-EM', - pt: 'pt-BR', - 'pt-PT': 'pt-BR', - 'pt-BR': 'pt-BR' -} diff --git a/workers/loc.api/i18next/fs.multilocation.backend.js b/workers/loc.api/i18next/fs.multilocation.backend.js new file mode 100644 index 00000000..ae324f1a --- /dev/null +++ b/workers/loc.api/i18next/fs.multilocation.backend.js @@ -0,0 +1,58 @@ +'use strict' + +const Backend = require('i18next-fs-backend') +const { merge, min } = require('lib-js-util-base') + +/** + * Extend the main fs Backend to provide the ability + * to load and merge translations in bfx-reports-framework + */ +class FsMultilocationBackend extends Backend { + async read (language, namespace, callback) { + try { + const loadPaths = this.options.loadPaths + const errors = [] + const dataArr = [] + const timestamps = [] + + for (const loadPath of loadPaths) { + this.options.loadPath = loadPath + + await new Promise((resolve) => { + super.read(language, namespace, (err, data, timestamp) => { + if (err) { + errors.push(err) + resolve() + + return + } + if (data) { + dataArr.push(data) + } + if (timestamp) { + timestamps.push(timestamp) + } + + resolve() + }) + }) + } + + if (loadPaths.length === errors.length) { + callback(errors[0], false) + + return + } + + callback( + null, + merge({}, ...dataArr), + min(timestamps) + ) + } catch (err) { + callback(err, false) + } + } +} + +module.exports = FsMultilocationBackend diff --git a/workers/loc.api/i18next/index.js b/workers/loc.api/i18next/index.js new file mode 100644 index 00000000..6baaba7f --- /dev/null +++ b/workers/loc.api/i18next/index.js @@ -0,0 +1,59 @@ +'use strict' + +const i18next = require('i18next') +const { merge } = require('lib-js-util-base') +const path = require('path') +const fs = require('fs') + +const FsMultilocationBackend = require('./fs.multilocation.backend') +const TRANSLATION_NAMESPACES = require('./translation.namespaces') + +let i18nextInstance = null + +module.exports = (params) => { + if (i18nextInstance) { + return i18nextInstance + } + + const { + i18nextConfigs, + transPaths + } = params ?? {} + + const configs = merge( + { + fallbackLng: 'en', + ns: Object.values(TRANSLATION_NAMESPACES), + defaultNS: 'email', + preload: [...transPaths.reduce((accum, transPath) => { + const allFileNames = fs.readdirSync(transPath) + + for (const fileName of allFileNames) { + const filePath = path.join(transPath, fileName) + const stats = fs.lstatSync(filePath) + + if (!stats.isDirectory()) { + continue + } + + accum.add(fileName) + } + + return accum + }, new Set())], + backend: { + loadPaths: transPaths.map((transPath) => ( + path.join(transPath, '{{lng}}/{{ns}}.json') + )) + } + }, + i18nextConfigs + ) + + const promise = i18next + .use(FsMultilocationBackend) + .init(configs) + i18nextInstance = i18next + + return promise.then(() => i18next) +} diff --git a/workers/loc.api/i18next/translation.namespaces.js b/workers/loc.api/i18next/translation.namespaces.js new file mode 100644 index 00000000..9932066f --- /dev/null +++ b/workers/loc.api/i18next/translation.namespaces.js @@ -0,0 +1,8 @@ +'use strict' + +const TRANSLATION_NAMESPACES = { + EMAIL: 'email', + PDF: 'pdf' +} + +module.exports = TRANSLATION_NAMESPACES diff --git a/workers/loc.api/queue/send-mail/index.js b/workers/loc.api/queue/send-mail/index.js index 18e533ba..1dfadd91 100644 --- a/workers/loc.api/queue/send-mail/index.js +++ b/workers/loc.api/queue/send-mail/index.js @@ -1,23 +1,33 @@ 'use strict' const path = require('path') -const fs = require('fs') const pug = require('pug') -const yaml = require('js-yaml') const getTranslator = require('../../helpers/get-translator') -const LANGUAGES = require('../../helpers/languages') +const TRANSLATION_NAMESPACES = require( + '../../i18next/translation.namespaces' +) const basePathToViews = path.join(__dirname, 'views') -const pathToTrans = path.join( - __dirname, - 'translations/email.yml' -) -const translations = yaml.load( - fs.readFileSync(pathToTrans, 'utf8') -) -module.exports = (grcBfxReq) => { +const SENDGRID_WORKER_SUPPORTED_LNGS = { + en: 'en', + 'en-US': 'en', + ru: 'ru', + 'ru-RU': 'ru', + zh: 'zh-CN', + 'zh-CN': 'zh-CN', + 'zh-TW': 'zh-TW', + pt: 'pt-BR', + 'pt-PT': 'pt-BR', + 'pt-BR': 'pt-BR' +} + +const _getSendgridLng = (lng) => { + return SENDGRID_WORKER_SUPPORTED_LNGS[lng] ?? lng +} + +module.exports = (grcBfxReq, i18next) => { return async ( configs, to, @@ -30,12 +40,14 @@ module.exports = (grcBfxReq) => { const { presigned_url: url, language = 'en' - } = { ...data } - const normLang = LANGUAGES?.[language] ?? 'en' - const translate = getTranslator({ - language: normLang, - translations - }) + } = data ?? {} + const translate = getTranslator( + { i18next }, + { + lng: language, + ns: TRANSLATION_NAMESPACES.EMAIL + } + ) const subject = translate( configs.subject, 'template.subject' @@ -60,7 +72,7 @@ module.exports = (grcBfxReq) => { text, subject, button, - language: normLang + language: _getSendgridLng(language) } return grcBfxReq({ diff --git a/workers/loc.api/queue/send-mail/translations/email.yml b/workers/loc.api/queue/send-mail/translations/email.yml deleted file mode 100644 index 3880f74e..00000000 --- a/workers/loc.api/queue/send-mail/translations/email.yml +++ /dev/null @@ -1,97 +0,0 @@ -en: - template: - subject: Your report is ready - btnText: Download Report - fileName: File name - unauth: Your file could not be completed, please try again - readyForDownload: The report you request is ready for download - ifDidNotInitAction: If you did not initiate this action and you suspect that your account may be compromised, please - freezeAccount: freeze your account - contactSupport: and contact support - youCan: You can - download: download - pgpSignature: a PGP digital signature file - -ru: - template: - subject: Ваш отчет готов - btnText: Скачать Отчет - fileName: Имя файла - unauth: Ваш файл не может быть завершен, пожалуйста, попробуйте еще раз - readyForDownload: Запрашиваемый вами отчет готов к загрузке - ifDidNotInitAction: Если вы не инициировали это действие и подозреваете, что ваша учетная запись может быть взломана, пожалуйста, - freezeAccount: заблокируйте ее - contactSupport: и обратитесь в службу поддержки - youCan: Вы можете - download: скачать - pgpSignature: файл цифровой подписи PGP - -zh-CN: - template: - subject: 您的报告已备妥 - btnText: 下载报告档案 - fileName: 档案名称 - unauth: 档案未下载成功,请重试一次 - readyForDownload: 您请求的报告已可下载 - ifDidNotInitAction: 如果您未进行此操作并且怀疑您的帐户已遭盗用,请 - freezeAccount: 冻结您的帐户 - contactSupport: 以及联络客服人员 - youCan: 您可以 - download: 下载 - pgpSignature: PGP数位签名档 - -zh-TW: - template: - subject: 您的報告已備妥 - btnText: 下載报告檔案 - fileName: 檔案名稱 - unauth: 檔案未下載成功,請重試一次 - readyForDownload: 您請求的報告已可下載 - ifDidNotInitAction: 如果您未進行此操作並且懷疑您的帳戶已遭盜用,請 - freezeAccount: 凍結您的帳戶 - contactSupport: 以及聯絡客服人員 - youCan: 您可以 - download: 下載 - pgpSignature: PGP數位簽名檔案 - -es-EM: - template: - subject: Tu reporte esta listo - btnText: Descargar el Informe - fileName: Nombre del archivo - unauth: Tu archivo no se pudo completar, intente de nuevo por favor. - readyForDownload: Tu reporte esta listo para ser descargado - ifDidNotInitAction: Si no realizaste esta acción y sospechas que tu cuenta puede estar comprometida, por favor - freezeAccount: congela tu cuenta - contactSupport: y contacta soporte - youCan: Puedes - download: descargar - pgpSignature: un archivo con firma digital PGP - -tr: - template: - subject: Raporunuz hazır - btnText: Raporu İndir - fileName: Dosya adı - unauth: Dosyanız tamamlanamadı, lütfen tekrar deneyin - readyForDownload: İstediğiniz rapor indirilmeye hazır - ifDidNotInitAction: Bu işlemi siz başlatmadıysanız ve hesabınızın güvenliğinin ihlal edilmiş olabileceğinden şüpheleniyorsanız, lütfen - freezeAccount: hesabınızı dondurun - contactSupport: ve destek ekibiyle iletişime geçin - youCan: Yapabilirsiniz - download: indir - pgpSignature: bir PGP dijital imza dosyası - -pt-BR: - template: - subject: Seu relatório está pronto - btnText: Baixar Relatório - fileName: Nome do arquivo - unauth: Não foi possível concluir seu arquivo, tente novamente - readyForDownload: O relatório solicitado está pronto para download - ifDidNotInitAction: Se você não iniciou esta ação e suspeita que sua conta pode estar comprometida - freezeAccount: por favor, congele sua conta - contactSupport: e entre em contato com o suporte - youCan: Você pode - download: baixar - pgpSignature: um arquivo de assinatura digital PGP