From 79b75cd56ce580b065b9ea5bbacb0e4ee5bb39e8 Mon Sep 17 00:00:00 2001 From: Glenn de Haan Date: Wed, 9 Oct 2024 10:33:43 +0200 Subject: [PATCH 1/7] Setup initial locales folder for translations. Added English translations for email. Added _locales.json for mapping --- locales/_locales.json | 3 +++ locales/en.json | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 locales/_locales.json create mode 100644 locales/en.json diff --git a/locales/_locales.json b/locales/_locales.json new file mode 100644 index 0000000..a259b5c --- /dev/null +++ b/locales/_locales.json @@ -0,0 +1,3 @@ +{ + "en": "English" +} diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..2b2cccf --- /dev/null +++ b/locales/en.json @@ -0,0 +1,21 @@ +{ + "email": { + "title": "WiFi Voucher Code", + "preHeader": "Your WiFi Voucher Code", + "greeting": "Hi there", + "intro": "Someone generated a WiFi Voucher, please use this code when connecting", + "connect": "Connect to", + "password": "Password", + "or": "or", + "scan": "Scan to connect", + "details": "Voucher Details", + "type": "Type", + "multiUse": "Multi-use", + "singleUse": "Single-use", + "duration": "Duration", + "dataLimit": "Data Limit", + "downloadLimit": "Download Limit", + "uploadLimit": "Upload Limit", + "poweredBy": "Powered by" + } +} From 1b1a934f87fe6702a6e80618ac07a3f2de65fd3e Mon Sep 17 00:00:00 2001 From: Glenn de Haan Date: Wed, 9 Oct 2024 11:37:47 +0200 Subject: [PATCH 2/7] Implemented the translation.js module. Implemented the 'TRANSLATION_DEBUG' environment variable. Updated the README.md to include the Translations chapter. Moved en.json to en/email.json to better utilize Crowdin --- README.md | 25 ++++++++++++++++++++++++ docker-compose.yml | 1 + locales/en.json | 21 --------------------- locales/en/email.json | 19 +++++++++++++++++++ modules/translation.js | 43 ++++++++++++++++++++++++++++++++++++++++++ modules/variables.js | 1 + 6 files changed, 89 insertions(+), 21 deletions(-) delete mode 100644 locales/en.json create mode 100644 locales/en/email.json create mode 100644 modules/translation.js diff --git a/README.md b/README.md index 6c1bfba..fdca28e 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,8 @@ services: SMTP_PASSWORD: '' # Sets the application Log Level (Valid Options: error|warn|info|debug|trace) LOG_LEVEL: 'info' + # Enables/disables translation debugging, when enabled only translation keys are shown + TRANSLATION_DEBUG: 'false' ``` ### Home Assistant Add-on @@ -454,6 +456,29 @@ Once the SMTP environment variables are configured, the email feature will be av ![Example Email](.docs/images/email_example.png) +## Translations + +The UniFi Voucher Site supports multiple languages, and we're actively working to expand the list of available translations. To facilitate this, we use **Crowdin**, a platform that allows people from around the world to help translate and improve the localization of the project. + +### How You Can Help + +If you'd like to contribute by translating the UniFi Voucher Site into your language or improve existing translations, you're welcome to join our project on Crowdin. Even small contributions can make a big difference! + +Simply visit our Crowdin project page by clicking the badge below: + +[![Crowdin](https://badges.crowdin.net/unifi-voucher-site/localized.svg)](https://crowdin.com/project/unifi-voucher-site) + +Once you're there, you can choose your language and start contributing immediately. Crowdin provides an intuitive interface to help you suggest translations, review them, or vote on others' contributions. + +### Getting Started + +1. **Create a Crowdin account** (if you don't have one already). +2. **Join the UniFi Voucher Site project** by visiting our [Crowdin page](https://crowdin.com/project/unifi-voucher-site). +3. Choose the language you want to contribute to or suggest improvements for. +4. Start translating or reviewing! + +Your contributions will be automatically included in the next release after review. + ## Screenshots ### Login (Desktop) diff --git a/docker-compose.yml b/docker-compose.yml index 128a671..1b0fc57 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,3 +34,4 @@ services: SMTP_USERNAME: '' SMTP_PASSWORD: '' LOG_LEVEL: 'info' + TRANSLATION_DEBUG: 'false' diff --git a/locales/en.json b/locales/en.json deleted file mode 100644 index 2b2cccf..0000000 --- a/locales/en.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "email": { - "title": "WiFi Voucher Code", - "preHeader": "Your WiFi Voucher Code", - "greeting": "Hi there", - "intro": "Someone generated a WiFi Voucher, please use this code when connecting", - "connect": "Connect to", - "password": "Password", - "or": "or", - "scan": "Scan to connect", - "details": "Voucher Details", - "type": "Type", - "multiUse": "Multi-use", - "singleUse": "Single-use", - "duration": "Duration", - "dataLimit": "Data Limit", - "downloadLimit": "Download Limit", - "uploadLimit": "Upload Limit", - "poweredBy": "Powered by" - } -} diff --git a/locales/en/email.json b/locales/en/email.json new file mode 100644 index 0000000..e3133b7 --- /dev/null +++ b/locales/en/email.json @@ -0,0 +1,19 @@ +{ + "title": "WiFi Voucher Code", + "preHeader": "Your WiFi Voucher Code", + "greeting": "Hi there", + "intro": "Someone generated a WiFi Voucher, please use this code when connecting", + "connect": "Connect to", + "password": "Password", + "or": "or", + "scan": "Scan to connect", + "details": "Voucher Details", + "type": "Type", + "multiUse": "Multi-use", + "singleUse": "Single-use", + "duration": "Duration", + "dataLimit": "Data Limit", + "downloadLimit": "Download Limit", + "uploadLimit": "Upload Limit", + "poweredBy": "Powered by" +} diff --git a/modules/translation.js b/modules/translation.js new file mode 100644 index 0000000..75e7358 --- /dev/null +++ b/modules/translation.js @@ -0,0 +1,43 @@ +/** + * Import base packages + */ +const fs = require('fs'); + +/** + * Import own modules + */ +const variables = require('./variables'); + +/** + * Translation returns translator function + * + * @param language + * @param module + * @return {(function(key: string): (string))} + */ +module.exports = (language = 'en', module) => { + // Check if translation file exists + if(!fs.existsSync(`${__dirname}/../locales/${language}/${module}.json`)) { + throw new Error(`[Translation] Missing translation file: ${__dirname}/../locales/${language}/${module}.json`); + } + + // Get locales mapping + const locales = JSON.parse(fs.readFileSync(`${__dirname}/../locales/_locales.json`, 'utf-8')); + // Get translation file + const translations = JSON.parse(fs.readFileSync(`${__dirname}/../locales/${language}/${module}.json`, 'utf-8')); + + // Return translate function + return (key) => { + if(key === '_locales') { + return locales; + } + + // Check if key exists within translation file + if(typeof translations[key] === 'undefined') { + throw new Error(`[Translation][${language}] Missing for key: ${key}`); + } + + // Check if debugging is enabled. If enabled only return key + return variables.translationDebug ? key : translations[key]; + }; +}; diff --git a/modules/variables.js b/modules/variables.js index 7d62da5..78ad33b 100644 --- a/modules/variables.js +++ b/modules/variables.js @@ -41,6 +41,7 @@ module.exports = { smtpUsername: config('smtp_username') || process.env.SMTP_USERNAME || '', smtpPassword: config('smtp_password') || process.env.SMTP_PASSWORD || '', logLevel: config('log_level') || process.env.LOG_LEVEL || 'info', + translationDebug: config('translation_debug') || (process.env.TRANSLATION_DEBUG === 'true') || false, gitTag: process.env.GIT_TAG || 'master', gitBuild: fs.existsSync('/etc/unifi_voucher_site_build') ? fs.readFileSync('/etc/unifi_voucher_site_build', 'utf-8') : 'Development' }; From 467ce7edb1fed52bba2ce5613081cdafa07f4b29 Mon Sep 17 00:00:00 2001 From: Glenn de Haan Date: Wed, 9 Oct 2024 12:39:44 +0200 Subject: [PATCH 3/7] Implemented mail translations. Updated debug output from translation.js --- modules/mail.js | 11 +++++++++-- modules/translation.js | 2 +- template/email/voucher.ejs | 32 ++++++++++++++++---------------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/modules/mail.js b/modules/mail.js index 73b6482..f65f00a 100644 --- a/modules/mail.js +++ b/modules/mail.js @@ -10,6 +10,7 @@ const nodemailer = require('nodemailer'); */ const variables = require('./variables'); const log = require('./log'); +const translation = require('./translation'); const qr = require('./qr'); /** @@ -47,12 +48,17 @@ module.exports = { */ send: (to, voucher) => { return new Promise(async (resolve, reject) => { + // Create new translator + const t = translation('en', 'email'); + + // Attempt to send mail via SMTP transport const result = await transport.sendMail({ from: variables.smtpFrom, to: to, - subject: 'WiFi Voucher Code', - text: `Hi there,\n\nSomeone generated a WiFi Voucher, please use this code when connecting:\n\n${voucher.code.slice(0, 5)}-${voucher.code.slice(5)}`, + subject: t('title'), + text: `${t('greeting')},\n\n${t('intro')}:\n\n${voucher.code.slice(0, 5)}-${voucher.code.slice(5)}`, html: ejs.render(fs.readFileSync(`${__dirname}/../template/email/voucher.ejs`, 'utf-8'), { + t, voucher, unifiSsid: variables.unifiSsid, unifiSsidPassword: variables.unifiSsidPassword, @@ -66,6 +72,7 @@ module.exports = { reject(`[Mail] ${e.message}`); }); + // Check if the email was sent successfully if(result) { log.info(`[Mail] Sent to: ${to}`); resolve(true); diff --git a/modules/translation.js b/modules/translation.js index 75e7358..f70a4e3 100644 --- a/modules/translation.js +++ b/modules/translation.js @@ -38,6 +38,6 @@ module.exports = (language = 'en', module) => { } // Check if debugging is enabled. If enabled only return key - return variables.translationDebug ? key : translations[key]; + return variables.translationDebug ? `%${key}%` : translations[key]; }; }; diff --git a/template/email/voucher.ejs b/template/email/voucher.ejs index c85d7d2..c54426b 100644 --- a/template/email/voucher.ejs +++ b/template/email/voucher.ejs @@ -3,7 +3,7 @@ - WiFi Voucher Code + <%= t('title') %> - + @@ -116,11 +116,11 @@

-

WiFi Voucher Code

+

<%= t('title') %>

-

Hi there,

-

Someone generated a WiFi Voucher, please use this code when connecting:

+

<%= t('greeting') %>,

+

<%= t('intro') %>:

<%= voucher.code.slice(0, 5) %>-<%= voucher.code.slice(5) %>

@@ -128,28 +128,28 @@ <% if(unifiSsid !== '') { %>
<% if(unifiSsidPassword !== '') { %> -

Connect to: <%= unifiSsid %>,

-

Password: <%= unifiSsidPassword %> or,

+

<%= t('connect') %>: <%= unifiSsid %>,

+

<%= t('password') %>: <%= unifiSsidPassword %> <%= t('or') %>,

<% } else { %> -

Connect to: <%= unifiSsid %> or,

+

<%= t('connect') %>: <%= unifiSsid %> <%= t('or') %>,

<% } %> -

Scan to connect:

+

<%= t('scan') %>:

 

<% } %> -

Voucher Details

+

<%= t('details') %>


-

Type: <%= voucher.quota === 0 ? 'Multi-use' : 'Single-use' %>

-

Duration: <%= timeConvert(voucher.duration) %>

+

<%= t('type') %>: <%= voucher.quota === 0 ? t('multiUse') : t('singleUse') %>

+

<%= t('duration') %>: <%= timeConvert(voucher.duration) %>

<% if(voucher.qos_usage_quota) { %> -

Data Limit: <%= bytesConvert(voucher.qos_usage_quota, 2) %>

+

<%= t('dataLimit') %>: <%= bytesConvert(voucher.qos_usage_quota, 2) %>

<% } %> <% if(voucher.qos_rate_max_down) { %> -

Download Limit: <%= bytesConvert(voucher.qos_rate_max_down, 1, true) %>

+

<%= t('downloadLimit') %>: <%= bytesConvert(voucher.qos_rate_max_down, 1, true) %>

<% } %> <% if(voucher.qos_rate_max_up) { %> -

Upload Limit: <%= bytesConvert(voucher.qos_rate_max_up, 1, true) %>

+

<%= t('uploadLimit') %>: <%= bytesConvert(voucher.qos_rate_max_up, 1, true) %>

<% } %> @@ -164,7 +164,7 @@
- Powered by UniFi Voucher Site. + <%= t('poweredBy') %> UniFi Voucher Site.
From 1bc459de66d71b410fafcfc1f0eec127b132d87e Mon Sep 17 00:00:00 2001 From: Glenn de Haan Date: Wed, 9 Oct 2024 13:48:26 +0200 Subject: [PATCH 4/7] Swapped mail function parameters. Replaced throw errors for log warnings. Implemented fallback language in translation.js. Added express-locale for future web i18n use --- modules/mail.js | 2 +- modules/translation.js | 13 +++++++++---- package-lock.json | 7 +++++++ package.json | 1 + server.js | 9 +++++++++ 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/modules/mail.js b/modules/mail.js index f65f00a..379a339 100644 --- a/modules/mail.js +++ b/modules/mail.js @@ -49,7 +49,7 @@ module.exports = { send: (to, voucher) => { return new Promise(async (resolve, reject) => { // Create new translator - const t = translation('en', 'email'); + const t = translation('email'); // Attempt to send mail via SMTP transport const result = await transport.sendMail({ diff --git a/modules/translation.js b/modules/translation.js index f70a4e3..c0bb1c5 100644 --- a/modules/translation.js +++ b/modules/translation.js @@ -6,19 +6,23 @@ const fs = require('fs'); /** * Import own modules */ +const log = require('./log'); const variables = require('./variables'); /** * Translation returns translator function * - * @param language * @param module + * @param language + * @param fallback * @return {(function(key: string): (string))} */ -module.exports = (language = 'en', module) => { +module.exports = (module, language = 'en', fallback = 'en') => { // Check if translation file exists if(!fs.existsSync(`${__dirname}/../locales/${language}/${module}.json`)) { - throw new Error(`[Translation] Missing translation file: ${__dirname}/../locales/${language}/${module}.json`); + log.warn(`[Translation] Missing translation file: ${__dirname}/../locales/${language}/${module}.json`); + language = fallback; + log.warn(`[Translation] Using fallback: ${__dirname}/../locales/${language}/${module}.json`); } // Get locales mapping @@ -34,7 +38,8 @@ module.exports = (language = 'en', module) => { // Check if key exists within translation file if(typeof translations[key] === 'undefined') { - throw new Error(`[Translation][${language}] Missing for key: ${key}`); + log.warn(`[Translation][${language}] Missing for key: ${key}`); + return `%${key}%`; } // Check if debugging is enabled. If enabled only return key diff --git a/package-lock.json b/package-lock.json index 14b5731..94fc3da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "cookie-parser": "^1.4.7", "ejs": "^3.1.10", "express": "^4.21.1", + "express-locale": "^2.0.2", "express-openid-connect": "^2.17.1", "js-logger": "^1.6.1", "jsonwebtoken": "^9.0.2", @@ -1343,6 +1344,12 @@ "node": ">= 0.10.0" } }, + "node_modules/express-locale": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/express-locale/-/express-locale-2.0.2.tgz", + "integrity": "sha512-z1hRa5iOwlgcM2iGpho3Mwq6DKv0534h1Ts2g2/Gct72g/YSrYSsSCHejLGjAT+hGoZOFcDUovmCkM+6YcQ4iQ==", + "license": "MIT" + }, "node_modules/express-openid-connect": { "version": "2.17.1", "resolved": "https://registry.npmjs.org/express-openid-connect/-/express-openid-connect-2.17.1.tgz", diff --git a/package.json b/package.json index d447f1e..e1207f7 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "cookie-parser": "^1.4.7", "ejs": "^3.1.10", "express": "^4.21.1", + "express-locale": "^2.0.2", "express-openid-connect": "^2.17.1", "js-logger": "^1.6.1", "jsonwebtoken": "^9.0.2", diff --git a/server.js b/server.js index a12bb44..476b7b5 100644 --- a/server.js +++ b/server.js @@ -6,6 +6,7 @@ const crypto = require('crypto'); const express = require('express'); const multer = require('multer'); const cookieParser = require('cookie-parser'); +const locale = require('express-locale'); /** * Import own modules @@ -96,6 +97,14 @@ if(!variables.authDisabled && variables.authOidcEnabled) { oidc.init(app); } +/** + * Enable locale + */ +app.use(locale({ + "priority": ["accept-language", "default"], + "default": "en-GB" +})); + /** * Enable multer */ From d5c19d021dcd8f1ad33b131caa3526c55e039641 Mon Sep 17 00:00:00 2001 From: Glenn de Haan Date: Thu, 10 Oct 2024 19:36:07 +0200 Subject: [PATCH 5/7] Removed _locales.json implementation. Added language parameter to mail.js. Implemented regex check to translation.js. Added language dropdown to email component. Added languages.js --- locales/_locales.json | 3 --- modules/mail.js | 5 +++-- modules/translation.js | 12 ++++++------ server.js | 4 +++- template/components/email.ejs | 10 ++++++++++ utils/languages.js | 7 +++++++ 6 files changed, 29 insertions(+), 12 deletions(-) delete mode 100644 locales/_locales.json create mode 100644 utils/languages.js diff --git a/locales/_locales.json b/locales/_locales.json deleted file mode 100644 index a259b5c..0000000 --- a/locales/_locales.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "en": "English" -} diff --git a/modules/mail.js b/modules/mail.js index 379a339..7757637 100644 --- a/modules/mail.js +++ b/modules/mail.js @@ -44,12 +44,13 @@ module.exports = { * * @param to * @param voucher + * @param language * @return {Promise} */ - send: (to, voucher) => { + send: (to, voucher, language) => { return new Promise(async (resolve, reject) => { // Create new translator - const t = translation('email'); + const t = translation('email', language); // Attempt to send mail via SMTP transport const result = await transport.sendMail({ diff --git a/modules/translation.js b/modules/translation.js index c0bb1c5..9a65149 100644 --- a/modules/translation.js +++ b/modules/translation.js @@ -18,6 +18,12 @@ const variables = require('./variables'); * @return {(function(key: string): (string))} */ module.exports = (module, language = 'en', fallback = 'en') => { + // Prevent users from escaping the filesystem + if(!new RegExp(/^[a-zA-Z]*$/).test(language)) { + log.error(`[Translation] Detected path escalation! Forcing fallback and skipping user input...`); + language = fallback; + } + // Check if translation file exists if(!fs.existsSync(`${__dirname}/../locales/${language}/${module}.json`)) { log.warn(`[Translation] Missing translation file: ${__dirname}/../locales/${language}/${module}.json`); @@ -25,17 +31,11 @@ module.exports = (module, language = 'en', fallback = 'en') => { log.warn(`[Translation] Using fallback: ${__dirname}/../locales/${language}/${module}.json`); } - // Get locales mapping - const locales = JSON.parse(fs.readFileSync(`${__dirname}/../locales/_locales.json`, 'utf-8')); // Get translation file const translations = JSON.parse(fs.readFileSync(`${__dirname}/../locales/${language}/${module}.json`, 'utf-8')); // Return translate function return (key) => { - if(key === '_locales') { - return locales; - } - // Check if key exists within translation file if(typeof translations[key] === 'undefined') { log.warn(`[Translation][${language}] Missing for key: ${key}`); diff --git a/server.js b/server.js index 476b7b5..c731b5c 100644 --- a/server.js +++ b/server.js @@ -35,6 +35,7 @@ const types = require('./utils/types'); const time = require('./utils/time'); const bytes = require('./utils/bytes'); const status = require('./utils/status'); +const languages = require('./utils/languages'); /** * Setup Express app @@ -298,6 +299,7 @@ if(variables.serviceWeb) { baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '', timeConvert: time, bytesConvert: bytes, + languages, voucher, updated: cache.updated }); @@ -324,7 +326,7 @@ if(variables.serviceWeb) { }); if(voucher) { - const emailResult = await mail.send(req.body.email, voucher).catch((e) => { + const emailResult = await mail.send(req.body.email, voucher, req.body.language).catch((e) => { res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`); }); diff --git a/template/components/email.ejs b/template/components/email.ejs index fc2a49f..90a2575 100644 --- a/template/components/email.ejs +++ b/template/components/email.ejs @@ -24,6 +24,16 @@ +
+ +
+ +
+
diff --git a/utils/languages.js b/utils/languages.js new file mode 100644 index 0000000..8405269 --- /dev/null +++ b/utils/languages.js @@ -0,0 +1,7 @@ +/** + * Exports all languages + */ +module.exports = { + en: 'English', + nl: 'Dutch' +}; From c3b36b55a3de653797d0becc0c2398a8677a6663 Mon Sep 17 00:00:00 2001 From: Glenn de Haan Date: Sun, 13 Oct 2024 10:37:26 +0200 Subject: [PATCH 6/7] Implemented the print.json translation file. Added a new print dialog. Moved /print function to a post request. Updated translation.js debug output. Implemented translator within print.js. Fixed typos. Removed unused utils from email and print components render function --- locales/en/print.json | 15 ++++++++ modules/print.js | 65 ++++++++++++++++++++--------------- modules/translation.js | 2 +- server.js | 30 +++++++++++++--- template/components/email.ejs | 2 +- template/components/print.ejs | 44 ++++++++++++++++++++++++ template/voucher.ejs | 30 +++++++++++++--- 7 files changed, 150 insertions(+), 38 deletions(-) create mode 100644 locales/en/print.json create mode 100644 template/components/print.ejs diff --git a/locales/en/print.json b/locales/en/print.json new file mode 100644 index 0000000..37bee9f --- /dev/null +++ b/locales/en/print.json @@ -0,0 +1,15 @@ +{ + "title": "WiFi Voucher Code", + "connect": "Connect to", + "password": "Password", + "or": "or", + "scan": "Scan to connect", + "details": "Voucher Details", + "type": "Type", + "multiUse": "Multi-use", + "singleUse": "Single-use", + "duration": "Duration", + "dataLimit": "Data Limit", + "downloadLimit": "Download Limit", + "uploadLimit": "Upload Limit" +} diff --git a/modules/print.js b/modules/print.js index e518688..fee79f0 100644 --- a/modules/print.js +++ b/modules/print.js @@ -11,6 +11,7 @@ const PrinterTypes = require('node-thermal-printer').types; const variables = require('./variables'); const log = require('./log'); const qr = require('./qr'); +const translation = require('./translation'); /** * Import own utils @@ -27,10 +28,14 @@ module.exports = { * Generates a voucher as a PDF * * @param voucher + * @param language * @return {Promise} */ - pdf: (voucher) => { + pdf: (voucher, language) => { return new Promise(async (resolve) => { + // Create new translator + const t = translation('print', language); + const doc = new PDFDocument({ bufferPages: true, size: [226.77165354330398, size(voucher)], @@ -54,7 +59,7 @@ module.exports = { doc.font('Helvetica-Bold') .fontSize(20) - .text(`WiFi Voucher Code`, { + .text(`${t('title')}`, { align: 'center' }); doc.font('Helvetica-Bold') @@ -68,7 +73,7 @@ module.exports = { if(variables.unifiSsid !== '') { doc.font('Helvetica') .fontSize(10) - .text(`Connect to: `, { + .text(`${t('connect')}: `, { continued: true }); doc.font('Helvetica-Bold') @@ -83,7 +88,7 @@ module.exports = { .text(`,`); doc.font('Helvetica') .fontSize(10) - .text(`Password: `, { + .text(`${t('password')}: `, { continued: true }); doc.font('Helvetica-Bold') @@ -93,16 +98,16 @@ module.exports = { }); doc.font('Helvetica') .fontSize(10) - .text(` or,`); + .text(` ${t('or')},`); } else { doc.font('Helvetica') .fontSize(10) - .text(` or,`); + .text(` ${t('or')},`); } doc.font('Helvetica') .fontSize(10) - .text(`Scan to connect:`); + .text(`${t('scan')}:`); doc.image(await qr(), 75, variables.unifiSsidPassword !== '' ? 215 : 205, {fit: [75, 75], align: 'center', valign: 'center'}); doc.moveDown(6); @@ -112,7 +117,7 @@ module.exports = { doc.font('Helvetica-Bold') .fontSize(12) - .text(`Voucher Details`); + .text(`${t('details')}`); doc.font('Helvetica-Bold') .fontSize(10) @@ -120,16 +125,16 @@ module.exports = { doc.font('Helvetica-Bold') .fontSize(10) - .text(`Type: `, { + .text(`${t('type')}: `, { continued: true }); doc.font('Helvetica') .fontSize(10) - .text(voucher.quota === 0 ? 'Multi-use' : 'Single-use'); + .text(voucher.quota === 0 ? t('multiUse') : t('singleUse')); doc.font('Helvetica-Bold') .fontSize(10) - .text(`Duration: `, { + .text(`${t('duration')}: `, { continued: true }); doc.font('Helvetica') @@ -139,7 +144,7 @@ module.exports = { if(voucher.qos_usage_quota) { doc.font('Helvetica-Bold') .fontSize(10) - .text(`Data Limit: `, { + .text(`${t('dataLimit')}: `, { continued: true }); doc.font('Helvetica') @@ -150,7 +155,7 @@ module.exports = { if(voucher.qos_rate_max_down) { doc.font('Helvetica-Bold') .fontSize(10) - .text(`Download Limit: `, { + .text(`${t('downloadLimit')}: `, { continued: true }); doc.font('Helvetica') @@ -161,7 +166,7 @@ module.exports = { if(voucher.qos_rate_max_up) { doc.font('Helvetica-Bold') .fontSize(10) - .text(`Upload Limit: `, { + .text(`${t('uploadLimit')}: `, { continued: true }); doc.font('Helvetica') @@ -177,10 +182,14 @@ module.exports = { * Sends a print job to an ESC/POS compatible network printer * * @param voucher + * @param language * @return {Promise} */ - escpos: (voucher) => { + escpos: (voucher, language) => { return new Promise(async (resolve, reject) => { + // Create new translator + const t = translation('print', language); + const printer = new ThermalPrinter({ type: PrinterTypes.EPSON, interface: `tcp://${variables.printerIp}` @@ -202,7 +211,7 @@ module.exports = { printer.alignCenter(); printer.newLine(); printer.setTextSize(2, 2); - printer.println('WiFi Voucher Code'); + printer.println(`${t('title')}`); printer.setTextSize(1, 1); printer.println(`${voucher.code.slice(0, 5)}-${voucher.code.slice(5)}`); printer.setTextNormal(); @@ -213,7 +222,7 @@ module.exports = { printer.newLine(); printer.alignLeft(); - printer.print('Connect to: '); + printer.print(`${t('connect')}: `); printer.setTypeFontB(); printer.setTextSize(1, 1); printer.print(variables.unifiSsid); @@ -221,18 +230,18 @@ module.exports = { if(variables.unifiSsidPassword) { printer.print(','); printer.newLine(); - printer.print('Password: '); + printer.print(`${t('password')}: `); printer.setTypeFontB(); printer.setTextSize(1, 1); printer.print(variables.unifiSsidPassword); printer.setTextNormal(); - printer.print(' or,'); + printer.print(` ${t('or')},`); printer.newLine(); } else { - printer.print(' or,'); + printer.print(` ${t('or')},`); printer.newLine(); } - printer.println('Scan to connect:'); + printer.println(`${t('scan')}:`); printer.alignCenter(); await printer.printImageBuffer(await qr(true)); } @@ -243,20 +252,20 @@ module.exports = { printer.alignLeft(); printer.setTypeFontB(); printer.setTextSize(1, 1); - printer.println('Voucher Details'); + printer.println(`${t('details')}`); printer.setTextNormal(); printer.drawLine(); printer.setTextDoubleHeight(); printer.invert(true); - printer.print('Type:'); + printer.print(`${t('type')}:`); printer.invert(false); - printer.print(voucher.quota === 0 ? ' Multi-use' : ' Single-use'); + printer.print(voucher.quota === 0 ? ` ${t('multiUse')}` : ` ${t('singleUse')}`); printer.newLine(); printer.setTextDoubleHeight(); printer.invert(true); - printer.print('Duration:'); + printer.print(`${t('duration')}:`); printer.invert(false); printer.print(` ${time(voucher.duration)}`); printer.newLine(); @@ -264,7 +273,7 @@ module.exports = { if(voucher.qos_usage_quota) { printer.setTextDoubleHeight(); printer.invert(true); - printer.print('Data Limit:'); + printer.print(`${t('dataLimit')}:`); printer.invert(false); printer.print(` ${bytes(voucher.qos_usage_quota, 2)}`); printer.newLine(); @@ -273,7 +282,7 @@ module.exports = { if(voucher.qos_rate_max_down) { printer.setTextDoubleHeight(); printer.invert(true); - printer.print('Download Limit:'); + printer.print(`${t('downloadLimit')}:`); printer.invert(false); printer.print(` ${bytes(voucher.qos_rate_max_down, 1, true)}`); printer.newLine(); @@ -282,7 +291,7 @@ module.exports = { if(voucher.qos_rate_max_up) { printer.setTextDoubleHeight(); printer.invert(true); - printer.print('Upload Limit:'); + printer.print(`${t('uploadLimit')}:`); printer.invert(false); printer.print(` ${bytes(voucher.qos_rate_max_up, 1, true)}`); printer.newLine(); diff --git a/modules/translation.js b/modules/translation.js index 9a65149..017abd6 100644 --- a/modules/translation.js +++ b/modules/translation.js @@ -43,6 +43,6 @@ module.exports = (module, language = 'en', fallback = 'en') => { } // Check if debugging is enabled. If enabled only return key - return variables.translationDebug ? `%${key}%` : translations[key]; + return variables.translationDebug ? `t('${key}')` : translations[key]; }; }; diff --git a/server.js b/server.js index c731b5c..da21e0d 100644 --- a/server.js +++ b/server.js @@ -257,9 +257,33 @@ if(variables.serviceWeb) { return e._id === req.params.id; }); + if(voucher) { + res.render('components/print', { + baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '', + languages, + voucher, + updated: cache.updated + }); + } else { + res.status(404); + res.render('404', { + baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '' + }); + } + }); + app.post('/voucher/:id/print', [authorization.web], async (req, res) => { + if(variables.printerType === '') { + res.status(501).send(); + return; + } + + const voucher = cache.vouchers.find((e) => { + return e._id === req.params.id; + }); + if(voucher) { if(variables.printerType === 'pdf') { - const buffers = await print.pdf(voucher); + const buffers = await print.pdf(voucher, req.body.language); const pdfData = Buffer.concat(buffers); res.writeHead(200, { 'Content-Length': Buffer.byteLength(pdfData), @@ -269,7 +293,7 @@ if(variables.serviceWeb) { } if(variables.printerType === 'escpos') { - const printResult = await print.escpos(voucher).catch((e) => { + const printResult = await print.escpos(voucher, req.body.language).catch((e) => { res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`); }); @@ -297,8 +321,6 @@ if(variables.serviceWeb) { if(voucher) { res.render('components/email', { baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '', - timeConvert: time, - bytesConvert: bytes, languages, voucher, updated: cache.updated diff --git a/template/components/email.ejs b/template/components/email.ejs index 90a2575..dc029a0 100644 --- a/template/components/email.ejs +++ b/template/components/email.ejs @@ -5,7 +5,7 @@
-
+