From d35b1241789dc1faaac9f695839ccebcc987d1da Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 5 Sep 2020 17:31:39 +0200 Subject: [PATCH] feat: i18n middleware feat: i18n configuration. feat: i18n with tenancy. --- server/config/config.js | 4 ++ server/package.json | 1 + server/src/http/controllers/Ping.ts | 1 + server/src/http/index.js | 63 ++++++++++--------- server/src/http/middleware/I18nMiddleware.ts | 15 +++++ .../src/http/middleware/TenancyMiddleware.js | 4 ++ server/src/jobs/ResetPasswordMail.ts | 9 +-- server/src/loaders/dependencyInjector.ts | 4 ++ server/src/loaders/express.ts | 26 +++++++- server/src/loaders/i18n.ts | 9 +++ server/src/loaders/index.ts | 4 ++ server/src/locales/ar.json | 4 ++ server/src/locales/en.json | 4 ++ server/src/services/Tenancy/TenancyService.ts | 8 +++ 14 files changed, 122 insertions(+), 34 deletions(-) create mode 100644 server/src/http/middleware/I18nMiddleware.ts create mode 100644 server/src/loaders/i18n.ts create mode 100644 server/src/locales/ar.json create mode 100644 server/src/locales/en.json diff --git a/server/config/config.js b/server/config/config.js index 6f409e1b4..fbac73223 100644 --- a/server/config/config.js +++ b/server/config/config.js @@ -76,4 +76,8 @@ module.exports = { jwtSecret: 'b0JDZW56RnV6aEthb0RGPXVEcUI', contactUsMail: 'support@bigcapital.ly', baseURL: 'https://bigcapital.ly', + + api: { + prefix: '/api' + } }; diff --git a/server/package.json b/server/package.json index c42adc525..8ca84982e 100644 --- a/server/package.json +++ b/server/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@hapi/boom": "^7.4.3", + "@types/i18n": "^0.8.7", "agenda": "^3.1.0", "agendash": "^1.0.0", "app-root-path": "^3.0.0", diff --git a/server/src/http/controllers/Ping.ts b/server/src/http/controllers/Ping.ts index 0cbb6ec08..7fee8bf19 100644 --- a/server/src/http/controllers/Ping.ts +++ b/server/src/http/controllers/Ping.ts @@ -1,5 +1,6 @@ import 'reflect-metadata'; import { Router, Request, Response } from 'express'; +import i18n from 'i18n'; export default class Ping { /** diff --git a/server/src/http/index.js b/server/src/http/index.js index b3ea77c38..e1756b6bd 100644 --- a/server/src/http/index.js +++ b/server/src/http/index.js @@ -1,4 +1,4 @@ -import express from 'express'; +import { Router } from 'express'; import { Container } from 'typedi'; // Middlewares @@ -8,6 +8,7 @@ import SubscriptionMiddleware from '@/http/middleware/SubscriptionMiddleware'; import TenancyMiddleware from '@/http/middleware/TenancyMiddleware'; import EnsureTenantIsInitialized from '@/http/middleware/EnsureTenantIsInitialized'; import SettingsMiddleware from '@/http/middleware/SettingsMiddleware'; +import I18nMiddleware from '@/http/middleware/I18nMiddleware'; // Routes import Authentication from '@/http/controllers/Authentication'; @@ -36,43 +37,49 @@ import Agendash from '@/http/controllers/Agendash'; import Subscription from '@/http/controllers/Subscription'; import VouchersController from '@/http/controllers/Subscription/Vouchers'; +export default () => { + const app = Router(); -export default (app) => { - app.use('/api/auth', Container.get(Authentication).router()); - app.use('/api/invite', Container.get(InviteUsers).router()); - app.use('/api/organization', Container.get(Organization).router()); - app.use('/api/vouchers', Container.get(VouchersController).router()); - app.use('/api/subscription', Container.get(Subscription).router()); - app.use('/api/ping', Container.get(Ping).router()); + app.use(I18nMiddleware); - const dashboard = express.Router(); + app.use('/auth', Container.get(Authentication).router()); + app.use('/invite', Container.get(InviteUsers).router()); + app.use('/organization', Container.get(Organization).router()); + app.use('/vouchers', Container.get(VouchersController).router()); + app.use('/subscription', Container.get(Subscription).router()); + app.use('/ping', Container.get(Ping).router()); + + const dashboard = Router(); dashboard.use(JWTAuth); dashboard.use(AttachCurrentTenantUser) dashboard.use(TenancyMiddleware); + dashboard.use(I18nMiddleware); dashboard.use(SubscriptionMiddleware('main')); dashboard.use(EnsureTenantIsInitialized); dashboard.use(SettingsMiddleware); - - dashboard.use('/api/users', Users.router()); - dashboard.use('/api/currencies', Currencies.router()); - dashboard.use('/api/accounts', Accounts.router()); - dashboard.use('/api/account_types', AccountTypes.router()); - dashboard.use('/api/accounting', Accounting.router()); - dashboard.use('/api/views', Views.router()); - dashboard.use('/api/items', Container.get(Items).router()); - dashboard.use('/api/item_categories', Container.get(ItemCategories).router()); - dashboard.use('/api/expenses', Expenses.router()); - dashboard.use('/api/financial_statements', FinancialStatements.router()); - dashboard.use('/api/settings', Container.get(Settings).router()); - dashboard.use('/api/sales', Sales.router()); - dashboard.use('/api/customers', Customers.router()); - dashboard.use('/api/vendors', Vendors.router()); - dashboard.use('/api/purchases', Purchases.router()); - dashboard.use('/api/resources', Resources.router()); - dashboard.use('/api/exchange_rates', ExchangeRates.router()); - dashboard.use('/api/media', Media.router()); + + dashboard.use('/users', Users.router()); + dashboard.use('/currencies', Currencies.router()); + dashboard.use('/accounts', Accounts.router()); + dashboard.use('/account_types', AccountTypes.router()); + dashboard.use('/accounting', Accounting.router()); + dashboard.use('/views', Views.router()); + dashboard.use('/items', Container.get(Items).router()); + dashboard.use('/item_categories', Container.get(ItemCategories).router()); + dashboard.use('/expenses', Expenses.router()); + dashboard.use('/financial_statements', FinancialStatements.router()); + dashboard.use('/settings', Container.get(Settings).router()); + dashboard.use('/sales', Sales.router()); + dashboard.use('/customers', Customers.router()); + dashboard.use('/vendors', Vendors.router()); + dashboard.use('/purchases', Purchases.router()); + dashboard.use('/resources', Resources.router()); + dashboard.use('/exchange_rates', ExchangeRates.router()); + dashboard.use('/media', Media.router()); app.use('/agendash', Agendash.router()); app.use('/', dashboard); + + return app; }; diff --git a/server/src/http/middleware/I18nMiddleware.ts b/server/src/http/middleware/I18nMiddleware.ts new file mode 100644 index 000000000..027084f19 --- /dev/null +++ b/server/src/http/middleware/I18nMiddleware.ts @@ -0,0 +1,15 @@ +import { Container } from 'typedi'; +import { Request, Response, NextFunction } from 'express'; +import i18n from 'i18n'; + +export default (req: Request, res: Response, next: NextFunction) => { + const Logger = Container.get('logger'); + let language = req.headers['accept-language'] || 'en'; + + if (req.user && req.user.language) { + language = req.user.language; + } + Logger.info('[i18n_middleware] set locale language to i18n.', { language, user: req.user }); + i18n.setLocale(req, language); + next(); +}; \ No newline at end of file diff --git a/server/src/http/middleware/TenancyMiddleware.js b/server/src/http/middleware/TenancyMiddleware.js index 9c635fa53..18783d139 100644 --- a/server/src/http/middleware/TenancyMiddleware.js +++ b/server/src/http/middleware/TenancyMiddleware.js @@ -48,5 +48,9 @@ export default async (req, res, next) => { tenantContainer.set('tenant', tenant); Logger.info('[tenancy_middleware] tenant dependencies injected to container.'); + if (res.locals) { + tenantContainer.set('i18n', res.locals); + Logger.info('[tenancy_middleware] i18n locals injected.'); + } next(); } diff --git a/server/src/jobs/ResetPasswordMail.ts b/server/src/jobs/ResetPasswordMail.ts index 0cc99523c..cd33e49b3 100644 --- a/server/src/jobs/ResetPasswordMail.ts +++ b/server/src/jobs/ResetPasswordMail.ts @@ -20,18 +20,19 @@ export default class WelcomeEmailJob { * @param {Function} done */ public async handler(job, done: Function): Promise { - const { user, token } = job.attrs.data; + const { data } = job.attrs; + const { user, token } = data; const Logger = Container.get('logger'); const authService = Container.get(AuthenticationService); - Logger.info(`[send_reset_password] started: ${job.attrs.data}`); + Logger.info(`[send_reset_password] started.`, { data }); try { await authService.mailMessages.sendResetPasswordMessage(user, token); - Logger.info(`[send_reset_password] finished: ${job.attrs.data}`); + Logger.info(`[send_reset_password] finished.`, { data }); done() } catch (error) { - Logger.info(`[send_reset_password] error: ${job.attrs.data}, error: ${error}`); + Logger.error(`[send_reset_password] error.`, { data, error }); done(error); } } diff --git a/server/src/loaders/dependencyInjector.ts b/server/src/loaders/dependencyInjector.ts index f45a7c748..80f9e3a29 100644 --- a/server/src/loaders/dependencyInjector.ts +++ b/server/src/loaders/dependencyInjector.ts @@ -4,6 +4,7 @@ import agendaFactory from '@/loaders/agenda'; import SmsClientLoader from '@/loaders/smsClient'; import mailInstance from '@/loaders/mail'; import dbManagerFactory from '@/loaders/dbManager'; +import i18n from '@/loaders/i18n'; export default ({ mongoConnection, knex }) => { try { @@ -29,6 +30,9 @@ export default ({ mongoConnection, knex }) => { Container.set('agenda', agendaInstance); LoggerInstance.info('Agenda has been injected into container'); + Container.set('i18n', i18n); + LoggerInstance.info('i18n has been injected into container'); + return { agenda: agendaInstance }; } catch (e) { LoggerInstance.error('Error on dependency injector loader: %o', e); diff --git a/server/src/loaders/express.ts b/server/src/loaders/express.ts index 2524dd4a7..0ec22d4e6 100644 --- a/server/src/loaders/express.ts +++ b/server/src/loaders/express.ts @@ -2,20 +2,42 @@ import express from 'express'; import helmet from 'helmet'; import boom from 'express-boom'; import errorHandler from 'errorhandler'; -import i18n from 'i18n'; import fileUpload from 'express-fileupload'; +import i18n from 'i18n'; import routes from '@/http'; +import config from '@/../config/config'; export default ({ app }) => { // Express configuration. app.set('port', 3000); + // Helmet helps you secure your Express apps by setting various HTTP headers. app.use(helmet()); + + // Allow to full error stack traces and internal details app.use(errorHandler()); + + // Boom response objects. app.use(boom()); + + // Parses both json and urlencoded. app.use(express.json()); + + // Handle multi-media requests. app.use(fileUpload({ createParentPath: true, })); - routes(app); + + // Initialize i18n node. + app.use(i18n.init) + + // Prefix all application routes. + app.use(config.api.prefix, routes()); + + // catch 404 and forward to error handler + app.use((req, res, next) => { + const err = new Error('Not Found'); + err['status'] = 404; + next(err); + }) }; \ No newline at end of file diff --git a/server/src/loaders/i18n.ts b/server/src/loaders/i18n.ts new file mode 100644 index 000000000..b6622b00d --- /dev/null +++ b/server/src/loaders/i18n.ts @@ -0,0 +1,9 @@ +import i18n from 'i18n'; +import path from 'path'; + +export default () => i18n.configure({ + locales: ['en', 'ar'], + register: global, + directory: path.join(global.rootPath, 'src/locales'), + updateFiles: false +}) \ No newline at end of file diff --git a/server/src/loaders/index.ts b/server/src/loaders/index.ts index f34bd3583..3eee701b4 100644 --- a/server/src/loaders/index.ts +++ b/server/src/loaders/index.ts @@ -5,6 +5,7 @@ import expressLoader from '@/loaders/express'; import databaseLoader from '@/database/knex'; import dependencyInjectorLoader from '@/loaders/dependencyInjector'; import objectionLoader from '@/database/objection'; +import i18nConfig from '@/loaders/i18n'; // We have to import at least all the events once so they can be triggered import '@/loaders/events'; @@ -29,4 +30,7 @@ export default async ({ expressApp }) => { expressLoader({ app: expressApp }); Logger.info('Express loaded'); + + i18nConfig(); + Logger.info('I18n node configured.'); }; diff --git a/server/src/locales/ar.json b/server/src/locales/ar.json new file mode 100644 index 000000000..d23481838 --- /dev/null +++ b/server/src/locales/ar.json @@ -0,0 +1,4 @@ +{ + "Empty": "", + "Hello": "مرحبا" +} \ No newline at end of file diff --git a/server/src/locales/en.json b/server/src/locales/en.json new file mode 100644 index 000000000..48c4c5fdc --- /dev/null +++ b/server/src/locales/en.json @@ -0,0 +1,4 @@ +{ + "Empty": "", + "Hello": "Hello" +} \ No newline at end of file diff --git a/server/src/services/Tenancy/TenancyService.ts b/server/src/services/Tenancy/TenancyService.ts index f8ca06b3a..5b01ce042 100644 --- a/server/src/services/Tenancy/TenancyService.ts +++ b/server/src/services/Tenancy/TenancyService.ts @@ -17,4 +17,12 @@ export default class HasTenancyService { models(tenantId: number) { return this.tenantContainer(tenantId).get('models'); } + + /** + * Retrieve i18n locales methods. + * @param {number} tenantId + */ + i18n(tenantId: number) { + this.tenantContainer(tenantId).get('i18n'); + } } \ No newline at end of file