Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
{
"presets": ["react", "es2015"]
"presets": ["react", "es2015"],
"plugins": [
["react-intl", {
"messagesDir": "./build/messages/"
}]
]
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules
dist
*.css
*.log
build
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 20 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
91 changes: 91 additions & 0 deletions scripts/bundle-loader-i18n.js
Original file line number Diff line number Diff line change
@@ -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);
});
}
46 changes: 46 additions & 0 deletions scripts/translate.js
Original file line number Diff line number Diff line change
@@ -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));
});
2 changes: 1 addition & 1 deletion settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@

<body>
<div id="root" class="root"></div>
<script src="dist/settings.js"></script>
<script src="settings.js"></script>
</body>
</html>
43 changes: 24 additions & 19 deletions src/settings/App.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import React from 'react'
import { render } from 'react-dom';
import Wix from 'Wix';
import settingsApp from './settings'
import $ from 'jquery';
import ReactDOM from 'react-dom';
import bundleLoaderI18n from '../../scripts/bundle-loader-i18n';
import {IntlProvider} from 'react-intl';
import App from './settings'
import './settings.scss';

var rootInstance = null;
$(document).ready(function () {
rootInstance = render(
React.createElement(settingsApp),
$('#root').get(0)
);
let rootInstance;

if (module.hot) {
require('react-hot-loader/Injection').RootInstanceProvider.injectProvider({
getRootInstances: function () {
// Help React Hot Loader figure out the root component instances on the page:
return [rootInstance];
}
});
}
});
bundleLoaderI18n(renderRootComponent, 'settings');

function renderRootComponent(locale, messages) {
rootInstance = ReactDOM.render(
<IntlProvider locale={locale} messages={messages}>
<App/>
</IntlProvider>,
document.getElementById('root')
);
}

if (module.hot) {
module.hot.accept();
require('react-hot-loader/Injection').RootInstanceProvider.injectProvider({
getRootInstances: function () {
// Help React Hot Loader figure out the root component instances on the page:
return [rootInstance];
}
});
}
25 changes: 25 additions & 0 deletions src/settings/lang/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
`en.json` will be automatically added to this directory (task: build:langs)

### To support more languages
1. Copy and rename `en.json` to desired locale name e.g. `ru.json`
2. Translate content of `ru.json`

##### Message Syntax

String/Message formatting is a paramount feature of React Intl and it builds on [ICU Message Formatting](http://userguide.icu-project.org/formatparse/messages) by using the [ICU Message Syntax](http://formatjs.io/guides/message-syntax/). This message syntax allows for simple to complex messages to defined, translated, and then formatted at runtime.

**Simple Message:**
```
Hello, {name}
```

**Complex Message:**
```
Hello, {name}, you have {itemCount, plural,
=0 {no items}
one {# item}
other {# items}
}.
```

**See:** The [Message Syntax Guide](http://formatjs.io/guides/message-syntax/) on the [FormatJS website](http://formatjs.io/).
5 changes: 5 additions & 0 deletions src/settings/lang/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"settings.main.intro": "This is the Wix App settings demo.",
"settings.main.introDesc": "Please add a short description of your App + CTA for the main action.",
"settings.support.enjoy": "Have you enjoyed the app? Spread the word and rate us in the app market"
}
File renamed without changes
21 changes: 15 additions & 6 deletions src/settings/modules/main/main.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import UI from 'editor-ui-lib';
import React from 'react'
import React from 'react';
import { FormattedMessage } from 'react-intl';

export default class Main extends React.Component {
onClick () {
Expand All @@ -9,14 +10,22 @@ export default class Main extends React.Component {
render () {
return (
<div className="main-tab">
<img className="app-logo" src="logo.svg" alt="app logo"/>
<img className="app-logo" src={require('./logo.svg')} alt="app logo"/>
<p className="app-description">
This is the Wix App settings demo.
<br/>
Please add a short description of your App + CTA for the main action.
<FormattedMessage
id="settings.main.intro"
description="Explanation about setting app"
defaultMessage="This is the Wix App settings demo."
/>
<br/>
<FormattedMessage
id="settings.main.introDesc"
description="Explanation about setting app - desc"
defaultMessage="Please add a short description of your App + CTA for the main action."
/>
</p>
<UI.button className="btn-confirm-primary" label="Main CTA" onClick={()=>this.onClick()}/>
</div>
)
}
}
}
9 changes: 8 additions & 1 deletion src/settings/modules/support/support.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FormattedMessage } from 'react-intl';

export default class Support extends React.Component {
constructor (props) {
super(props);
Expand Down Expand Up @@ -53,7 +55,12 @@ export default class Support extends React.Component {
<UI.sectionDividerLabeled label="Review the app" />
<hr className="divider-long"/>

<p className="review-paragraph">Have you enjoyed the app? Spread the word and rate us in the app market</p>
<p className="review-paragraph">
<FormattedMessage
id="settings.support.enjoy"
defaultMessage="Have you enjoyed the app? Spread the word and rate us in the app market"
/>
</p>
<div className="button-wrapper-center">
<UI.button
className="btn-confirm-primary"
Expand Down
Loading