Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I18n for static strings in UI #403

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from 11 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
23 changes: 23 additions & 0 deletions docs/contributor-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,26 @@ While we work on building this page (and you can help!), here are some links wit
* [Project Roadmap](https://github.com/netlify/netlify-cms/projects/3)
* [Code of Conduct](https://github.com/netlify/netlify-cms/blob/master/CODE_OF_CONDUCT.md)
* [Setup instructions and Contribution Guidelines](https://github.com/netlify/netlify-cms/blob/master/CONTRIBUTING.md)


## I18N

I18N strings are managed by [polyglot](http://airbnb.io/polyglot.js/).
Strings are extracted from source code with static analysis by [i18n-extract](https://github.com/oliviertassinari/i18n-extract).
Code is searched for regex /polyglot.t('[^']')/ so if you want particular string to be translated,
you have to follow this rule.
For more info see [polyglot](http://airbnb.io/polyglot.js/) or [example](../src/components/FindBar/FindBar.js)

Extractions shall be extracted manually extra for each language.
By one extraction run, one file (JSON) is produced (or updated if it already exists).
to extract/update i18n strings run:

```
npm run extract --lang <code of your language>
```

E.g. czech language strings (international code cs) run:

```
npm run extract --lang cs
```
4 changes: 4 additions & 0 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<title>This is an example</title>

<link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500' rel='stylesheet' type='text/css'>
<script src="https://rawgit.com/airbnb/polyglot.js/master/build/polyglot.js" type="text/javascript"></script>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an NPM dependency (https://www.npmjs.com/package/node-polyglot), not a script tag referencing an external resource.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this project uses npm for dependency management.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polyglot is NODE library. There are builds for frontend that ARE loaded as html scripts like I have done. If you install it as npm depend, it will give you error because it rely on node fs module.
Other argument is that not everything should be bundled. For instance React, lodash and some more libs can (and actually should) be externalized to make to bundle size smaller ...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vencax it isn't just about bundle size, it's about automated dependency management. This project uses npm for that. Webpack should handle importing polyglot just fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know what you mean, but the version installed by npm is NOT frontend version of polyglot. Try it, and you'll see ...

<link rel="stylesheet" href="/cms.css"/>
<script>
window.repoFiles = {
Expand Down Expand Up @@ -76,6 +77,9 @@
</head>
<body>

<script>
var polyglot = new Polyglot();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go in the CMS source, not as a script tag. We also shouldn't be using globals except to provide a hook point for end users (i.e., not for internal CMS stuff).

</script>
<script src='/cms.js'></script>
<script>
var PostPreview = createClass({
Expand Down
59 changes: 59 additions & 0 deletions extract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const i18n = require('i18n-extract');
Copy link
Contributor

@Benaiah Benaiah Jun 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file should be moved to scripts, set up to use webpack (similarly to scripts/autoconfigure.collection.js), and these require calls should be changed to import ... from....

const path = require('path');
const fs = require('fs');
const _ = require('lodash').mixin(require('lodash-keyarrange'))
const i18nFolder = path.join(__dirname, 'src/i18n');

const keys = i18n.extractFromFiles([
'src/**/*.js',
], {
marker: 'polyglot.t',
});

function mergeMissing(translations, missing, makeVal) {
_.each(missing, (i) => {
if (translations[i.key] === undefined) {
translations[i.key] = makeVal(i);
}
});
}

function writeTranslations(file, translations) {
const sorted = _.keyArrange(translations)
fs.writeFileSync(file, JSON.stringify(sorted, {}, 2));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use asynchronous IO actions, and be sure to handle errors when doing IO. Take a look at util.promisify (we may need a polyfill for older Node versions), which lets you use a promise-based interface to the Node stdlib.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it works without async IO, so no need to waste time with syntactic sugar, sorry :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not talking about asynchronous/await syntax sugar, I'm talking about using fs.writeFile instead of fs.writeFileSync.

}

// check english trans first, they MUST be complete !!!
const enFile = 'en.json';
const enFilePath = path.join(i18nFolder, enFile);
const enTransls = JSON.parse(fs.readFileSync(enFilePath));
Copy link
Contributor

@Benaiah Benaiah Jun 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use asynchronous IO functions, and be sure to handle errors when doing IO. Don't abbreviate translations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the same as above


const enMissing = i18n.findMissing(enTransls, keys);
if (enMissing.length > 0) {
mergeMissing(enTransls, enMissing, (i) => i.type);
writeTranslations(enFilePath, enTransls);
console.log('Some errors in english translation. Correct them:');
console.log(JSON.stringify(enMissing, {}, 2));
return -1;
}

const errors = []

fs.readdirSync(i18nFolder).forEach(file => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use asynchronous IO functions, and be sure to handle errors when doing IO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the same as above

if (file === enFile) {
return
}
const filePath = path.join(i18nFolder, file);
const translations = JSON.parse(fs.readFileSync(filePath));

const missing = i18n.findMissing(translations, keys);
if (missing.length > 0) {
mergeMissing(translations, missing, (i) => enTransls[i.key])
writeTranslations(filePath, translations);
errors.push({file: file, missing: missing})
}
});

if (errors.length > 0) {
console.log(JSON.stringify(errors, {}, 2));
}
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"lint:css:fix": "stylefmt --recursive src/",
"lint:staged": "lint-staged",
"deps": "npm-check -s",
"deps:update": "npm-check -u"
"deps:update": "npm-check -u",
"extract": "node extract.js"
},
"lint-staged": {
"*.js": [
Expand Down Expand Up @@ -66,10 +67,13 @@
"exports-loader": "^0.6.3",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.8.5",
"i18n-extract": "^0.4.2",
"identity-obj-proxy": "^3.0.0",
"imports-loader": "^0.6.5",
"jest-cli": "^16.0.1",
"lint-staged": "^3.1.0",
"lodash": "^4.17.4",
"lodash-keyarrange": "^1.1.0",
"node-sass": "^3.10.0",
"npm-check": "^5.2.3",
"postcss-cssnext": "^2.7.0",
Expand Down
23 changes: 23 additions & 0 deletions src/actions/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const CONFIG_FAILURE = "CONFIG_FAILURE";

const defaults = {
publish_mode: publishModes.SIMPLE,
lang: 'en'
};

export function applyDefaults(config) {
Expand Down Expand Up @@ -82,6 +83,27 @@ export function configDidLoad(config) {
};
}

const DEFAULT_I18N_URL = 'https://raw.githubusercontent.com/netlify/netlify-cms/master/src';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned in another comment, we can allow translation objects to be loaded in through the registry and forego any fetching while allowing translation to be independent of cms releases. I believe that would address concerns on both sides, let me know if you think otherwise.

The registry methods are added to the window object, source is here: https://github.com/netlify/netlify-cms/blob/1d08f1a33b9562e60145dc9704eb6379f64df044/src/lib/registry.js


function loadTranslations(config) {
const URL_BASE = config.default_i18n_urlbase || DEFAULT_I18N_URL;
const translationsURL = `${URL_BASE}/i18n/${config.lang}.json`;
return fetch(translationsURL)
.then((res) => {
if (res.status === 404) {
// try fallback en
console.error(`Translations for lang: ${config.lang} not exist, defaulting to english`);
config.lang = 'en';
return fetch(`${URL_BASE}/i18n/en.json`).then(res => res.json());
}
return res.json();
})
.then(transls => {
polyglot.replace(transls);
return config;
});
}

export function loadConfig() {
if (window.CMS_CONFIG) {
return configDidLoad(window.CMS_CONFIG);
Expand All @@ -99,6 +121,7 @@ export function loadConfig() {
.then(parseConfig)
.then(validateConfig)
.then(applyDefaults)
.then(loadTranslations)
.then((config) => {
dispatch(configDidLoad(config));
dispatch(authenticateUser());
Expand Down
8 changes: 5 additions & 3 deletions src/backends/test-repo/AuthenticationPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Card, Icon } from "../../components/UI";
import logo from "../netlify-auth/netlify_logo.svg";
import styles from "../netlify-auth/AuthenticationPage.css";



Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace change is unrelated to PR.

export default class AuthenticationPage extends React.Component {
static propTypes = {
onLogin: React.PropTypes.func.isRequired,
Expand All @@ -25,10 +27,10 @@ export default class AuthenticationPage extends React.Component {
return (<section className={styles.root}>
<Card className={styles.card}>
<img src={logo} width={100} role="presentation" />
<p className={styles.message}>This is a demo, enter your email to start</p>
<p className={styles.message}>{polyglot.t('demo_message')}</p>
<Input
type="text"
label="Email"
label={polyglot.t('email')}
name="email"
value={this.state.email}
onChange={this.handleEmailChange}
Expand All @@ -38,7 +40,7 @@ export default class AuthenticationPage extends React.Component {
raised
onClick={this.handleLogin}
>
<Icon type="login" /> Login
<Icon type="login" /> {polyglot.t('login')}
</Button>
</Card>
</section>);
Expand Down
3 changes: 2 additions & 1 deletion src/components/AppHeader/AppHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import FindBar from "../FindBar/FindBar";
import { stringToRGB } from "../../lib/textHelper";
import styles from "./AppHeader.css";


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace change is unrelated to PR.

export default class AppHeader extends React.Component {

static propTypes = {
Expand Down Expand Up @@ -89,7 +90,7 @@ export default class AppHeader extends React.Component {
<FindBar runCommand={runCommand} />
<Avatar style={avatarStyle} title={user.get("name")} image={user.get("avatar_url")} />
<IconMenu icon="settings" position="topRight" theme={styles}>
<MenuItem onClick={onLogoutClick} value="log out" caption="Log Out" />
<MenuItem onClick={onLogoutClick} value="log out" caption={polyglot.t('logout')} />
</IconMenu>
</AppBar>
);
Expand Down
3 changes: 2 additions & 1 deletion src/components/EntryEditor/EntryEditorToolbar.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { PropTypes } from 'react';
import { Button } from 'react-toolbox/lib/button';


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace change is unrelated to PR.

const EntryEditorToolbar = (
{
isPersisting,
Expand All @@ -16,7 +17,7 @@ const EntryEditorToolbar = (
onClick={onPersist}
disabled={disabled}
>
{ isPersisting ? 'Saving...' : 'Save' }
{ isPersisting ? polyglot.t('saving') + ' ...' : polyglot.t('save') }
</Button>
{' '}
<Button onClick={onCancelEdit}>
Expand Down
4 changes: 2 additions & 2 deletions src/components/FindBar/FindBar.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { Component, PropTypes } from 'react';
import styles from './FindBar.css';


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace change is unrelated to PR.

export const SEARCH = 'SEARCH';
const PLACEHOLDER = 'Search entry titles...';

class FindBar extends Component {
static propTypes = {
Expand All @@ -17,7 +17,7 @@ class FindBar extends Component {
};
this.state = {
value: '',
placeholder: PLACEHOLDER,
placeholder: polyglot.t('search_entry_titles') + ' ...',
};
}

Expand Down
6 changes: 4 additions & 2 deletions src/components/UnpublishedListing/UnpublishedListing.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import UnpublishedListingCardMeta from './UnpublishedListingCardMeta.js';
import { status, statusDescriptions } from '../../constants/publishModes';
import styles from './UnpublishedListing.css';



Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace change is unrelated to PR.

class UnpublishedListing extends React.Component {
static propTypes = {
entries: ImmutablePropTypes.orderedMap,
Expand All @@ -27,13 +29,13 @@ class UnpublishedListing extends React.Component {
};

requestDelete = (collection, slug, ownStatus) => {
if (window.confirm('Are you sure you want to delete this entry?')) {
if (window.confirm(polyglot.t('really_delete'))) {
this.props.handleDelete(collection, slug, ownStatus);
}
};
requestPublish = (collection, slug, ownStatus) => {
if (ownStatus !== status.last()) return;
if (window.confirm('Are you sure you want to publish this entry?')) {
if (window.confirm(polyglot.t('really_publish'))) {
this.props.handlePublish(collection, slug, ownStatus);
}
};
Expand Down
4 changes: 3 additions & 1 deletion src/components/Widgets/FileControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { truncateMiddle } from '../../lib/textHelper';
import { Loader } from '../UI';
import AssetProxy, { createAssetProxy } from '../../valueObjects/AssetProxy';



Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace change is unrelated to PR.

const MAX_DISPLAY_LENGTH = 50;

export default class FileControl extends React.Component {
Expand Down Expand Up @@ -92,7 +94,7 @@ export default class FileControl extends React.Component {
onDrop={this.handleChange}
>
<span style={styles.message} onClick={this.handleClick}>
{fileName ? fileName : 'Tip: Click here to select a file to upload, or drag an image directly into this box from your desktop'}
{fileName ? fileName : polyglot.t('filecontrol_tip')}
</span>
<input
type="file"
Expand Down
3 changes: 2 additions & 1 deletion src/components/Widgets/ImageControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { truncateMiddle } from '../../lib/textHelper';
import { Loader } from '../UI';
import AssetProxy, { createAssetProxy } from '../../valueObjects/AssetProxy';


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace change is unrelated to PR.

const MAX_DISPLAY_LENGTH = 50;

export default class ImageControl extends React.Component {
Expand Down Expand Up @@ -96,7 +97,7 @@ export default class ImageControl extends React.Component {
onDrop={this.handleChange}
>
<span style={styles.message} onClick={this.handleClick}>
{imageName ? imageName : 'Tip: Click here to upload an image from your file browser, or drag an image directly into this box from your desktop'}
{imageName ? imageName : polyglot.t('imagecontrol_tip')}
</span>
<input
type="file"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ToolbarPluginForm from './ToolbarPluginForm';
import { Icon } from '../../../UI';
import styles from './Toolbar.css';


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace change is unrelated to PR.

export default class Toolbar extends React.Component {
static propTypes = {
selectionPosition: PropTypes.object,
Expand Down Expand Up @@ -64,11 +65,11 @@ export default class Toolbar extends React.Component {

return (
<div className={styles.Toolbar}>
<ToolbarButton label="Header 1" icon="h1" action={onH1}/>
<ToolbarButton label="Header 2" icon="h2" action={onH2}/>
<ToolbarButton label="Bold" icon="bold" action={onBold}/>
<ToolbarButton label="Italic" icon="italic" action={onItalic}/>
<ToolbarButton label="Link" icon="link" action={onLink}/>
<ToolbarButton label={polyglot.t('header1')} icon="h1" action={onH1}/>
<ToolbarButton label={polyglot.t('header2')} icon="h2" action={onH2}/>
<ToolbarButton label={polyglot.t('bold')} icon="bold" action={onBold}/>
<ToolbarButton label={polyglot.t('italic')} icon="italic" action={onItalic}/>
<ToolbarButton label={polyglot.t('link')} icon="link" action={onLink}/>
<ToolbarComponentsMenu
plugins={plugins}
onComponentMenuItemClick={this.handlePluginFormDisplay}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ export default class ToolbarPluginForm extends React.Component {
))}
</div>
<div className={styles.footer}>
<Button raised onClick={this.handleSubmit}>Insert</Button>
<Button raised onClick={this.handleSubmit}>{polyglot.t('insert')}</Button>
{' '}
<Button onClick={onCancel}>Cancel</Button>
<Button onClick={onCancel}>{polyglot.t('cancel')}</Button>
</div>
</form>
);
Expand Down
7 changes: 4 additions & 3 deletions src/constants/publishModes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Map, OrderedMap } from 'immutable';


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace change is unrelated to PR.

// Create/edit workflow modes
export const SIMPLE = 'simple';
export const EDITORIAL_WORKFLOW = 'editorial_workflow';
Expand All @@ -12,7 +13,7 @@ export const status = OrderedMap({
});

export const statusDescriptions = Map({
[status.get('DRAFT')]: 'Draft',
[status.get('PENDING_REVIEW')]: 'Waiting for Review',
[status.get('PENDING_PUBLISH')]: 'Waiting to go live',
[status.get('DRAFT')]: polyglot.t('draft'),
[status.get('PENDING_REVIEW')]: polyglot.t('review_waiting'),
[status.get('PENDING_PUBLISH')]: polyglot.t('go_live_waiting'),
});
5 changes: 3 additions & 2 deletions src/containers/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { SIMPLE, EDITORIAL_WORKFLOW } from '../constants/publishModes';
import styles from './App.css';
import sidebarStyles from './Sidebar.css';


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whitespace change is unrelated to PR.

TopBarProgress.config({
barColors: {
"0": '#3ab7a5',
Expand Down Expand Up @@ -125,7 +126,7 @@ class App extends React.Component {
}

if (config.get('isFetching')) {
return <Loader active>Loading configuration...</Loader>;
return <Loader active>{polyglot.t('loading_configuration')}</Loader>;
}

if (user == null) {
Expand All @@ -145,7 +146,7 @@ class App extends React.Component {
</section>
}
<section>
<h1 className={sidebarStyles.heading}>Collections</h1>
<h1 className={sidebarStyles.heading}>{polyglot.t('collections')}</h1>
{
collections.valueSeq().map((collection) => {
const collectionName = collection.get('name');
Expand Down
Loading