Add i18n api

parent cd8715a4
const options = {
allowedHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'X-Access-Token'],
credentials: true,
methods: 'GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE',
origin: true,
preflightContinue: false
};
module.exports = options;
......@@ -3349,6 +3349,15 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cors": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.4.tgz",
"integrity": "sha1-K9OB8usgECAQXNUOpZ2mMJBpRoY=",
"requires": {
"object-assign": "^4",
"vary": "^1"
}
},
"corser": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
......@@ -9896,8 +9905,7 @@
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"object-component": {
"version": "0.0.3",
......@@ -13750,8 +13758,7 @@
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
"dev": true
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"verror": {
"version": "1.10.0",
......
......@@ -9,7 +9,7 @@
"description": "Version 2 of arsnova.click (Frontend WebApp)",
"scripts": {
"ng": "ng",
"start:SSR": "node dist/server",
"start:SSR": "cd dist && node server",
"start:DEV": "ng serve --host 0.0.0.0 --port 4200 --disable-host-check --aot",
"build:DEV": "ng serve --host 0.0.0.0 --port 4200 --disable-host-check --aot --prod",
"build:SSR": "npm run build:PROD && npm run purify && npm run build:SERVER && npm run webpack:SERVER",
......@@ -59,7 +59,8 @@
"ngx-translate-messageformat-compiler": "^4.1.1",
"rxjs": "^6.1.0",
"ts-loader": "^4.3.0",
"zone.js": "^0.8.26"
"zone.js": "^0.8.26",
"cors": "^2.8.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.6.3",
......
// These are important and needed before anything else
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { enableProdMode } from '@angular/core';
import * as express from 'express';
import * as compress from 'compression';
import { join } from 'path';
// Express Engine
import { ngExpressEngine } from '@nguniversal/express-engine';
// Import module map for lazy loading
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
import * as bodyParser from 'body-parser';
import { spawnSync } from 'child_process';
import * as compress from 'compression';
import * as cors from 'cors';
import * as express from 'express';
import * as fs from 'fs';
import * as path from 'path';
import { join } from 'path';
import 'reflect-metadata';
// These are important and needed before anything else
import 'zone.js/dist/zone-node';
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();
......@@ -20,23 +23,239 @@ enableProdMode();
const app = express();
const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist');
const corsOptions = require('./cors.config.ts');
const cache = { 'arsnova-click-v2-frontend': {} };
const availableLangs = ['en', 'de', 'fr', 'es', 'it'];
const projectGitLocation = {
'arsnova-click-v2-frontend': path.join(__dirname, 'browser'),
};
const projectBaseLocation = {
'arsnova-click-v2-frontend': path.join(projectGitLocation['arsnova-click-v2-frontend']),
};
const projectAppLocation = {
'arsnova-click-v2-frontend': path.join(projectBaseLocation['arsnova-click-v2-frontend']),
};
const i18nFileBaseLocation = {
'arsnova-click-v2-frontend': path.join(projectBaseLocation['arsnova-click-v2-frontend'], 'assets', 'i18n'),
};
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use(cors(corsOptions));
app.use(compress());
app.param('project', (req, 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();
}
});
app.options('*', cors(corsOptions));
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { RootServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main');
const 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(fromDir(filename, filter));
} else if (filter.test(filename)) {
result.push(filename);
}
}
return result;
};
const 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(objectPath(obj[prop], localCurrentPath + prop));
} else {
result.push(localCurrentPath + prop);
}
}
}
return result;
};
const isString = (data) => {
return typeof data === 'string';
};
const buildKeys = ({ root, dataNode, langRef, langData }) => {
if (!dataNode) {
return;
}
if (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}.` : '';
buildKeys({ root: `${rootKey}${key}`, dataNode: dataNode[key], langRef, langData });
});
}
};
const 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 };
}
}
};
const getUnusedKeys = (req) => {
const result = {};
const fileNames = 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 = 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;
};
const 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', '');
};
app.engine('html', ngExpressEngine({
bootstrap: RootServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
provideModuleMap(LAZY_MODULE_MAP),
],
}));
app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));
// TODO: implement data requests securely
app.get('/api/v1/plugin/i18nator/:project/langFile', async (req, res) => {
const payload = { langData: {}, unused: {}, branch: {} };
if (!cache[req.projectCache].langData) {
const langData = [];
availableLangs.forEach((langRef, index) => {
buildKeys({
root: '',
dataNode: JSON.parse(fs.readFileSync(path.join(req.i18nFileBaseLocation, `${langRef}.json`)).toString('UTF-8')),
langRef,
langData,
});
});
cache[req.projectCache].langData = langData;
}
payload.langData = cache[req.projectCache].langData;
if (!cache[req.projectCache].unused) {
cache[req.projectCache].unused = getUnusedKeys(req);
}
payload.unused = cache[req.projectCache].unused;
if (!cache[req.projectCache].branch) {
cache[req.projectCache].branch = getBranch(req);
}
payload.branch = cache[req.projectCache].branch;
res.send({ status: 'STATUS:SUCCESSFUL', payload });
});
app.post('/api/v1/plugin/i18nator/:project/updateLang', async (req, res) => {
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);
createObjectFromKeys({ data: req.body.data, result });
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' });
}
});
});
app.get('/api/*', (req, res) => {
res.status(404).send('data requests are not supported');
});
......@@ -52,4 +271,45 @@ app.get('*', (req, res) => {
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
Object.keys(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) => {
buildKeys({
root: '',
dataNode: JSON.parse(fs.readFileSync(path.join(i18nFileBaseLocation[projectName], `${langRef}.json`)).toString('UTF-8')),
langRef,
langData,
});
});
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();
cache[projectName].unused = 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();
cache[projectName].branch = getBranch({
projectGitLocation: projectGitLocation[projectName],
});
const gitBranchEnd = new Date().getTime();
console.log(`-- Done. Took ${gitBranchEnd - gitBranchStart}ms`);
});
console.log(``);
console.log(`Cache built successfully`);
});
......@@ -2,9 +2,13 @@ const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: { server: './server.ts' },
resolve: { extensions: ['.js', '.ts'] },
entry: {server: './server.ts'},
resolve: {extensions: ['.js', '.ts']},
target: 'node',
node: {
__dirname: false,
__filename: false,
},
mode: 'none',
// this makes sure we include node_modules and other 3rd party libraries
externals: [/node_modules/],
......@@ -13,7 +17,7 @@ module.exports = {
filename: '[name].js'
},
module: {
rules: [{ test: /\.ts$/, loader: 'ts-loader' }]
rules: [{test: /\.ts$/, loader: 'ts-loader'}]
},
plugins: [
// Temporary Fix for issue: https://github.com/angular/angular/issues/11580
......
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