-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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
Changes from 11 commits
f7f46cd
905126b
6f08d7d
4f48dba
f2b1783
cec6835
86018b9
5f80a81
659f273
2a0cfe8
830d3f8
657ce96
cdf42b0
db77f21
843d3a0
43cde1e
a47dab3
77bb466
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> | ||
<link rel="stylesheet" href="/cms.css"/> | ||
<script> | ||
window.repoFiles = { | ||
|
@@ -76,6 +77,9 @@ | |
</head> | ||
<body> | ||
|
||
<script> | ||
var polyglot = new Polyglot(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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({ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
const i18n = require('i18n-extract'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file should be moved to |
||
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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 :) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
} | ||
|
||
// 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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ export const CONFIG_FAILURE = "CONFIG_FAILURE"; | |
|
||
const defaults = { | ||
publish_mode: publishModes.SIMPLE, | ||
lang: 'en' | ||
}; | ||
|
||
export function applyDefaults(config) { | ||
|
@@ -82,6 +83,27 @@ export function configDidLoad(config) { | |
}; | ||
} | ||
|
||
const DEFAULT_I18N_URL = 'https://raw.githubusercontent.com/netlify/netlify-cms/master/src'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
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); | ||
|
@@ -99,6 +121,7 @@ export function loadConfig() { | |
.then(parseConfig) | ||
.then(validateConfig) | ||
.then(applyDefaults) | ||
.then(loadTranslations) | ||
.then((config) => { | ||
dispatch(configDidLoad(config)); | ||
dispatch(authenticateUser()); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"; | ||
|
||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
@@ -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} | ||
|
@@ -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>); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ import FindBar from "../FindBar/FindBar"; | |
import { stringToRGB } from "../../lib/textHelper"; | ||
import styles from "./AppHeader.css"; | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = { | ||
|
@@ -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> | ||
); | ||
|
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'; | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whitespace change is unrelated to PR. |
||
const EntryEditorToolbar = ( | ||
{ | ||
isPersisting, | ||
|
@@ -16,7 +17,7 @@ const EntryEditorToolbar = ( | |
onClick={onPersist} | ||
disabled={disabled} | ||
> | ||
{ isPersisting ? 'Saving...' : 'Save' } | ||
{ isPersisting ? polyglot.t('saving') + ' ...' : polyglot.t('save') } | ||
</Button> | ||
{' '} | ||
<Button onClick={onCancelEdit}> | ||
|
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'; | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = { | ||
|
@@ -17,7 +17,7 @@ class FindBar extends Component { | |
}; | ||
this.state = { | ||
value: '', | ||
placeholder: PLACEHOLDER, | ||
placeholder: polyglot.t('search_entry_titles') + ' ...', | ||
}; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,8 @@ import UnpublishedListingCardMeta from './UnpublishedListingCardMeta.js'; | |
import { status, statusDescriptions } from '../../constants/publishModes'; | ||
import styles from './UnpublishedListing.css'; | ||
|
||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
@@ -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); | ||
} | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,8 @@ import { truncateMiddle } from '../../lib/textHelper'; | |
import { Loader } from '../UI'; | ||
import AssetProxy, { createAssetProxy } from '../../valueObjects/AssetProxy'; | ||
|
||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
@@ -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" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ import { truncateMiddle } from '../../lib/textHelper'; | |
import { Loader } from '../UI'; | ||
import AssetProxy, { createAssetProxy } from '../../valueObjects/AssetProxy'; | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
@@ -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" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ import ToolbarPluginForm from './ToolbarPluginForm'; | |
import { Icon } from '../../../UI'; | ||
import styles from './Toolbar.css'; | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
@@ -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} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import { Map, OrderedMap } from 'immutable'; | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'; | ||
|
@@ -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'), | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,7 @@ import { SIMPLE, EDITORIAL_WORKFLOW } from '../constants/publishModes'; | |
import styles from './App.css'; | ||
import sidebarStyles from './Sidebar.css'; | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whitespace change is unrelated to PR. |
||
TopBarProgress.config({ | ||
barColors: { | ||
"0": '#3ab7a5', | ||
|
@@ -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) { | ||
|
@@ -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'); | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ...
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ...