diff --git a/.babelrc b/.babelrc index 0578679..5fbb85d 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,8 @@ { - "presets": ["react", "es2015"] + "presets": ["react", "es2015"], + "plugins": [ + ["react-intl", { + "messagesDir": "./build/messages/" + }] + ] } diff --git a/.gitignore b/.gitignore index 5a0aa70..482e0fa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules dist *.css *.log +build diff --git a/README.md b/README.md index be0cbd0..163fa86 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,32 @@ Generate the app: `cd tpa-demo-app-react-jsx/` 3. Install npm `npm install` -4. Run webpack -`Webpack` +4. Run build +`npm run build` 5. Run the app `npm start` - This will run a webserver on your local host. You can now view your app in your browser: http://localhost:3000/ +###**Internationalisation** +The entire app supports dates formating, numbers, and strings, including pluralization and handling translations, using [react-intl](https://github.com/yahoo/react-intl) +#### Adding new locale +Add comma separated locales to environment variable `WIX_TPA_LOCALES` +Example: +* for development `WIX_TPA_LOCALES=en,ru npm run start` +* for deploying `WIX_TPA_LOCALES=en,ru npm run build` + +There is no need to do any importing or preloading, it is served automatically + +#### Translating messages (strings) +Each app (`src/settings` and `src/widget`) has `lang` dir inside. +`en.json` will be automatically added to this directory (task: build:langs) +1. Copy and rename `en.json` to desired locale name e.g. `ru.json` +2. Translate content of `ru.json` +See `src/settings/lang/README.md` for messages syntax + +#### Language detection +Currently language is defined by browser (first value of `navigator.languages`) + ###**Registering the App** 1. Follow the registration guide to create a new app. diff --git a/package.json b/package.json index 494e5c8..b3f675f 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,40 @@ { "name": "tpa-demo-app-react-jsx", "scripts": { - "start": "node server.js" + "start": "./node_modules/.bin/cross-env node server.js", + "build:run": "./node_modules/.bin/cross-env ./node_modules/.bin/webpack", + "build:langs": "./node_modules/.bin/cross-env ./node_modules/.bin/babel-node scripts/translate.js", + "build": "npm run build:langs && npm run build:run" }, - "version": "0.0.1", + "version": "0.1.2", "dependencies": { + "async": "^2.1.5", + "intl": "^1.2.5", "react": "^15.3.0", - "react-dom": "^15.3.0" + "react-dom": "^15.3.0", + "react-intl": "^2.2.3" }, "devDependencies": { + "babel-cli": "^6.23.0", "babel-core": "^6.3.26", "babel-loader": "^6.2.0", + "babel-plugin-react-intl": "^2.3.1", "babel-preset-es2015": "^6.5.0", "babel-preset-react": "^6.5.0", + "bundle-loader": "^0.5.5", + "cross-env": "^3.1.4", "css-loader": "^0.23.1", + "file-loader": "^0.10.1", + "glob": "^7.1.1", + "json-loader": "^0.5.4", + "mkdirp": "^0.5.1", "node-sass": "^3.4.2", + "react-hot-loader": "^1.3.0", + "rimraf": "^2.6.1", "sass-loader": "^3.2.0", "script-loader": "^0.6.1", "style-loader": "^0.13.0", "webpack": "^1.12.14", - "webpack-dev-server": "^1.14.1", - "react-hot-loader": "^1.3.0" + "webpack-dev-server": "^1.14.1" } } diff --git a/scripts/bundle-loader-i18n.js b/scripts/bundle-loader-i18n.js new file mode 100644 index 0000000..71a4a72 --- /dev/null +++ b/scripts/bundle-loader-i18n.js @@ -0,0 +1,91 @@ +import waterfall from 'async/waterfall'; +import {addLocaleData} from 'react-intl'; + +export default function (resultCallback, appName) { + const + fbLocale = 'en', + locale = (navigator.languages && navigator.languages[0]) || + navigator.language || + navigator.userLanguage || + fbLocale; + + waterfall([ + function (callback) { + if (!window.Intl) { + require(`bundle?lazy&name=intl.pf!intl`)((file) => { + callback(null, true); + }); + } else { + callback(null, false); + } + }, + function (oldBrowser, callback) { + if (oldBrowser) { + try { + require(`bundle?lazy&name=[name].locale.pf!intl/locale-data/jsonp/${locale}`)((file) => { + callback(null, oldBrowser); + }); + } catch (err) { + console.error(`Can not find polyfill for locale '${locale}'. Fallback to locale ${fbLocale}`); + + require(`bundle?lazy&name=[name].locale.pf!intl/locale-data/jsonp/${fbLocale}`)((file) => { + callback(null, oldBrowser); + }); + } + } else { + callback(null, oldBrowser); + } + }, + function (oldBrowser, callback) { + if (oldBrowser) { + callback(null, oldBrowser); + } else { + try { + require(`bundle?lazy&name=[name].locale!react-intl/locale-data/${locale.substring(0, 2)}`)((file) => { + addLocaleData(file); + callback(null, oldBrowser); + }); + } catch (err) { + console.error(`Can not find any data for locale '${locale}'. Fallback to locale ${fbLocale}`); + + require(`bundle?lazy&name=[name].locale!react-intl/locale-data/${fbLocale}`)((file) => { + addLocaleData(file); + callback(null, oldBrowser); + }); + } + } + }, function (oldBrowser, callback) { + try { + // no way to substitute string - it should be known outside of a runtime + if (appName == 'settings') { + require(`bundle?lazy&name=[name].settings!../src/settings/lang/${locale}.json`)((file) => { + callback(null, {locale: locale, data: file}); + }); + } else if (appName == 'widget') { + require(`bundle?lazy&name=[name].widget!../src/widget/lang/${locale}.json`)((file) => { + callback(null, {locale: locale, data: file}); + }); + } + + } catch (err) { + console.error(`Can not load translated messages for locale '${locale}'. Check existence in 'lang' directory. Fallback to default strings of locale: ${fbLocale}`); + + // no way to substitute string - it should be known outside of a runtime + if (appName == 'settings') { + require(`bundle?lazy&name=[name].settings!../src/settings/lang/${fbLocale}.json`)((file) => { + callback(null, {locale: fbLocale, data: file}); + }); + } else if (appName == 'widget') { + require(`bundle?lazy&name=[name].widget!../src/widget/lang/${fbLocale}.json`)((file) => { + callback(null, {locale: fbLocale, data: file}); + }); + } + + } + } + ], function (err, result) { + if (err) throw new Error('Locale initialization problem.'); + + resultCallback(result.locale, result.data); + }); +} diff --git a/scripts/translate.js b/scripts/translate.js new file mode 100644 index 0000000..7170534 --- /dev/null +++ b/scripts/translate.js @@ -0,0 +1,46 @@ +import * as fs from 'fs'; +import path from 'path'; +import {sync as globSync} from 'glob'; +import {sync as mkdirpSync} from 'mkdirp'; + +const SRC_MESSAGES_BASE = './build/messages/src'; +const SRC_MESSAGES_PATTERN = SRC_MESSAGES_BASE + '/**/*.json'; +const DST_MESSAGES_BASE = './src/'; +const LANG_DIR_NAME = 'lang'; + +let defaultMessages = globSync(SRC_MESSAGES_PATTERN) + .map((filename) => { + return { + subDir: path.relative(SRC_MESSAGES_BASE, filename).split('/').shift(), + filename: filename + }; + }) + .map((item) => { + return { + subDir: item.subDir, + content: fs.readFileSync(item.filename, 'utf8') + }; + }) + .map((file) => { + return JSON.parse(file.content).map( + (item) => Object.assign({subDir: file.subDir}, item) + ); + }) + .reduce((collection, descriptor) => { + descriptor.forEach(({id, defaultMessage, subDir}) => { + if (collection[subDir] && collection[subDir].hasOwnProperty(id)) { + throw new Error(`Duplicate message id: ${id}`); + } + if (!collection[subDir]) collection[subDir] = {}; + collection[subDir][id] = defaultMessage; + }); + + return collection; + }, {}); + +Object.keys(defaultMessages).map(function (subDir) { + let destDir = DST_MESSAGES_BASE + subDir + '/' + LANG_DIR_NAME; + mkdirpSync(destDir); + fs.writeFileSync(destDir + '/en.json', + JSON.stringify(defaultMessages[subDir], null, 2)); +}); diff --git a/settings.html b/settings.html index b679b7a..73f4936 100644 --- a/settings.html +++ b/settings.html @@ -13,6 +13,6 @@
- +