Add i18n api

parent cc7c237e
{"major":2,"minor":0,"build":4,"version":"2.0.4","timestamp":1526723375995}
\ No newline at end of file
{"major":2,"minor":0,"build":14,"version":"2.0.14","timestamp":1527520803792}
\ No newline at end of file
import * as express from 'express';
import * as compress from 'compression';
import * as logger from 'morgan';
import * as bodyParser from 'body-parser';
import * as cors from 'cors';
import * as compress from 'compression';
import * as busboy from 'connect-busboy';
import * as cors from 'cors';
import * as express from 'express';
import { NextFunction, Request, Response, Router } from 'express';
import * as i18n from 'i18n';
import * as logger from 'morgan';
import * as path from 'path';
import {NextFunction, Router, Response, Request} from 'express';
import options from './cors.config';
import {dynamicStatistics, staticStatistics} from './statistics';
import {apiRouter} from './routes/api';
import {libRouter} from './routes/lib';
import {legacyApiRouter} from './routes/legacy-api';
import {quizRouter} from './routes/quiz';
import {lobbyRouter} from './routes/lobby';
import {nicksRouter} from './routes/nicks';
import {themesRouter} from './routes/themes';
import {memberRouter} from './routes/member';
import {debugRouter} from './routes/debug';
import { apiRouter } from './routes/api';
import { debugRouter } from './routes/debug';
import { i18nApiRouter } from './routes/i18n-api';
import { legacyApiRouter } from './routes/legacy-api';
import { libRouter } from './routes/lib';
import { lobbyRouter } from './routes/lobby';
import { memberRouter } from './routes/member';
import { nicksRouter } from './routes/nicks';
import { quizRouter } from './routes/quiz';
import { themesRouter } from './routes/themes';
import { dynamicStatistics, staticStatistics } from './statistics';
i18n.configure({
// setup some locales - other locales default to en silently
locales: ['en', 'de', 'it', 'es', 'fr'],
// fall back from Dutch to German
fallbacks: {'nl': 'de'},
fallbacks: { 'nl': 'de' },
// where to store json files - defaults to './locales' relative to modules directory
directory: path.join(staticStatistics.pathToAssets, 'i18n'),
......@@ -73,8 +73,8 @@ i18n.configure({
// note that this will *not* overwrite existing properties with the same name
api: {
'__': 't', // now req.__ becomes req.t
'__n': 'tn' // and req.__n can be called as req.tn
}
'__n': 'tn', // and req.__n can be called as req.tn
},
});
// Creates and configures an ExpressJS web server.
......@@ -98,9 +98,9 @@ class App {
private middleware(): void {
this._express.use(logger('dev'));
this._express.use(busboy());
this._express.use(bodyParser.json());
this._express.use(bodyParser.json({ limit: '50mb' }));
this._express.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
this._express.use(i18n.init);
this._express.use(bodyParser.urlencoded({extended: false}));
this._express.use(cors(options));
this._express.use(compress());
this._express.options('*', cors(options));
......@@ -121,6 +121,7 @@ class App {
this._express.use(`${staticStatistics.routePrefix}/api/v1/lobby`, lobbyRouter);
this._express.use(`${staticStatistics.routePrefix}/api/v1/nicks`, nicksRouter);
this._express.use(`${staticStatistics.routePrefix}/api/v1/themes`, themesRouter);
this._express.use(`${staticStatistics.routePrefix}/api/v1/plugin/i18nator`, i18nApiRouter);
this._express.use(`${staticStatistics.routePrefix}/debug`, debugRouter);
}
......
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { availableLangs, i18nFileBaseLocation, projectAppLocation, projectGitLocation } from '../statistics';
export class I18nDAO {
public static readonly cache = { 'arsnova-click-v2-backend': {} };
constructor() {
this.reloadCache();
}
public reloadCache() {
Object.keys(I18nDAO.cache).forEach(projectName => {
console.log(``);
console.log(`------- Building cache for '${projectName}' -------`);
console.log(`* Fetching language data`);
const langDataStart = new Date().getTime();
const langData = [];
availableLangs.forEach((langRef, index) => {
this.buildKeys({
root: '',
dataNode: JSON.parse(fs.readFileSync(path.join(i18nFileBaseLocation[projectName], `${langRef}.json`)).toString('UTF-8')),
langRef,
langData,
});
});
I18nDAO.cache[projectName].langData = langData;
const langDataEnd = new Date().getTime();
console.log(`-- Done. Took ${langDataEnd - langDataStart}ms`);
console.log(`* Fetching unused keys`);
const unusedKeysStart = new Date().getTime();
I18nDAO.cache[projectName].unused = this.getUnusedKeys({
params: {},
projectAppLocation: projectAppLocation[projectName],
i18nFileBaseLocation: i18nFileBaseLocation[projectName],
});
const unusedKeysEnd = new Date().getTime();
console.log(`-- Done. Took ${unusedKeysEnd - unusedKeysStart}ms`);
console.log(`* Fetching active git branch`);
const gitBranchStart = new Date().getTime();
I18nDAO.cache[projectName].branch = this.getBranch({
projectGitLocation: projectGitLocation[projectName],
});
const gitBranchEnd = new Date().getTime();
console.log(`-- Done. Took ${gitBranchEnd - gitBranchStart}ms`);
});
console.log(``);
console.log(`Cache built successfully`);
}
public buildKeys({ root, dataNode, langRef, langData }) {
if (!dataNode) {
return;
}
if (this.isString(dataNode)) {
const existingKey = langData.find(elem => elem.key === root);
if (existingKey) {
langData.find(elem => elem.key === root).value[langRef] = dataNode;
} else {
const value = {};
value[langRef] = dataNode;
langData.push({ key: root, value });
}
} else {
Object.keys(dataNode).forEach(key => {
const rootKey = root ? `${root}.` : '';
this.buildKeys({ root: `${rootKey}${key}`, dataNode: dataNode[key], langRef, langData });
});
}
}
public getUnusedKeys(req) {
const result = {};
const fileNames = this.fromDir(req.projectAppLocation, /\.(ts|html|js)$/);
const langRefs = req.params.langRef ? [req.params.langRef] : availableLangs;
for (let i = 0; i < langRefs.length; i++) {
result[langRefs[i]] = [];
const i18nFileContent = JSON.parse(fs.readFileSync(path.join(req.i18nFileBaseLocation, `${langRefs[i]}.json`)).toString('UTF-8'));
const objectPaths = this.objectPath(i18nFileContent);
objectPaths.forEach((i18nPath => {
let matched = false;
fileNames.forEach(filename => {
if (matched) {
return;
}
const fileContent = fs.readFileSync(filename).toString('UTF-8');
matched = fileContent.indexOf(i18nPath) > -1;
});
if (!matched) {
result[langRefs[i]].push(i18nPath);
}
}));
}
return result;
}
public getBranch(req) {
const command = `git branch 2> /dev/null | sed -e '/^[^*]/d' -e "s/* \\(.*\\)/\\1/"`;
const child = spawnSync('/bin/sh', [`-c`, command], { cwd: req.projectGitLocation });
return child.stdout.toString().replace('\n', '');
}
public createObjectFromKeys({ data, result }) {
for (const langRef in result) {
if (result.hasOwnProperty(langRef)) {
const obj = {};
data.forEach(elem => {
const val = elem.value[langRef];
const objPath = elem.key.split('.');
objPath.reduce((prevVal, currentVal, index) => {
if (!prevVal[currentVal]) {
prevVal[currentVal] = {};
if (index === objPath.length - 1) {
prevVal[currentVal] = val;
}
}
return prevVal[currentVal];
}, obj);
});
result[langRef] = { ...result[langRef], ...obj };
}
}
}
private fromDir(startPath, filter) {
if (!fs.existsSync(startPath)) {
console.log('no dir ', startPath);
return;
}
let result = [];
const files = fs.readdirSync(startPath);
for (let i = 0; i < files.length; i++) {
const filename = path.join(startPath, files[i]);
const stat = fs.lstatSync(filename);
if (stat.isDirectory()) {
result = result.concat(this.fromDir(filename, filter));
} else if (filter.test(filename)) {
result.push(filename);
}
}
return result;
}
private objectPath(obj, currentPath = '') {
let localCurrentPath = currentPath;
let result = [];
if (localCurrentPath.length) {
localCurrentPath = localCurrentPath + '.';
}
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
if (typeof obj[prop] === 'object') {
result = result.concat(this.objectPath(obj[prop], localCurrentPath + prop));
} else {
result.push(localCurrentPath + prop);
}
}
}
return result;
}
private isString(data) {
return typeof data === 'string';
}
}
const i18nDAO = new I18nDAO();
export { i18nDAO };
import { NextFunction, Request, Response, Router } from 'express';
import * as fs from 'fs';
import * as path from 'path';
import { i18nDAO, I18nDAO } from '../db/i18nDAO';
import { availableLangs, i18nFileBaseLocation, projectAppLocation, projectBaseLocation, projectGitLocation } from '../statistics';
export class I18nApiRouter {
private _router: Router;
get router(): Router {
return this._router;
}
/**
* Initialize the I18nApiRouter
*/
constructor() {
this._router = Router();
this.init();
}
public init(): void {
this._router.param('project', (req: any, res, next, project) => {
if (!project || !i18nFileBaseLocation[project]) {
res.status(500).send({ status: 'STATUS:FAILED', data: 'Invalid Project specified', payload: { project } });
} else {
req.i18nFileBaseLocation = i18nFileBaseLocation[project];
req.projectBaseLocation = projectBaseLocation[project];
req.projectAppLocation = projectAppLocation[project];
req.projectGitLocation = projectGitLocation[project];
req.projectCache = project;
next();
}
});
this._router.get('/', this.getAll);
this._router.get('/:project/langFile', this.getLangFile);
this._router.post('/:project/updateLang', this.updateLang);
}
private getAll(req: Request, res: Response, next: NextFunction): void {
res.json({});
}
private getLangFile(req: any, res: Response, next: NextFunction): void {
const payload = { langData: {}, unused: {}, branch: {} };
if (!I18nDAO.cache[req.projectCache].langData) {
const langData = [];
availableLangs.forEach((langRef, index) => {
i18nDAO.buildKeys({
root: '',
dataNode: JSON.parse(fs.readFileSync(path.join(req.i18nFileBaseLocation, `${langRef}.json`)).toString('UTF-8')),
langRef,
langData,
});
});
I18nDAO.cache[req.projectCache].langData = langData;
}
payload.langData = I18nDAO.cache[req.projectCache].langData;
if (!I18nDAO.cache[req.projectCache].unused) {
I18nDAO.cache[req.projectCache].unused = i18nDAO.getUnusedKeys(req);
}
payload.unused = I18nDAO.cache[req.projectCache].unused;
if (!I18nDAO.cache[req.projectCache].branch) {
I18nDAO.cache[req.projectCache].branch = i18nDAO.getBranch(req);
}
payload.branch = I18nDAO.cache[req.projectCache].branch;
res.send({ status: 'STATUS:SUCCESSFUL', payload });
}
private updateLang(req: any, res: Response, next: NextFunction): void {
if (!req.body.data) {
res.status(500).send({ status: 'STATUS:FAILED', data: 'Invalid Data', payload: { body: req.body } });
return;
}
const result = { en: {}, de: {}, es: {}, fr: {}, it: {} };
const langKeys = Object.keys(result);
i18nDAO.createObjectFromKeys({ data: req.body.data, result });
I18nDAO.cache[req.projectCache].langData = req.body.data;
langKeys.forEach((langRef, index) => {
const fileContent = result[langRef];
const fileLocation = path.join(req.i18nFileBaseLocation, `${langRef}.json`);
const exists = fs.existsSync(fileLocation);
if (!exists) {
res.status(404).send({ status: 'STATUS:FAILED', data: 'File not found', payload: { fileLocation } });
return;
}
fs.writeFileSync(fileLocation, JSON.stringify(fileContent));
if (index === langKeys.length - 1) {
res.send({ status: 'STATUS:SUCCESSFUL' });
}
});
}
}
// Create the I18nApiRouter, and export its configured Express.Router
const i18nApiRoutes = new I18nApiRouter();
const i18nApiRouter = i18nApiRoutes.router;
export { i18nApiRouter };
import {cpus, freemem, loadavg, totalmem, hostname, networkInterfaces} from 'os';
import { cpus, freemem, hostname, loadavg, networkInterfaces, totalmem } from 'os';
import * as path from 'path';
import {QuizManagerDAO} from './db/QuizManagerDAO';
import { QuizManagerDAO } from './db/QuizManagerDAO';
const interfaces = networkInterfaces();
const localAddress = interfaces[Object.keys(interfaces).filter(netIface => {
const singleInterface = interfaces[netIface][0];
return singleInterface.family === 'IPv4' &&
singleInterface.internal === false;
singleInterface.internal === false;
})[0]];
const localIpv4Address = localAddress ? localAddress[0].address : '127.0.0.1';
const portInternal = +process.env.BACKEND_PORT_INTERNAL || 3000;
......@@ -33,7 +33,7 @@ export const dynamicStatistics = () => {
totalmem: totalmem(),
connectedUsers: QuizManagerDAO.getAllActiveMembers(),
activeQuizzes: QuizManagerDAO.getAllActiveQuizNames(),
persistedQuizzes: Object.keys(QuizManagerDAO.getAllPersistedQuizzes()).length
persistedQuizzes: Object.keys(QuizManagerDAO.getAllPersistedQuizzes()).length,
};
};
......@@ -41,8 +41,23 @@ export const settings = {
public: {
cacheQuizAssets: true,
createQuizPasswordRequired: true,
limitActiveQuizzes: Infinity
limitActiveQuizzes: Infinity,
},
limitQuizCreationToCasAccounts: [],
createQuizPassword: 'abc'
createQuizPassword: 'abc',
};
export const cache = { 'arsnova-click-v2-backend': {} };
export const availableLangs = ['en', 'de', 'fr', 'es', 'it'];
export const projectGitLocation = {
'arsnova-click-v2-backend': path.join(__dirname),
};
export const projectBaseLocation = {
'arsnova-click-v2-backend': path.join(projectGitLocation['arsnova-click-v2-backend']),
};
export const projectAppLocation = {
'arsnova-click-v2-backend': path.join(projectBaseLocation['arsnova-click-v2-backend']),
};
export const i18nFileBaseLocation = {
'arsnova-click-v2-backend': path.join(projectBaseLocation['arsnova-click-v2-backend'], 'assets', 'i18n'),
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment