diff --git a/backend/package.json b/backend/package.json
index 0439a379..3faeabe5 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -12,6 +12,9 @@
},
"dependencies": {
"express": "^4.14.0",
+ "locale": "^0.1.0",
+ "lodash.template": "^4.4.0",
+ "lodash.uniq": "^4.5.0",
"st": "^1.2.0"
},
"devDependencies": {
diff --git a/backend/src/index.html b/backend/src/index.html
new file mode 100644
index 00000000..0e4aa3aa
--- /dev/null
+++ b/backend/src/index.html
@@ -0,0 +1,12 @@
+
+
+
+ Joyent Triton Dashboard
+
+
+
+
+
+
+
+
diff --git a/backend/src/index.js b/backend/src/index.js
index d4f73e28..3cace565 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -1,13 +1,19 @@
+const template = require('lodash.template');
+const locale = require('locale');
const path = require('path');
const express = require('express');
const st = require('st');
+const fs = require('fs');
const app = express();
+const index = path.join(__dirname, './index.html');
+const html = template(fs.readFileSync(index, 'utf-8'));
+
var mount = st({
path: path.join(__dirname, '../static'),
url: 'static/',
- index: 'index.html',
+ index: false,
dot: false,
passthrough: false,
gzip: true,
@@ -15,11 +21,18 @@ var mount = st({
});
app.use(mount);
+app.use(locale(require('./locales')));
app.get('/*', (req, res, next) => {
- mount(Object.assign(req, {
- sturl: '/static/index.html'
- }), res, next);
+ const locale = (req.locale || '').toLowerCase();
+ const lang = locale.split(/\-/)[0];
+
+ res.header('Content-Type', 'text/html');
+
+ res.send(html({
+ locale,
+ lang
+ }));
});
app.listen(8000, (err, address) => {
diff --git a/backend/src/locales.js b/backend/src/locales.js
new file mode 100644
index 00000000..d75eee7e
--- /dev/null
+++ b/backend/src/locales.js
@@ -0,0 +1,11 @@
+const uniq = require('lodash.uniq');
+const fs = require('fs');
+const path = require('path');
+
+const files = fs.readdirSync(path.join(__dirname, '../static/locales'));
+
+module.exports = uniq(files.map((file) => {
+ return file.replace(/\.js$/, '');
+}).filter((file) => {
+ return file.match(/.*?-.*?/);
+}));
diff --git a/backend/static/.gitignore b/backend/static/.gitignore
new file mode 100644
index 00000000..d6b7ef32
--- /dev/null
+++ b/backend/static/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/frontend/README.md b/frontend/README.md
index f1cf7b5d..4e233a73 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -26,6 +26,8 @@ npm run test
│ ├── store.js
│ └── thunks
├── static
+├── locales
+├── scripts
├── test
├── webpack
├── .babelrc
@@ -38,6 +40,8 @@ npm run test
- **src/state/actions.js**: Not only exports all the actions available (declared in the file), but also goes through all the thunks and exports them.
- **src/state/thunks**: Directory to place thunks so that actions or reducers don't get too confusing.
- **src/state/reducers**: Each file here represents a reducer scope. So, `state.app` will be controlled in `reducers/app.js`.
+ - **locales**: Translation definitions for each locale supported.
+ - **scripts**: Utility scripts (e.g. building localizations).
- **test**: Self explanatory.
- **webpack**: Webpack configuration for multiple enviroments. Development configuration includes a dev-server and hot module replacement support.
- **.babelrc**: This babel configuration outputs ES2015 code, so it will produce code only for modern browsers.
diff --git a/frontend/locales/en-us.json b/frontend/locales/en-us.json
new file mode 100644
index 00000000..8f8d7125
--- /dev/null
+++ b/frontend/locales/en-us.json
@@ -0,0 +1,3 @@
+{
+ "greetings": "Hello"
+}
\ No newline at end of file
diff --git a/frontend/locales/pt-pt.json b/frontend/locales/pt-pt.json
new file mode 100644
index 00000000..1eb32ea0
--- /dev/null
+++ b/frontend/locales/pt-pt.json
@@ -0,0 +1,3 @@
+{
+ "greetings": "Olá"
+}
\ No newline at end of file
diff --git a/frontend/package.json b/frontend/package.json
index 1182e4b2..d59a5bf7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -8,9 +8,11 @@
"lint": "eslint .",
"test": "NODE_ENV=test nyc ava test/*.js --verbose",
"open": "nyc report --reporter=html & open coverage/index.html",
- "coverage": "nyc check-coverage --statements 100 --functions 100 --lines 100 --branches 100"
+ "coverage": "nyc check-coverage --statements 100 --functions 100 --lines 100 --branches 100",
+ "build-locales": "NODE_ENV=test babel-node scripts/build-locales"
},
"dependencies": {
+ "babel-cli": "^6.16.0",
"babel-core": "^6.17.0",
"babel-loader": "^6.2.5",
"babel-plugin-add-module-exports": "^0.2.1",
@@ -21,9 +23,13 @@
"constant-case": "^2.0.0",
"fast-async": "^6.1.1",
"json-loader": "^0.5.4",
+ "ncp": "^2.0.0",
+ "querystring": "^0.2.0",
"react": "^15.3.2",
"react-dom": "^15.3.2",
"react-hot-loader": "^3.0.0-beta.6",
+ "react-intl": "^2.1.5",
+ "react-intl-redux": "^0.1.0",
"react-redux": "^4.4.5",
"react-router": "^4.0.0-alpha.4",
"reduce-reducers": "^0.1.2",
@@ -33,7 +39,9 @@
"redux-logger": "^2.7.0",
"redux-promise-middleware": "^4.1.0",
"redux-thunk": "^2.1.0",
- "webpack": "^2.1.0-beta.25"
+ "thenify": "^3.2.1",
+ "webpack": "^2.1.0-beta.25",
+ "webpack-shell-plugin": "^0.4.3"
},
"devDependencies": {
"ava": "^0.16.0",
@@ -52,7 +60,8 @@
"nyc": "^8.3.1",
"pre-commit": "^1.1.3",
"react-addons-test-utils": "^15.3.2",
- "webpack-dev-server": "^1.16.2"
+ "webpack-dev-server": "^1.16.2",
+ "webpack-shell-plugin": "^0.4.3"
},
"ava": {
"failFast": true,
diff --git a/frontend/scripts/build-locales.js b/frontend/scripts/build-locales.js
new file mode 100644
index 00000000..8ca37b7b
--- /dev/null
+++ b/frontend/scripts/build-locales.js
@@ -0,0 +1,74 @@
+const path = require('path');
+const thenify = require('thenify');
+const fs = require('fs');
+const Ncp = require('ncp');
+
+const readdir = thenify(fs.readdir);
+const writeFile = thenify(fs.writeFile);
+const readFile = thenify(fs.readFile);
+const ncp = thenify(Ncp.ncp);
+
+const root = path.join(__dirname, '../locales');
+const sttic = path.join(__dirname, '../static/locales');
+const intl = path.join(__dirname, '../node_modules/react-intl/locale-data');
+
+const source = ({
+ name,
+ json
+}) => `
+ (() => {
+ const Locales = window.Locales || {};
+ Locales['${name}'] = ${json};
+ window.Locales = Locales;
+ })();
+`;
+
+const compile = async () => {
+ const files = await readdir(root);
+ const jsons = files.filter(filename => path.extname(filename) === '.json');
+
+ const locales = files.reduce((res, filename) => {
+ const name = path.parse(filename).name;
+ const json = JSON.stringify(require(path.join(root, filename)));
+ const lang = name.split(/\-/)[0];
+
+ return {
+ ...res,
+ [name]: {
+ lang,
+ json,
+ filename
+ }
+ };
+ }, {});
+
+ await Promise.all(Object.keys(locales).map((name) => {
+ console.log(`Copying locale-data for ${name}`);
+
+ const locale = locales[name];
+ const source = path.join(intl, `${locale.lang}.js`);
+ const destination = path.join(sttic, `${locale.lang}.js`);
+
+ return ncp(source, destination);
+ }));
+
+
+
+ return await Promise.all(Object.keys(locales).map((name) => {
+ console.log(`Writing ${name}.js`);
+
+ const locale = locales[name];
+
+ return writeFile(path.join(sttic, `${name}.js`), source({
+ ...locale,
+ name
+ }));
+ }));
+};
+
+console.log('Building Locales');
+compile().then(() => {
+ console.log('Locales Built');
+}, (err) => {
+ throw err;
+});
diff --git a/frontend/src/containers/app.js b/frontend/src/containers/app.js
index b7600a42..d64b7f03 100644
--- a/frontend/src/containers/app.js
+++ b/frontend/src/containers/app.js
@@ -28,6 +28,7 @@ const App = connect()(React.createClass({
// ugly hack needed because of a limitation of react-router api
// that doens't pass it's instance to matched routes
+ // wait for react-router-redux@5
dispatch(updateRouter(router));
},
render: function() {
diff --git a/frontend/src/containers/home.js b/frontend/src/containers/home.js
index db54116e..6677effd 100644
--- a/frontend/src/containers/home.js
+++ b/frontend/src/containers/home.js
@@ -1,9 +1,16 @@
const React = require('react');
+const ReactIntl = require('react-intl');
+
+const {
+ FormattedMessage
+} = ReactIntl;
module.exports = () => {
return (
-
Home
+
+
+
);
};
diff --git a/frontend/src/intl.js b/frontend/src/intl.js
new file mode 100644
index 00000000..c771ce54
--- /dev/null
+++ b/frontend/src/intl.js
@@ -0,0 +1,38 @@
+const qs = require('querystring');
+const ReactIntl = require('react-intl');
+
+const {
+ addLocaleData
+} = ReactIntl;
+
+module.exports = (({
+ Locales = {},
+ ReactIntlLocaleData = {}
+}) => {
+ const en = Locales['en-us'] || {};
+
+ Object.keys(ReactIntlLocaleData).forEach((lang) => {
+ addLocaleData(ReactIntlLocaleData[lang] || []);
+ });
+
+ // http://stackoverflow.com/a/38150585
+ const detectedLocale = (
+ qs.parse((document.location.search || '').replace(/^\?/, '')).locale ||
+ navigator.languages && navigator.languages[0] || // Chrome / Firefox
+ navigator.language || // All browsers
+ navigator.userLanguage || // IE <= 10
+ 'en-US'
+ ).toLowerCase();
+
+ const lang = detectedLocale.split(/\-/)[0];
+ const locale = ReactIntlLocaleData[lang] ?
+ (Locales[detectedLocale] ? detectedLocale : 'en-us') : 'en-us';
+
+ return {
+ locale,
+ messages: {
+ ...en,
+ ...(Locales[locale] || {})
+ }
+ };
+})(window);
diff --git a/frontend/src/root.js b/frontend/src/root.js
index 78fc1998..db12152c 100644
--- a/frontend/src/root.js
+++ b/frontend/src/root.js
@@ -1,4 +1,6 @@
+const qs = require('querystring');
const React = require('react');
+const ReactIntlRedux = require('react-intl-redux');
const ReactHotLoader = require('react-hot-loader');
const ReactRedux = require('react-redux');
const ReactRouter = require('react-router');
@@ -6,6 +8,10 @@ const ReactRouter = require('react-router');
const App = require('./containers/app');
const store = require('./state/store');
+const {
+ IntlProvider
+} = ReactIntlRedux;
+
const {
AppContainer
} = ReactHotLoader;
@@ -22,9 +28,11 @@ module.exports = () => {
return (
-
- {App}
-
+
+
+ {App}
+
+
);
diff --git a/frontend/src/state/reducers/index.js b/frontend/src/state/reducers/index.js
index 935f2b24..5c9bd0ec 100644
--- a/frontend/src/state/reducers/index.js
+++ b/frontend/src/state/reducers/index.js
@@ -6,6 +6,7 @@ const {
module.exports = () => {
return combineReducers({
- app: require('./app.js')
+ app: require('./app.js'),
+ intl: require('./intl.js')
});
};
diff --git a/frontend/src/state/reducers/intl.js b/frontend/src/state/reducers/intl.js
new file mode 100644
index 00000000..479a5936
--- /dev/null
+++ b/frontend/src/state/reducers/intl.js
@@ -0,0 +1,7 @@
+const ReduxActions = require('redux-actions');
+
+const {
+ handleActions
+} = ReduxActions;
+
+module.exports = handleActions({}, require('../../intl'));
diff --git a/frontend/static/.gitignore b/frontend/static/.gitignore
new file mode 100644
index 00000000..d6b7ef32
--- /dev/null
+++ b/frontend/static/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/frontend/static/.gitkeep b/frontend/static/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/frontend/static/index.html b/frontend/static/index.html
index d36d2c24..c831e18e 100644
--- a/frontend/static/index.html
+++ b/frontend/static/index.html
@@ -5,6 +5,10 @@
+
+
+
+