improvement(i18n): extract core UI texts to external file (#1708)

This commit is contained in:
Nahuel Dealbera 2018-11-02 14:19:49 -03:00 committed by Shawn Erquhart
parent d538554c88
commit c2e21ff9db
28 changed files with 673 additions and 297 deletions

View File

@ -7,172 +7,177 @@ publish_mode: editorial_workflow
media_folder: assets/uploads
collections: # A list of collections the CMS should be able to edit
- name: "posts" # Used in routes, ie.: /admin/collections/:slug/edit
label: "Posts" # Used in the UI
label_singular: "Post" # Used in the UI, ie: "New Post"
- name: 'posts' # Used in routes, ie.: /admin/collections/:slug/edit
label: 'Posts' # Used in the UI
label_singular: 'Post' # Used in the UI, ie: "New Post"
description: >
The description is a great place for tone setting, high level information, and editing
guidelines that are specific to a collection.
folder: "_posts"
slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
folder: '_posts'
slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
create: true # Allow users to create new documents in this collection
fields: # The fields each document in this collection have
- {label: "Title", name: "title", widget: "string", tagname: "h1"}
- {label: "Publish Date", name: "date", widget: "datetime", format: "YYYY-MM-DD hh:mma"}
- label: "Cover Image"
name: "image"
widget: "image"
- { label: 'Title', name: 'title', widget: 'string', tagname: 'h1' }
- { label: 'Publish Date', name: 'date', widget: 'datetime', format: 'YYYY-MM-DD hh:mma' }
- label: 'Cover Image'
name: 'image'
widget: 'image'
required: false
tagname: ""
tagname: ''
- {label: "Body", name: "body", widget: "markdown", hint: "Main content goes here."}
- { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' }
meta:
- {label: "SEO Description", name: "description", widget: "text"}
- { label: 'SEO Description', name: 'description', widget: 'text' }
- name: "faq" # Used in routes, ie.: /admin/collections/:slug/edit
label: "FAQ" # Used in the UI
folder: "_faqs"
- name: 'faq' # Used in routes, ie.: /admin/collections/:slug/edit
label: 'FAQ' # Used in the UI
folder: '_faqs'
create: true # Allow users to create new documents in this collection
fields: # The fields each document in this collection have
- {label: "Question", name: "title", widget: "string", tagname: "h1"}
- {label: "Answer", name: "body", widget: "markdown"}
- { label: 'Question', name: 'title', widget: 'string', tagname: 'h1' }
- { label: 'Answer', name: 'body', widget: 'markdown' }
- name: "settings"
label: "Settings"
- name: 'settings'
label: 'Settings'
delete: false # Prevent users from deleting documents in this collection
editor:
preview: false
files:
- name: "general"
label: "Site Settings"
file: "_data/settings.json"
description: "General Site Settings"
- name: 'general'
label: 'Site Settings'
file: '_data/settings.json'
description: 'General Site Settings'
fields:
- {label: "Global title", name: "site_title", widget: "string"}
- label: "Post Settings"
- { label: 'Global title', name: 'site_title', widget: 'string' }
- label: 'Post Settings'
name: posts
widget: "object"
widget: 'object'
fields:
- {label: "Number of posts on frontpage", name: front_limit, widget: number}
- {label: "Default Author", name: author, widget: string}
- {label: "Default Thumbnail", name: thumb, widget: image, class: "thumb"}
- { label: 'Number of posts on frontpage', name: front_limit, widget: number }
- { label: 'Default Author', name: author, widget: string }
- { label: 'Default Thumbnail', name: thumb, widget: image, class: 'thumb' }
- name: "authors"
label: "Authors"
file: "_data/authors.yml"
description: "Author descriptions"
- name: 'authors'
label: 'Authors'
file: '_data/authors.yml'
description: 'Author descriptions'
fields:
- name: authors
label: Authors
label_singular: "Author"
label_singular: 'Author'
widget: list
fields:
- {label: "Name", name: "name", widget: "string", hint: "First and Last"}
- {label: "Description", name: "description", widget: "markdown"}
- { label: 'Name', name: 'name', widget: 'string', hint: 'First and Last' }
- { label: 'Description', name: 'description', widget: 'markdown' }
- name: "kitchenSink" # all the things in one entry, for documentation and quick testing
label: "Kitchen Sink"
folder: "_sink"
- name: 'kitchenSink' # all the things in one entry, for documentation and quick testing
label: 'Kitchen Sink'
folder: '_sink'
create: true
fields:
- label: "Related Post"
name: "post"
widget: "relationKitchenSinkPost"
collection: "posts"
displayFields: ["title", "date"]
searchFields: ["title", "body"]
valueField: "title"
- {label: "Title", name: "title", widget: "string"}
- {label: "Boolean", name: "boolean", widget: "boolean", default: true}
- {label: "Text", name: "text", widget: "text", hint: "Plain text, not markdown"}
- {label: "Number", name: "number", widget: "number", hint: "To infinity and beyond!"}
- {label: "Markdown", name: "markdown", widget: "markdown"}
- {label: "Datetime", name: "datetime", widget: "datetime"}
- {label: "Date", name: "date", widget: "date"}
- {label: "Image", name: "image", widget: "image"}
- {label: "File", name: "file", widget: "file"}
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]}
- {label: "Hidden", name: "hidden", widget: "hidden", default: "hidden"}
- label: "Object"
name: "object"
widget: "object"
- label: 'Related Post'
name: 'post'
widget: 'relationKitchenSinkPost'
collection: 'posts'
displayFields: ['title', 'date']
searchFields: ['title', 'body']
valueField: 'title'
- { label: 'Title', name: 'title', widget: 'string' }
- { label: 'Boolean', name: 'boolean', widget: 'boolean', default: true }
- { label: 'Text', name: 'text', widget: 'text', hint: 'Plain text, not markdown' }
- { label: 'Number', name: 'number', widget: 'number', hint: 'To infinity and beyond!' }
- { label: 'Markdown', name: 'markdown', widget: 'markdown' }
- { label: 'Datetime', name: 'datetime', widget: 'datetime' }
- { label: 'Date', name: 'date', widget: 'date' }
- { label: 'Image', name: 'image', widget: 'image' }
- { label: 'File', name: 'file', widget: 'file' }
- { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
- { label: 'Hidden', name: 'hidden', widget: 'hidden', default: 'hidden' }
- label: 'Object'
name: 'object'
widget: 'object'
fields:
- label: "Related Post"
name: "post"
widget: "relationKitchenSinkPost"
collection: "posts"
searchFields: ["title", "body"]
valueField: "title"
- {label: "String", name: "string", widget: "string"}
- {label: "Boolean", name: "boolean", widget: "boolean", default: false}
- {label: "Text", name: "text", widget: "text"}
- {label: "Number", name: "number", widget: "number"}
- {label: "Markdown", name: "markdown", widget: "markdown"}
- {label: "Datetime", name: "datetime", widget: "datetime"}
- {label: "Date", name: "date", widget: "date"}
- {label: "Image", name: "image", widget: "image"}
- {label: "File", name: "file", widget: "file"}
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]}
- label: "List"
name: "list"
widget: "list"
- label: 'Related Post'
name: 'post'
widget: 'relationKitchenSinkPost'
collection: 'posts'
searchFields: ['title', 'body']
valueField: 'title'
- { label: 'String', name: 'string', widget: 'string' }
- { label: 'Boolean', name: 'boolean', widget: 'boolean', default: false }
- { label: 'Text', name: 'text', widget: 'text' }
- { label: 'Number', name: 'number', widget: 'number' }
- { label: 'Markdown', name: 'markdown', widget: 'markdown' }
- { label: 'Datetime', name: 'datetime', widget: 'datetime' }
- { label: 'Date', name: 'date', widget: 'date' }
- { label: 'Image', name: 'image', widget: 'image' }
- { label: 'File', name: 'file', widget: 'file' }
- { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
- label: 'List'
name: 'list'
widget: 'list'
fields:
- {label: "String", name: "string", widget: "string"}
- {label: "Boolean", name: "boolean", widget: "boolean"}
- {label: "Text", name: "text", widget: "text"}
- {label: "Number", name: "number", widget: "number"}
- {label: "Markdown", name: "markdown", widget: "markdown"}
- {label: "Datetime", name: "datetime", widget: "datetime"}
- {label: "Date", name: "date", widget: "date"}
- {label: "Image", name: "image", widget: "image"}
- {label: "File", name: "file", widget: "file"}
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]}
- label: "Object"
name: "object"
widget: "object"
- { label: 'String', name: 'string', widget: 'string' }
- { label: 'Boolean', name: 'boolean', widget: 'boolean' }
- { label: 'Text', name: 'text', widget: 'text' }
- { label: 'Number', name: 'number', widget: 'number' }
- { label: 'Markdown', name: 'markdown', widget: 'markdown' }
- { label: 'Datetime', name: 'datetime', widget: 'datetime' }
- { label: 'Date', name: 'date', widget: 'date' }
- { label: 'Image', name: 'image', widget: 'image' }
- { label: 'File', name: 'file', widget: 'file' }
- { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
- label: 'Object'
name: 'object'
widget: 'object'
fields:
- {label: "String", name: "string", widget: "string"}
- {label: "Boolean", name: "boolean", widget: "boolean"}
- {label: "Text", name: "text", widget: "text"}
- {label: "Number", name: "number", widget: "number"}
- {label: "Markdown", name: "markdown", widget: "markdown"}
- {label: "Datetime", name: "datetime", widget: "datetime"}
- {label: "Date", name: "date", widget: "date"}
- {label: "Image", name: "image", widget: "image"}
- {label: "File", name: "file", widget: "file"}
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]}
- label: "List"
name: "list"
widget: "list"
- { label: 'String', name: 'string', widget: 'string' }
- { label: 'Boolean', name: 'boolean', widget: 'boolean' }
- { label: 'Text', name: 'text', widget: 'text' }
- { label: 'Number', name: 'number', widget: 'number' }
- { label: 'Markdown', name: 'markdown', widget: 'markdown' }
- { label: 'Datetime', name: 'datetime', widget: 'datetime' }
- { label: 'Date', name: 'date', widget: 'date' }
- { label: 'Image', name: 'image', widget: 'image' }
- { label: 'File', name: 'file', widget: 'file' }
- { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
- label: 'List'
name: 'list'
widget: 'list'
fields:
- label: "Related Post"
name: "post"
widget: "relationKitchenSinkPost"
collection: "posts"
searchFields: ["title", "body"]
valueField: "title"
- {label: "String", name: "string", widget: "string"}
- {label: "Boolean", name: "boolean", widget: "boolean"}
- {label: "Text", name: "text", widget: "text"}
- {label: "Number", name: "number", widget: "number"}
- {label: "Markdown", name: "markdown", widget: "markdown"}
- {label: "Datetime", name: "datetime", widget: "datetime"}
- {label: "Date", name: "date", widget: "date"}
- {label: "Image", name: "image", widget: "image"}
- {label: "File", name: "file", widget: "file"}
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]}
- {label: "Hidden", name: "hidden", widget: "hidden", default: "hidden"}
- label: "Object"
name: "object"
widget: "object"
- label: 'Related Post'
name: 'post'
widget: 'relationKitchenSinkPost'
collection: 'posts'
searchFields: ['title', 'body']
valueField: 'title'
- { label: 'String', name: 'string', widget: 'string' }
- { label: 'Boolean', name: 'boolean', widget: 'boolean' }
- { label: 'Text', name: 'text', widget: 'text' }
- { label: 'Number', name: 'number', widget: 'number' }
- { label: 'Markdown', name: 'markdown', widget: 'markdown' }
- { label: 'Datetime', name: 'datetime', widget: 'datetime' }
- { label: 'Date', name: 'date', widget: 'date' }
- { label: 'Image', name: 'image', widget: 'image' }
- { label: 'File', name: 'file', widget: 'file' }
- { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
- { label: 'Hidden', name: 'hidden', widget: 'hidden', default: 'hidden' }
- label: 'Object'
name: 'object'
widget: 'object'
fields:
- {label: "String", name: "string", widget: "string"}
- {label: "Boolean", name: "boolean", widget: "boolean"}
- {label: "Text", name: "text", widget: "text"}
- {label: "Number", name: "number", widget: "number"}
- {label: "Markdown", name: "markdown", widget: "markdown"}
- {label: "Datetime", name: "datetime", widget: "datetime"}
- {label: "Date", name: "date", widget: "date"}
- {label: "Image", name: "image", widget: "image"}
- {label: "File", name: "file", widget: "file"}
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]}
- { label: 'String', name: 'string', widget: 'string' }
- { label: 'Boolean', name: 'boolean', widget: 'boolean' }
- { label: 'Text', name: 'text', widget: 'text' }
- { label: 'Number', name: 'number', widget: 'number' }
- { label: 'Markdown', name: 'markdown', widget: 'markdown' }
- { label: 'Datetime', name: 'datetime', widget: 'datetime' }
- { label: 'Date', name: 'date', widget: 'date' }
- { label: 'Image', name: 'image', widget: 'image' }
- { label: 'File', name: 'file', widget: 'file' }
- {
label: 'Select',
name: 'select',
widget: 'select',
options: ['a', 'b', 'c'],
}

View File

@ -38,6 +38,7 @@
"netlify-cms-lib-auth": "^2.0.4",
"netlify-cms-lib-util": "^2.1.0",
"netlify-cms-ui-default": "^2.0.6",
"node-polyglot": "^2.3.0",
"prop-types": "^15.5.10",
"react": "^16.4.1",
"react-dnd": "^2.5.4",
@ -49,6 +50,7 @@
"react-immutable-proptypes": "^2.1.0",
"react-is": "16.3.1",
"react-modal": "^3.1.5",
"react-polyglot": "^0.2.6",
"react-redux": "^4.4.0",
"react-router-dom": "^4.2.2",
"react-router-redux": "^5.0.0-alpha.8",

View File

@ -244,7 +244,10 @@ export function loadUnpublishedEntry(collection, slug) {
} else {
dispatch(
notifSend({
message: `Error loading entry: ${error}`,
message: {
key: 'ui.toast.onFailToLoadEntries',
details: error,
},
kind: 'danger',
dismissAfter: 8000,
}),
@ -266,7 +269,10 @@ export function loadUnpublishedEntries(collections) {
.catch(error => {
dispatch(
notifSend({
message: `Error loading entries: ${error}`,
message: {
key: 'ui.toast.onFailToLoadEntries',
details: error,
},
kind: 'danger',
dismissAfter: 8000,
}),
@ -292,7 +298,9 @@ export function persistUnpublishedEntry(collection, existingUnpublishedEntry) {
if (hasPresenceErrors) {
dispatch(
notifSend({
message: "Oops, you've missed a required field. Please complete before saving.",
message: {
key: 'ui.toast.missingRequiredField',
},
kind: 'danger',
dismissAfter: 8000,
}),
@ -332,7 +340,9 @@ export function persistUnpublishedEntry(collection, existingUnpublishedEntry) {
const newSlug = await persistAction.call(...persistCallArgs);
dispatch(
notifSend({
message: 'Entry saved',
message: {
key: 'ui.toast.entrySaved',
},
kind: 'success',
dismissAfter: 4000,
}),
@ -341,7 +351,10 @@ export function persistUnpublishedEntry(collection, existingUnpublishedEntry) {
} catch (error) {
dispatch(
notifSend({
message: `Failed to persist entry: ${error}`,
message: {
key: 'ui.toast.onFailToPersist',
details: error,
},
kind: 'danger',
dismissAfter: 8000,
}),
@ -364,7 +377,9 @@ export function updateUnpublishedEntryStatus(collection, slug, oldStatus, newSta
.then(() => {
dispatch(
notifSend({
message: 'Entry status updated',
message: {
key: 'ui.toast.entryUpdated',
},
kind: 'success',
dismissAfter: 4000,
}),
@ -382,7 +397,10 @@ export function updateUnpublishedEntryStatus(collection, slug, oldStatus, newSta
.catch(error => {
dispatch(
notifSend({
message: `Failed to update status: ${error}`,
message: {
key: 'ui.toast.onFailToUpdateStatus',
details: error,
},
kind: 'danger',
dismissAfter: 8000,
}),
@ -403,7 +421,7 @@ export function deleteUnpublishedEntry(collection, slug) {
.then(() => {
dispatch(
notifSend({
message: 'Unpublished changes deleted',
message: { key: 'ui.toast.onDeleteUnpublishedChanges' },
kind: 'success',
dismissAfter: 4000,
}),
@ -413,7 +431,7 @@ export function deleteUnpublishedEntry(collection, slug) {
.catch(error => {
dispatch(
notifSend({
message: `Failed to delete unpublished changes: ${error}`,
message: { key: 'ui.toast.onDeleteUnpublishedChanges', details: error },
kind: 'danger',
dismissAfter: 8000,
}),
@ -434,7 +452,7 @@ export function publishUnpublishedEntry(collection, slug) {
.then(() => {
dispatch(
notifSend({
message: 'Entry published',
message: { key: 'ui.toast.entryPublished' },
kind: 'success',
dismissAfter: 4000,
}),
@ -444,7 +462,7 @@ export function publishUnpublishedEntry(collection, slug) {
.catch(error => {
dispatch(
notifSend({
message: `Failed to publish: ${error}`,
message: { key: 'ui.toast.onFailToPublishEntry', details: error },
kind: 'danger',
dismissAfter: 8000,
}),

View File

@ -233,7 +233,10 @@ export function loadEntry(collection, slug) {
console.error(error);
dispatch(
notifSend({
message: `Failed to load entry: ${error.message}`,
message: {
details: error.message,
key: 'ui.toast.onFailToLoadEntries',
},
kind: 'danger',
dismissAfter: 8000,
}),
@ -300,7 +303,10 @@ export function loadEntries(collection, page = 0) {
.catch(err => {
dispatch(
notifSend({
message: `Failed to load entries: ${err}`,
message: {
details: err,
key: 'ui.toast.onFailToLoadEntries',
},
kind: 'danger',
dismissAfter: 8000,
}),
@ -348,7 +354,10 @@ export function traverseCollectionCursor(collection, action) {
console.error(err);
dispatch(
notifSend({
message: `Failed to persist entry: ${err}`,
message: {
details: err,
key: 'ui.toast.onFailToPersist',
},
kind: 'danger',
dismissAfter: 8000,
}),
@ -409,7 +418,9 @@ export function persistEntry(collection) {
if (hasPresenceErrors) {
dispatch(
notifSend({
message: "Oops, you've missed a required field. Please complete before saving.",
message: {
key: 'ui.toast.missingRequiredField',
},
kind: 'danger',
dismissAfter: 8000,
}),
@ -437,7 +448,9 @@ export function persistEntry(collection) {
.then(slug => {
dispatch(
notifSend({
message: 'Entry saved',
message: {
key: 'ui.toast.missingRequiredField',
},
kind: 'success',
dismissAfter: 4000,
}),
@ -448,7 +461,10 @@ export function persistEntry(collection) {
console.error(error);
dispatch(
notifSend({
message: `Failed to persist entry: ${error}`,
message: {
details: error,
key: 'ui.toast.onFailToPersist',
},
kind: 'danger',
dismissAfter: 8000,
}),
@ -472,7 +488,10 @@ export function deleteEntry(collection, slug) {
.catch(error => {
dispatch(
notifSend({
message: `Failed to delete entry: ${error}`,
message: {
details: error,
key: 'ui.toast.onFailToDelete',
},
kind: 'danger',
dismissAfter: 8000,
}),

View File

@ -6,6 +6,8 @@ import { ConnectedRouter } from 'react-router-redux';
import history from 'Routing/history';
import store from 'Redux';
import { mergeConfig } from 'Actions/config';
import { getPhrases } from 'Constants/defaultPhrases';
import { I18n } from 'react-polyglot';
import { ErrorBoundary } from 'UI';
import App from 'App/App';
import 'EditorWidgets';
@ -60,13 +62,15 @@ function bootstrap(opts = {}) {
* Create connected root component.
*/
const Root = () => (
<ErrorBoundary>
<Provider store={store}>
<ConnectedRouter history={history}>
<Route component={App} />
</ConnectedRouter>
</Provider>
</ErrorBoundary>
<I18n locale={'en'} messages={getPhrases()}>
<ErrorBoundary>
<Provider store={store}>
<ConnectedRouter history={history}>
<Route component={App} />
</ConnectedRouter>
</Provider>
</ErrorBoundary>
</I18n>
);
/**

View File

@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import { hot } from 'react-hot-loader';
import { translate } from 'react-polyglot';
import ImmutablePropTypes from 'react-immutable-proptypes';
import styled from 'react-emotion';
import { connect } from 'react-redux';
@ -63,17 +64,19 @@ class App extends React.Component {
useMediaLibrary: PropTypes.bool,
openMediaLibrary: PropTypes.func.isRequired,
showMediaButton: PropTypes.bool,
t: PropTypes.func.isRequired,
};
static configError(config) {
const t = this.props.t;
return (
<ErrorContainer>
<h1>Error loading the CMS configuration</h1>
<h1>{t('app.app.errorHeader')}</h1>
<div>
<strong>Config Errors:</strong>
<strong>{t('app.app.configErrors')}:</strong>
<ErrorCodeBlock>{config.get('error')}</ErrorCodeBlock>
<span>Check your config.yml file.</span>
<span>{t('app.app.checkConfigYml')}</span>
</div>
</ErrorContainer>
);
@ -89,13 +92,13 @@ class App extends React.Component {
}
authenticating() {
const { auth } = this.props;
const { auth, t } = this.props;
const backend = currentBackend(this.props.config);
if (backend == null) {
return (
<div>
<h1>Waiting for backend...</h1>
<h1>{t('app.app.waitingBackend')}</h1>
</div>
);
}
@ -133,6 +136,7 @@ class App extends React.Component {
publishMode,
useMediaLibrary,
openMediaLibrary,
t,
showMediaButton,
} = this.props;
@ -145,11 +149,11 @@ class App extends React.Component {
}
if (config.get('isFetching')) {
return <Loader active>Loading configuration...</Loader>;
return <Loader active>{t('app.app.loadingConfig')}</Loader>;
}
if (user == null) {
return this.authenticating();
return this.authenticating(t);
}
const defaultPath = `/collections/${collections.first().get('name')}`;
@ -225,5 +229,5 @@ export default hot(module)(
connect(
mapStateToProps,
mapDispatchToProps,
)(App),
)(translate()(App)),
);

View File

@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import styled, { css } from 'react-emotion';
import { translate } from 'react-polyglot';
import { NavLink } from 'react-router-dom';
import {
Icon,
@ -99,7 +100,7 @@ const AppHeaderQuickNewButton = styled(StyledDropdownButton)`
}
`;
export default class Header extends React.Component {
class Header extends React.Component {
static propTypes = {
user: ImmutablePropTypes.map.isRequired,
collections: ImmutablePropTypes.orderedMap.isRequired,
@ -108,6 +109,7 @@ export default class Header extends React.Component {
openMediaLibrary: PropTypes.func.isRequired,
hasWorkflow: PropTypes.bool.isRequired,
displayUrl: PropTypes.string,
t: PropTypes.func.isRequired,
};
handleCreatePostClick = collectionName => {
@ -125,6 +127,7 @@ export default class Header extends React.Component {
openMediaLibrary,
hasWorkflow,
displayUrl,
t,
showMediaButton,
} = this.props;
@ -143,25 +146,27 @@ export default class Header extends React.Component {
isActive={(match, location) => location.pathname.startsWith('/collections/')}
>
<Icon type="page" />
Content
{t('app.header.content')}
</AppHeaderNavLink>
{hasWorkflow ? (
<AppHeaderNavLink to="/workflow" activeClassName="header-link-active">
<Icon type="workflow" />
Workflow
{t('app.header.workflow')}
</AppHeaderNavLink>
) : null}
{showMediaButton ? (
<AppHeaderButton onClick={openMediaLibrary}>
<Icon type="media-alt" />
Media
{t('app.header.media')}
</AppHeaderButton>
) : null}
</nav>
<AppHeaderActions>
{createableCollections.size > 0 && (
<Dropdown
renderButton={() => <AppHeaderQuickNewButton>Quick add</AppHeaderQuickNewButton>}
renderButton={() => (
<AppHeaderQuickNewButton> {t('app.header.quickAdd')}</AppHeaderQuickNewButton>
)}
dropdownTopOverlap="30px"
dropdownWidth="160px"
dropdownPosition="left"
@ -187,3 +192,5 @@ export default class Header extends React.Component {
);
}
}
export default translate()(Header);

View File

@ -1,15 +1,21 @@
import React from 'react';
import styled from 'react-emotion';
import { translate } from 'react-polyglot';
import { lengths } from 'netlify-cms-ui-default';
import PropTypes from 'prop-types';
const NotFoundContainer = styled.div`
margin: ${lengths.pageMargin};
`;
const NotFoundPage = () => (
const NotFoundPage = ({ t }) => (
<NotFoundContainer>
<h2>Not Found</h2>
<h2>{t('app.notFoundPage.header')}</h2>
</NotFoundContainer>
);
export default NotFoundPage;
NotFoundPage.propTypes = {
t: PropTypes.func.isRequired,
};
export default translate()(NotFoundPage);

View File

@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import styled from 'react-emotion';
import { translate } from 'react-polyglot';
import { Link } from 'react-router-dom';
import { Icon, components, buttons, shadows, colors } from 'netlify-cms-ui-default';
import { VIEW_STYLE_LIST, VIEW_STYLE_GRID } from 'Constants/collectionViews';
@ -70,6 +71,7 @@ const CollectionTop = ({
viewStyle,
onChangeViewStyle,
newEntryUrl,
t,
}) => {
return (
<CollectionTopContainer>
@ -77,7 +79,9 @@ const CollectionTop = ({
<CollectionTopHeading>{collectionLabel}</CollectionTopHeading>
{newEntryUrl ? (
<CollectionTopNewButton to={newEntryUrl}>
{`New ${collectionLabelSingular || collectionLabel}`}
{t('collection.collectionTop.newButton', {
collectionLabel: collectionLabelSingular || collectionLabel,
})}
</CollectionTopNewButton>
) : null}
</CollectionTopRow>
@ -85,7 +89,7 @@ const CollectionTop = ({
<CollectionTopDescription>{collectionDescription}</CollectionTopDescription>
) : null}
<ViewControls>
<ViewControlsText>View as:</ViewControlsText>
<ViewControlsText>{t('collection.collectionTop.viewAs')}:</ViewControlsText>
<ViewControlsButton
isActive={viewStyle === VIEW_STYLE_LIST}
onClick={() => onChangeViewStyle(VIEW_STYLE_LIST)}
@ -110,6 +114,7 @@ CollectionTop.propTypes = {
viewStyle: PropTypes.oneOf([VIEW_STYLE_LIST, VIEW_STYLE_GRID]).isRequired,
onChangeViewStyle: PropTypes.func.isRequired,
newEntryUrl: PropTypes.string,
t: PropTypes.func.isRequired,
};
export default CollectionTop;
export default translate()(CollectionTop);

View File

@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { translate } from 'react-polyglot';
import { Loader } from 'netlify-cms-ui-default';
import EntryListing from './EntryListing';
@ -12,8 +13,13 @@ const Entries = ({
viewStyle,
cursor,
handleCursorActions,
t,
}) => {
const loadingMessages = ['Loading Entries', 'Caching Entries', 'This might take several minutes'];
const loadingMessages = [
t('collection.entries.loadingEntries'),
t('collection.entries.cachingEntries'),
t('collection.entries.longerLoading'),
];
if (entries) {
return (
@ -44,6 +50,7 @@ Entries.propTypes = {
viewStyle: PropTypes.string,
cursor: PropTypes.any.isRequired,
handleCursorActions: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default Entries;
export default translate()(Entries);

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import styled, { css } from 'react-emotion';
import { translate } from 'react-polyglot';
import { NavLink } from 'react-router-dom';
import { Icon, components, colors, colorsRaw, lengths } from 'netlify-cms-ui-default';
import { searchCollections } from 'Actions/collections';
@ -87,10 +88,11 @@ const SidebarNavLink = styled(NavLink)`
}
`;
export default class Sidebar extends React.Component {
class Sidebar extends React.Component {
static propTypes = {
collections: ImmutablePropTypes.orderedMap.isRequired,
searchTerm: PropTypes.string,
t: PropTypes.func.isRequired,
};
static defaultProps = {
@ -114,18 +116,18 @@ export default class Sidebar extends React.Component {
};
render() {
const { collections } = this.props;
const { collections, t } = this.props;
const { query } = this.state;
return (
<SidebarContainer>
<SidebarHeading>Collections</SidebarHeading>
<SidebarHeading>{t('collection.sidebar.collections')}</SidebarHeading>
<SearchContainer>
<Icon type="search" size="small" />
<SearchInput
onChange={e => this.setState({ query: e.target.value })}
onKeyDown={e => e.key === 'Enter' && searchCollections(query)}
placeholder="Search all"
placeholder={t('collection.sidebar.searchAll')}
value={query}
/>
</SearchContainer>
@ -134,3 +136,5 @@ export default class Sidebar extends React.Component {
);
}
}
export default translate()(Sidebar);

View File

@ -3,6 +3,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { Loader } from 'netlify-cms-ui-default';
import { translate } from 'react-polyglot';
import history from 'Routing/history';
import { logoutUser } from 'Actions/auth';
import {
@ -69,6 +70,7 @@ class Editor extends React.Component {
pathname: PropTypes.string,
}),
hasChanged: PropTypes.bool,
t: PropTypes.func.isRequired,
};
componentDidMount() {
@ -80,6 +82,7 @@ class Editor extends React.Component {
createEmptyDraft,
loadEntries,
collectionEntriesLoaded,
t,
} = this.props;
if (newEntry) {
@ -88,7 +91,7 @@ class Editor extends React.Component {
loadEntry(collection, slug);
}
const leaveMessage = 'Are you sure you want to leave this page?';
const leaveMessage = t('editor.editor.onLeavePage');
this.exitBlocker = event => {
if (this.props.entryDraft.get('hasChanged')) {
@ -190,9 +193,10 @@ class Editor extends React.Component {
collection,
slug,
currentStatus,
t,
} = this.props;
if (entryDraft.get('hasChanged')) {
window.alert('You have unsaved changes, please save before updating status.');
window.alert(t('editor.editor.onUpdatingWithUnsavedChanges'));
return;
}
const newStatus = status.get(newStatusName);
@ -230,14 +234,15 @@ class Editor extends React.Component {
slug,
currentStatus,
loadEntry,
t,
} = this.props;
if (currentStatus !== status.last()) {
window.alert('Please update status to "Ready" before publishing.');
window.alert(t('editor.editor.onPublishingNotReady'));
return;
} else if (entryDraft.get('hasChanged')) {
window.alert('You have unsaved changes, please save before publishing.');
window.alert(t('editor.editor.onPublishingWithUnsavedChanges'));
return;
} else if (!window.confirm('Are you sure you want to publish this entry?')) {
} else if (!window.confirm(t('editor.editor.onPublishing'))) {
return;
}
@ -251,16 +256,12 @@ class Editor extends React.Component {
};
handleDeleteEntry = () => {
const { entryDraft, newEntry, collection, deleteEntry, slug } = this.props;
const { entryDraft, newEntry, collection, deleteEntry, slug, t } = this.props;
if (entryDraft.get('hasChanged')) {
if (
!window.confirm(
'Are you sure you want to delete this published entry, as well as your unsaved changes from the current session?',
)
) {
if (!window.confirm(t('editor.editor.onDeleteWithUnsavedChanges'))) {
return;
}
} else if (!window.confirm('Are you sure you want to delete this published entry?')) {
} else if (!window.confirm(t('editor.editor.onDeletePublishedEntry'))) {
return;
}
if (newEntry) {
@ -281,19 +282,14 @@ class Editor extends React.Component {
deleteUnpublishedEntry,
loadEntry,
isModification,
t,
} = this.props;
if (
entryDraft.get('hasChanged') &&
!window.confirm(
'This will delete all unpublished changes to this entry, as well as your unsaved changes from the current session. Do you still want to delete?',
)
!window.confirm(t('editor.editor.onDeleteUnpublishedChangesWithUnsavedChanges'))
) {
return;
} else if (
!window.confirm(
'All unpublished changes to this entry will be deleted. Do you still want to delete?',
)
) {
} else if (!window.confirm(t('editor.editor.onDeleteUnpublishedChanges'))) {
return;
}
await deleteUnpublishedEntry(collection.get('name'), slug);
@ -323,6 +319,7 @@ class Editor extends React.Component {
isModification,
currentStatus,
logoutUser,
t,
} = this.props;
if (entry && entry.get('error')) {
@ -336,7 +333,7 @@ class Editor extends React.Component {
entryDraft.get('entry') === undefined ||
(entry && entry.get('isFetching'))
) {
return <Loader active>Loading entry...</Loader>;
return <Loader active>{t('editor.editor.loadingEntry')}</Loader>;
}
return (
@ -422,4 +419,4 @@ export default connect(
deleteUnpublishedEntry,
logoutUser,
},
)(withWorkflow(Editor));
)(withWorkflow(translate()(Editor)));

View File

@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { translate } from 'react-polyglot';
import styled, { css, cx } from 'react-emotion';
import { partial, uniqueId } from 'lodash';
import { connect } from 'react-redux';
@ -142,6 +143,7 @@ class EditorControl extends React.Component {
queryHits: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
isFetching: PropTypes.bool,
clearSearch: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
state = {
@ -168,6 +170,7 @@ class EditorControl extends React.Component {
queryHits,
isFetching,
clearSearch,
t,
} = this.props;
const widgetName = field.get('widget');
const widget = resolveWidget(widgetName);
@ -233,6 +236,7 @@ class EditorControl extends React.Component {
queryHits={queryHits}
clearSearch={clearSearch}
isFetching={isFetching}
t={t}
/>
{fieldHint && (
<ControlHint active={this.state.styleActive} error={!!errors}>
@ -266,6 +270,6 @@ const ConnectedEditorControl = connect(
mapDispatchToProps,
null,
{ withRef: true },
)(EditorControl);
)(translate()(EditorControl));
export default ConnectedEditorControl;

View File

@ -48,6 +48,7 @@ export default class Widget extends Component {
queryHits: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
editorControl: PropTypes.func.isRequired,
uniqueFieldId: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
};
shouldComponentUpdate(nextProps) {
@ -103,11 +104,14 @@ export default class Widget extends Component {
};
validatePresence = (field, value) => {
const t = this.props.t;
const isRequired = field.get('required', true);
if (isRequired && isEmpty(value)) {
const error = {
type: ValidationErrorTypes.PRESENCE,
message: `${field.get('label', field.get('name'))} is required.`,
message: t('editor.editorControlPane.widget.required', {
fieldLabel: field.get('label', field.get('name')),
}),
};
return { error };
@ -116,6 +120,7 @@ export default class Widget extends Component {
};
validatePattern = (field, value) => {
const t = this.props.t;
const pattern = field.get('pattern', false);
if (isEmpty(value)) {
@ -125,10 +130,10 @@ export default class Widget extends Component {
if (pattern && !RegExp(pattern.first()).test(value)) {
const error = {
type: ValidationErrorTypes.PATTERN,
message: `${field.get(
'label',
field.get('name'),
)} didn't match the pattern: ${pattern.last()}`,
message: t('editor.editorControlPane.widget.pattern', {
fieldLabel: field.get('label', field.get('name')),
pattern: pattern.last(),
}),
};
return { error };
@ -138,6 +143,7 @@ export default class Widget extends Component {
};
validateWrappedControl = field => {
const t = this.props.t;
const response = this.wrappedControlValid();
if (typeof response === 'boolean') {
const isValid = response;
@ -161,7 +167,9 @@ export default class Widget extends Component {
const error = {
type: ValidationErrorTypes.CUSTOM,
message: `${field.get('label', field.get('name'))} is processing.`,
message: t('editor.editorControlPane.widget.processing', {
fieldLabel: field.get('label', field.get('name')),
}),
};
return { error };
@ -214,6 +222,7 @@ export default class Widget extends Component {
queryHits,
clearSearch,
isFetching,
t,
} = this.props;
return React.createElement(controlComponent, {
field,
@ -245,6 +254,7 @@ export default class Widget extends Component {
queryHits,
clearSearch,
isFetching,
t,
});
}
}

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import styled, { css } from 'react-emotion';
import { translate } from 'react-polyglot';
import { Link } from 'react-router-dom';
import {
Icon,
@ -165,7 +166,7 @@ const StatusDropdownItem = styled(DropdownItem)`
}
`;
export default class EditorToolbar extends React.Component {
class EditorToolbar extends React.Component {
static propTypes = {
isPersisting: PropTypes.bool,
isPublishing: PropTypes.bool,
@ -190,12 +191,17 @@ export default class EditorToolbar extends React.Component {
isModification: PropTypes.bool,
currentStatus: PropTypes.string,
onLogoutClick: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
renderSimpleSaveControls = () => {
const { showDelete, onDelete } = this.props;
const { showDelete, onDelete, t } = this.props;
return (
<div>{showDelete ? <DeleteButton onClick={onDelete}>Delete entry</DeleteButton> : null}</div>
<div>
{showDelete ? (
<DeleteButton onClick={onDelete}>{t('editor.editorToolbar.deleteEntry')}</DeleteButton>
) : null}
</div>
);
};
@ -207,9 +213,10 @@ export default class EditorToolbar extends React.Component {
isPersisting,
hasChanged,
isNewEntry,
t,
} = this.props;
if (!isNewEntry && !hasChanged) {
return <StatusPublished>Published</StatusPublished>;
return <StatusPublished>{t('editor.editorToolbar.published')}</StatusPublished>;
}
return (
<div>
@ -217,7 +224,11 @@ export default class EditorToolbar extends React.Component {
dropdownTopOverlap="40px"
dropdownWidth="150px"
renderButton={() => (
<PublishButton>{isPersisting ? 'Publishing...' : 'Publish'}</PublishButton>
<PublishButton>
{isPersisting
? t('editor.editorToolbar.publishing')
: t('editor.editorToolbar.publish')}
</PublishButton>
)}
>
<DropdownItem
@ -227,7 +238,11 @@ export default class EditorToolbar extends React.Component {
onClick={onPersist}
/>
{collection.get('create') ? (
<DropdownItem label="Publish and create new" icon="add" onClick={onPersistAndNew} />
<DropdownItem
label={t('editor.editorToolbar.publishAndCreateNew')}
icon="add"
onClick={onPersistAndNew}
/>
) : null}
</ToolbarDropdown>
</div>
@ -245,23 +260,28 @@ export default class EditorToolbar extends React.Component {
isDeleting,
isNewEntry,
isModification,
t,
} = this.props;
const deleteLabel =
(hasUnpublishedChanges && isModification && 'Delete unpublished changes') ||
(hasUnpublishedChanges && (isNewEntry || !isModification) && 'Delete unpublished entry') ||
(!hasUnpublishedChanges && !isModification && 'Delete published entry');
(hasUnpublishedChanges &&
isModification &&
t('editor.editorToolbar.deleteUnpublishedChanges')) ||
(hasUnpublishedChanges &&
(isNewEntry || !isModification) &&
t('editor.editorToolbar.deleteUnpublishedEntry')) ||
(!hasUnpublishedChanges && !isModification && t('editor.editorToolbar.deletePublishedEntry'));
return [
<SaveButton key="save-button" onClick={() => hasChanged && onPersist()}>
{isPersisting ? 'Saving...' : 'Save'}
{isPersisting ? t('editor.editorToolbar.saving') : t('editor.editorToolbar.save')}
</SaveButton>,
isNewEntry || !deleteLabel ? null : (
<DeleteButton
key="delete-button"
onClick={hasUnpublishedChanges ? onDeleteUnpublishedChanges : onDelete}
>
{isDeleting ? 'Deleting...' : deleteLabel}
{isDeleting ? t('editor.editorToolbar.deleting') : deleteLabel}
</DeleteButton>
),
];
@ -277,6 +297,7 @@ export default class EditorToolbar extends React.Component {
onPublishAndNew,
currentStatus,
isNewEntry,
t,
} = this.props;
if (currentStatus) {
return (
@ -285,21 +306,25 @@ export default class EditorToolbar extends React.Component {
dropdownTopOverlap="40px"
dropdownWidth="120px"
renderButton={() => (
<StatusButton>{isUpdatingStatus ? 'Updating...' : 'Set status'}</StatusButton>
<StatusButton>
{isUpdatingStatus
? t('editor.editorToolbar.updating')
: t('editor.editorToolbar.setStatus')}
</StatusButton>
)}
>
<StatusDropdownItem
label="Draft"
label={t('editor.editorToolbar.draft')}
onClick={() => onChangeStatus('DRAFT')}
icon={currentStatus === status.get('DRAFT') && 'check'}
/>
<StatusDropdownItem
label="In review"
label={t('editor.editorToolbar.inReview')}
onClick={() => onChangeStatus('PENDING_REVIEW')}
icon={currentStatus === status.get('PENDING_REVIEW') && 'check'}
/>
<StatusDropdownItem
label="Ready"
label={t('editor.editorToolbar.ready')}
onClick={() => onChangeStatus('PENDING_PUBLISH')}
icon={currentStatus === status.get('PENDING_PUBLISH') && 'check'}
/>
@ -308,17 +333,25 @@ export default class EditorToolbar extends React.Component {
dropdownTopOverlap="40px"
dropdownWidth="150px"
renderButton={() => (
<PublishButton>{isPublishing ? 'Publishing...' : 'Publish'}</PublishButton>
<PublishButton>
{isPublishing
? t('editor.editorToolbar.publishing')
: t('editor.editorToolbar.publish')}
</PublishButton>
)}
>
<DropdownItem
label="Publish now"
label={t('editor.editorToolbar.publishNow')}
icon="arrow"
iconDirection="right"
onClick={onPublish}
/>
{collection.get('create') ? (
<DropdownItem label="Publish and create new" icon="add" onClick={onPublishAndNew} />
<DropdownItem
label={t('editor.editorToolbar.publishAndCreateNew')}
icon="add"
onClick={onPublishAndNew}
/>
) : null}
</ToolbarDropdown>
</>
@ -326,12 +359,12 @@ export default class EditorToolbar extends React.Component {
}
if (!isNewEntry) {
return <StatusPublished>Published</StatusPublished>;
return <StatusPublished>{t('editor.editorToolbar.published')}</StatusPublished>;
}
};
render() {
const { user, hasChanged, displayUrl, collection, hasWorkflow, onLogoutClick } = this.props;
const { user, hasChanged, displayUrl, collection, hasWorkflow, onLogoutClick, t } = this.props;
return (
<ToolbarContainer>
@ -339,12 +372,14 @@ export default class EditorToolbar extends React.Component {
<BackArrow></BackArrow>
<div>
<BackCollection>
Writing in <strong>{collection.get('label')}</strong> collection
{t('editor.editorToolbar.backCollection', {
collectionLabel: collection.get('label'),
})}
</BackCollection>
{hasChanged ? (
<BackStatusChanged>Unsaved Changes</BackStatusChanged>
<BackStatusChanged>{t('editor.editorToolbar.unsavedChanges')}</BackStatusChanged>
) : (
<BackStatusUnchanged>Changes saved</BackStatusUnchanged>
<BackStatusUnchanged>{t('editor.editorToolbar.changesSaved')}</BackStatusUnchanged>
)}
</div>
</ToolbarSectionBackLink>
@ -369,3 +404,5 @@ export default class EditorToolbar extends React.Component {
);
}
}
export default translate()(EditorToolbar);

View File

@ -1,10 +1,17 @@
import React from 'react';
import { translate } from 'react-polyglot';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
export default function UnknownControl({ field }) {
return <div>{`No control for widget '${field.get('widget')}'.`}</div>;
}
const UnknownControl = ({ field, t }) => {
return (
<div>{t('editor.editorWidgets.unknownControl.noControl', { widget: field.get('widget') })}</div>
);
};
UnknownControl.propTypes = {
field: ImmutablePropTypes.map,
t: PropTypes.func.isRequired,
};
export default translate()(UnknownControl);

View File

@ -1,15 +1,19 @@
import React from 'react';
import { translate } from 'react-polyglot';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
export default function UnknownPreview({ field }) {
const UnknownPreview = ({ field, t }) => {
return (
<div className="nc-widgetPreview">
No preview for widget {field.get('widget')}
.
{t('editor.editorWidgets.unknownPreview.noPreview', field.get('widget'))}
</div>
);
}
};
UnknownPreview.propTypes = {
field: ImmutablePropTypes.map,
t: PropTypes.func.isRequired,
};
export default translate()(UnknownPreview);

View File

@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { orderBy, map } from 'lodash';
import { Map } from 'immutable';
import { translate } from 'react-polyglot';
import fuzzy from 'fuzzy';
import { resolvePath, fileExtension } from 'netlify-cms-lib-util';
import {
@ -56,6 +57,7 @@ class MediaLibrary extends React.Component {
insertMedia: PropTypes.func.isRequired,
publicFolder: PropTypes.string,
closeMediaLibrary: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
/**
@ -212,8 +214,8 @@ class MediaLibrary extends React.Component {
*/
handleDelete = () => {
const { selectedFile } = this.state;
const { files, deleteMedia, privateUpload } = this.props;
if (!window.confirm('Are you sure you want to delete selected media?')) {
const { files, deleteMedia, privateUpload, t } = this.props;
if (!window.confirm(t('mediaLibrary.mediaLibrary.onDelete'))) {
return;
}
const file = files.find(file => selectedFile.key === file.key);
@ -285,6 +287,7 @@ class MediaLibrary extends React.Component {
hasNextPage,
isPaginating,
privateUpload,
t,
} = this.props;
return (
@ -316,6 +319,7 @@ class MediaLibrary extends React.Component {
handleAssetClick={this.handleAssetClick}
handleLoadMore={this.handleLoadMore}
getDisplayURL={this.getDisplayURL}
t={t}
/>
);
}
@ -358,4 +362,4 @@ const mapDispatchToProps = {
export default connect(
mapStateToProps,
mapDispatchToProps,
)(MediaLibrary);
)(translate()(MediaLibrary));

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import styled from 'react-emotion';
import { isEmpty } from 'lodash';
import { translate } from 'react-polyglot';
import { Modal } from 'UI';
import MediaLibrarySearch from './MediaLibrarySearch';
import MediaLibraryHeader from './MediaLibraryHeader';
@ -93,6 +94,7 @@ const MediaLibraryModal = ({
handleAssetClick,
handleLoadMore,
getDisplayURL,
t,
}) => {
const filteredFiles = forImage ? handleFilter(files) : files;
const queriedFiles = !dynamicSearch && query ? handleQuery(query, filteredFiles) : filteredFiles;
@ -103,11 +105,11 @@ const MediaLibraryModal = ({
const hasMedia = hasSearchResults;
const shouldShowEmptyMessage = !hasMedia;
const emptyMessage =
(isLoading && !hasMedia && 'Loading...') ||
(dynamicSearchActive && 'No results.') ||
(!hasFiles && 'No assets found.') ||
(!hasFilteredFiles && 'No images found.') ||
(!hasSearchResults && 'No results.');
(isLoading && !hasMedia && t('mediaLibrary.mediaLibraryModal.loading')) ||
(dynamicSearchActive && t('mediaLibrary.mediaLibraryModal.noResults')) ||
(!hasFiles && t('mediaLibrary.mediaLibraryModal.noAssetsFound')) ||
(!hasFilteredFiles && t('mediaLibrary.mediaLibraryModal.noImagesFound')) ||
(!hasSearchResults && t('mediaLibrary.mediaLibraryModal.noResults'));
const hasSelection = hasMedia && !isEmpty(selectedFile);
const shouldShowButtonLoader = isPersisting || isDeleting;
@ -117,21 +119,33 @@ const MediaLibraryModal = ({
<div>
<MediaLibraryHeader
onClose={handleClose}
title={`${privateUpload ? 'Private ' : ''}${forImage ? 'Images' : 'Media assets'}`}
title={`${privateUpload ? t('mediaLibrary.mediaLibraryModal.private') : ''}${
forImage
? t('mediaLibrary.mediaLibraryModal.images')
: t('mediaLibrary.mediaLibraryModal.mediaAssets')
}`}
isPrivate={privateUpload}
/>
<MediaLibrarySearch
value={query}
onChange={handleSearchChange}
onKeyDown={handleSearchKeyDown}
placeholder="Search..."
placeholder={t('mediaLibrary.mediaLibraryModal.search')}
disabled={!dynamicSearchActive && !hasFilteredFiles}
/>
</div>
<MediaLibraryActions
uploadButtonLabel={isPersisting ? 'Uploading...' : 'Upload new'}
deleteButtonLabel={isDeleting ? 'Deleting...' : 'Delete selected'}
insertButtonLabel="Choose selected"
uploadButtonLabel={
isPersisting
? t('mediaLibrary.mediaLibraryModal.uploading')
: t('mediaLibrary.mediaLibraryModal.uploadNew')
}
deleteButtonLabel={
isDeleting
? t('mediaLibrary.mediaLibraryModal.deleting')
: t('mediaLibrary.mediaLibraryModal.deleteSelected')
}
insertButtonLabel={t('mediaLibrary.mediaLibraryModal.chooseSelected')}
uploadEnabled={!shouldShowButtonLoader}
deleteEnabled={!shouldShowButtonLoader && hasSelection}
insertEnabled={hasSelection}
@ -153,7 +167,7 @@ const MediaLibraryModal = ({
canLoadMore={hasNextPage}
onLoadMore={handleLoadMore}
isPaginating={isPaginating}
paginatingMessage="Loading..."
paginatingMessage={t('mediaLibrary.mediaLibraryModal.loading')}
cardWidth={cardWidth}
cardMargin={cardMargin}
isPrivate={privateUpload}
@ -200,6 +214,7 @@ MediaLibraryModal.propTypes = {
handleAssetClick: PropTypes.func.isRequired,
handleLoadMore: PropTypes.func.isRequired,
getDisplayURL: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default MediaLibraryModal;
export default translate()(MediaLibraryModal);

View File

@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-polyglot';
import { css } from 'react-emotion';
import { colors } from 'netlify-cms-ui-default';
@ -14,9 +15,10 @@ const styles = {
`,
};
export class ErrorBoundary extends React.Component {
class ErrorBoundary extends React.Component {
static propTypes = {
children: PropTypes.node,
t: PropTypes.func.isRequired,
};
state = {
@ -34,23 +36,25 @@ export class ErrorBoundary extends React.Component {
if (!hasError) {
return this.props.children;
}
const t = this.props.t;
return (
<div className={styles.errorBoundary}>
<h1 className={styles.errorBoundaryText}>Sorry!</h1>
<h1 className={styles.errorBoundaryText}>{t('ui.errorBoundary.title')}</h1>
<p>
<span>{"There's been an error - please "}</span>
<span>{t('ui.errorBoundary.details')}</span>
<a
href={ISSUE_URL}
target="_blank"
rel="noopener noreferrer"
className={styles.errorBoundaryText}
>
report it
{t('ui.errorBoundary.reportIt')}
</a>
!
</p>
<p>{errorMessage}</p>
</div>
);
}
}
export default translate()(ErrorBoundary);

View File

@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled, { css } from 'react-emotion';
import { translate } from 'react-polyglot';
import { Icon, Dropdown, DropdownItem, DropdownButton, colors } from 'netlify-cms-ui-default';
import { stripProtocol } from 'Lib/urlHelper';
@ -47,7 +48,7 @@ Avatar.propTypes = {
imageUrl: PropTypes.string,
};
const SettingsDropdown = ({ displayUrl, imageUrl, onLogoutClick }) => (
const SettingsDropdown = ({ displayUrl, imageUrl, onLogoutClick, t }) => (
<React.Fragment>
{displayUrl ? (
<AppHeaderSiteLink href={displayUrl} target="_blank">
@ -64,7 +65,7 @@ const SettingsDropdown = ({ displayUrl, imageUrl, onLogoutClick }) => (
</DropdownButton>
)}
>
<DropdownItem label="Log Out" onClick={onLogoutClick} />
<DropdownItem label={t('ui.settingsDropdown.logOut')} onClick={onLogoutClick} />
</Dropdown>
</React.Fragment>
);
@ -73,6 +74,7 @@ SettingsDropdown.propTypes = {
displayUrl: PropTypes.string,
imageUrl: PropTypes.string,
onLogoutClick: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default SettingsDropdown;
export default translate()(SettingsDropdown);

View File

@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { css, injectGlobal, cx } from 'react-emotion';
import { translate } from 'react-polyglot';
import reduxNotificationsStyles from 'redux-notifications/lib/styles.css';
import { shadows, colors, lengths } from 'netlify-cms-ui-default';
@ -41,11 +42,16 @@ const styles = {
`,
};
export const Toast = ({ kind, message }) => (
<div className={cx(styles.toast, styles[kind])}>{message}</div>
const Toast = ({ kind, message, t }) => (
<div className={cx(styles.toast, styles[kind])}>
{t(message.key, { details: message.details })}
</div>
);
Toast.propTypes = {
kind: PropTypes.oneOf(['info', 'success', 'warning', 'danger']).isRequired,
message: PropTypes.string,
message: PropTypes.object,
t: PropTypes.func.isRequired,
};
export default translate()(Toast);

View File

@ -1,5 +1,5 @@
export { DragSource, DropTarget, HTML5DragDrop } from './DragDrop';
export { ErrorBoundary } from './ErrorBoundary';
export ErrorBoundary from './ErrorBoundary';
export { FileUploadButton } from './FileUploadButton';
export { Modal } from './Modal';
export { Toast } from './Toast';
export Toast from './Toast';

View File

@ -3,6 +3,7 @@ import React, { Component } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import styled from 'react-emotion';
import { OrderedMap } from 'immutable';
import { translate } from 'react-polyglot';
import { connect } from 'react-redux';
import {
Dropdown,
@ -60,6 +61,7 @@ class Workflow extends Component {
updateUnpublishedEntryStatus: PropTypes.func.isRequired,
publishUnpublishedEntry: PropTypes.func.isRequired,
deleteUnpublishedEntry: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
componentDidMount() {
@ -78,10 +80,11 @@ class Workflow extends Component {
publishUnpublishedEntry,
deleteUnpublishedEntry,
collections,
t,
} = this.props;
if (!isEditorialWorkflow) return null;
if (isFetching) return <Loader active>Loading Editorial Workflow Entries</Loader>;
if (isFetching) return <Loader active>{t('workflow.workflow.loading')}</Loader>;
const reviewCount = unpublishedEntries.get('pending_review').size;
const readyCount = unpublishedEntries.get('pending_publish').size;
@ -89,12 +92,14 @@ class Workflow extends Component {
<WorkflowContainer>
<WorkflowTop>
<WorkflowTopRow>
<WorkflowTopHeading>Editorial Workflow</WorkflowTopHeading>
<WorkflowTopHeading>{t('workflow.workflow.workflowHeading')}</WorkflowTopHeading>
<Dropdown
dropdownWidth="160px"
dropdownPosition="left"
dropdownTopOverlap="40px"
renderButton={() => <StyledDropdownButton>New Post</StyledDropdownButton>}
renderButton={() => (
<StyledDropdownButton>{t('workflow.workflow.newPost')}</StyledDropdownButton>
)}
>
{collections
.filter(collection => collection.get('create'))
@ -109,8 +114,10 @@ class Workflow extends Component {
</Dropdown>
</WorkflowTopRow>
<WorkflowTopDescription>
{reviewCount} {reviewCount === 1 ? 'entry' : 'entries'} waiting for review, {readyCount}{' '}
ready to go live.
{t('workflow.workflow.description', {
smart_count: reviewCount,
readyCount: readyCount,
})}
</WorkflowTopDescription>
</WorkflowTop>
<WorkflowList
@ -153,4 +160,4 @@ export default connect(
publishUnpublishedEntry,
deleteUnpublishedEntry,
},
)(Workflow);
)(translate()(Workflow));

View File

@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled, { css } from 'react-emotion';
import { translate } from 'react-polyglot';
import { Link } from 'react-router-dom';
import { components, colors, colorsRaw, transitions, buttons } from 'netlify-cms-ui-default';
@ -107,6 +108,7 @@ const WorkflowCard = ({
onDelete,
canPublish,
onPublish,
t,
}) => (
<WorkflowCardContainer>
<WorkflowLink to={editLink}>
@ -119,10 +121,14 @@ const WorkflowCard = ({
</WorkflowLink>
<CardButtonContainer>
<DeleteButton onClick={onDelete}>
{isModification ? 'Delete changes' : 'Delete new entry'}
{isModification
? t('workflow.workflowCard.deleteChanges')
: t('workflow.workflowCard.deleteNewEntry')}
</DeleteButton>
<PublishButton disabled={!canPublish} onClick={onPublish}>
{isModification ? 'Publish changes' : 'Publish new entry'}
{isModification
? t('workflow.workflowCard.publishChanges')
: t('workflow.workflowCard.publishNewEntry')}
</PublishButton>
</CardButtonContainer>
</WorkflowCardContainer>
@ -139,6 +145,7 @@ WorkflowCard.propTypes = {
onDelete: PropTypes.func.isRequired,
canPublish: PropTypes.bool.isRequired,
onPublish: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
export default WorkflowCard;
export default translate()(WorkflowCard);

View File

@ -3,6 +3,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import styled, { css, cx } from 'react-emotion';
import moment from 'moment';
import { translate } from 'react-polyglot';
import { colors, lengths } from 'netlify-cms-ui-default';
import { status } from 'Constants/publishModes';
import { DragSource, DropTarget, HTML5DragDrop } from 'UI';
@ -96,14 +97,14 @@ const ColumnCount = styled.p`
// This is a namespace so that we can only drop these elements on a DropTarget with the same
const DNDNamespace = 'cms-workflow';
const getColumnHeaderText = columnName => {
const getColumnHeaderText = (columnName, t) => {
switch (columnName) {
case 'draft':
return 'Drafts';
return t('workflow.workflowList.draftHeader');
case 'pending_review':
return 'In Review';
return t('workflow.workflowList.inReviewHeader');
case 'pending_publish':
return 'Ready';
return t('workflow.workflowList.readyHeader');
}
};
@ -113,6 +114,7 @@ class WorkflowList extends React.Component {
handleChangeStatus: PropTypes.func.isRequired,
handlePublish: PropTypes.func.isRequired,
handleDelete: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
handleChangeStatus = (newStatus, dragProps) => {
@ -123,20 +125,16 @@ class WorkflowList extends React.Component {
};
requestDelete = (collection, slug, ownStatus) => {
if (window.confirm('Are you sure you want to delete this entry?')) {
if (window.confirm(this.props.t('workflow.workflowList.onDeleteEntry'))) {
this.props.handleDelete(collection, slug, ownStatus);
}
};
requestPublish = (collection, slug, ownStatus) => {
if (ownStatus !== status.last()) {
window.alert(
`Only items with a "Ready" status can be published.
Please drag the card to the "Ready" column to enable publishing.`,
);
window.alert(this.props.t('workflow.workflowList.onPublishingNotReadyEntry'));
return;
} else if (!window.confirm('Are you sure you want to publish this entry?')) {
} else if (!window.confirm(this.props.t('workflow.workflowList.onPublishEntry'))) {
return;
}
this.props.handlePublish(collection, slug);
@ -155,9 +153,13 @@ Please drag the card to the "Ready" column to enable publishing.`,
{(connect, { isHovered }) =>
connect(
<div className={cx(styles.column, { [styles.columnHovered]: isHovered })}>
<ColumnHeader name={currColumn}>{getColumnHeaderText(currColumn)}</ColumnHeader>
<ColumnHeader name={currColumn}>
{getColumnHeaderText(currColumn, this.props.t)}
</ColumnHeader>
<ColumnCount>
{currEntries.size} {currEntries.size === 1 ? 'entry' : 'entries'}
{this.props.t('workflow.workflowList.currentEntries', {
smart_count: currEntries.size,
})}
</ColumnCount>
{this.renderColumns(currEntries, currColumn)}
</div>,
@ -218,4 +220,4 @@ Please drag the card to the "Ready" column to enable publishing.`,
}
}
export default HTML5DragDrop(WorkflowList);
export default HTML5DragDrop(translate()(WorkflowList));

View File

@ -0,0 +1,160 @@
export function getPhrases() {
return {
app: {
header: {
content: 'Contents',
workflow: 'Workflow',
media: 'Media',
quickAdd: 'Quick add',
},
app: {
errorHeader: 'Error loading the CMS configuration',
configErrors: 'Config Errors',
checkConfigYml: 'Check your config.yml file.',
loadingConfig: 'Loading configuration...',
waitingBackend: 'Waiting for backend...',
},
notFoundPage: {
header: 'Not Found',
},
},
collection: {
sidebar: {
collections: 'Collections',
searchAll: 'Search all',
},
collectionTop: {
viewAs: 'View as',
newButton: 'New %{collectionLabel}',
},
entries: {
loadingEntries: 'Loading Entries',
cachingEntries: 'Caching Entries',
longerLoading: 'This might take several minutes',
},
},
editor: {
editorControlPane: {
widget: {
required: '%{fieldLabel} is required.',
regexPattern: "%{fieldLabel} didn't match the pattern: %{pattern}.",
processing: '%{fieldLabel} is processing.',
},
},
editor: {
onLeavePage: 'Are you sure you want to leave this page?',
onUpdatingWithUnsavedChanges:
'You have unsaved changes, please save before updating status.',
onPublishingNotReady: 'Please update status to "Ready" before publishing.',
onPublishingWithUnsavedChanges: 'You have unsaved changes, please save before publishing.',
onPublishing: 'Are you sure you want to publish this entry?',
onDeleteWithUnsavedChanges:
'Are you sure you want to delete this published entry, as well as your unsaved changes from the current session?',
onDeletePublishedEntry: 'Are you sure you want to delete this published entry?',
onDeleteUnpublishedChangesWithUnsavedChanges:
'This will delete all unpublished changes to this entry, as well as your unsaved changes from the current session. Do you still want to delete?',
onDeleteUnpublishedChanges:
'All unpublished changes to this entry will be deleted. Do you still want to delete?',
loadingEntry: 'Loading entry...',
},
editorToolbar: {
publishing: 'Publishing...',
publish: 'Publish',
published: 'Published',
publishAndCreateNew: 'Publish and create new',
deleteUnpublishedChanges: 'Delete unpublished changes',
deleteUnpublishedEntry: 'Delete unpublished entry',
deletePublishedEntry: 'Delete published entry',
deleteEntry: 'Delete entry',
saving: 'Saving...',
save: 'Save',
deleting: 'Deleting...',
updating: 'Updating...',
setStatus: 'Set status',
backCollection: ' Writing in %{collectionLabel} collection',
unsavedChanges: 'Unsaved Changes',
changesSaved: 'Changes saved',
draft: 'Draft',
inReview: 'In review',
ready: 'Ready',
publishNow: 'Publish now',
},
editorWidgets: {
unknownControl: {
noControl: "No control for widget '%{widget}'.",
},
unknownPreview: {
noPreview: "No preview for widget '%{widget}'.",
},
},
},
mediaLibrary: {
mediaLibrary: {
onDelete: 'Are you sure you want to delete selected media?',
},
mediaLibraryModal: {
loading: 'Loading...',
noResults: 'No results.',
noAssetsFound: 'No assets found.',
noImagesFound: 'No images found.',
private: 'Private ',
images: 'Images',
mediaAssets: 'Media assets',
search: 'Search...',
uploading: 'Uploading...',
uploadNew: 'Upload new',
deleting: 'Deleting...',
deleteSelected: 'Delete selected',
chooseSelected: 'Choose selected',
},
},
ui: {
errorBoundary: {
title: 'Sorry!',
details: "There's been an error - please ",
reportIt: 'report it!',
},
settingsDropdown: {
logOut: 'Log Out',
},
toast: {
onFailToLoadEntries: 'Failed to load entry: %{details}',
onFailToPersist: 'Failed to persist entry: %{details}',
onFailToDelete: 'Failed to delete entry: %{details}',
onFailToUpdateStatus: 'Failed to update status: %{details}',
missingRequiredField:
"Oops, you've missed a required field. Please complete before saving.",
entrySaved: 'Entry saved',
entryPublished: 'Entry published',
onFailToPublishEntry: 'Failed to publish: %{details}',
entryUpdated: 'Entry status updated',
onDeleteUnpublishedChanges: 'Unpublished changes deleted',
},
},
workflow: {
workflow: {
loading: 'Loading Editorial Workflow Entries',
workflowHeading: 'Editorial Workflow',
newPost: 'New Post',
description:
'%{smart_count} entry waiting for review, %{readyCount} ready to go live. |||| %{smart_count} entries waiting for review, %{readyCount} ready to go live. ',
},
workflowCard: {
deleteChanges: 'Delete changes',
deleteNewEntry: 'Delete new entry',
publishChanges: 'Publish changes',
publishNewEntry: 'Publish new entry',
},
workflowList: {
onDeleteEntry: 'Are you sure you want to delete this entry?',
onPublishingNotReadyEntry:
'Only items with a "Ready" status can be published. Please drag the card to the "Ready" column to enable publishing.',
onPublishEntry: 'Are you sure you want to publish this entry?',
draftHeader: 'Drafts',
inReviewHeader: 'In Review',
readyHeader: 'Ready',
currentEntries: '%{smart_count} entry |||| %{smart_count} entries',
},
},
};
}

View File

@ -3858,7 +3858,7 @@ error-stack-parser@^2.0.0:
dependencies:
stackframe "^1.0.4"
es-abstract@^1.10.0, es-abstract@^1.4.3, es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
es-abstract@^1.10.0, es-abstract@^1.4.3, es-abstract@^1.5.0, es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165"
integrity sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==
@ -4548,6 +4548,12 @@ follow-redirects@^1.0.0:
dependencies:
debug "^3.1.0"
for-each@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
dependencies:
is-callable "^1.1.3"
for-in@^1.0.1, for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@ -7646,6 +7652,15 @@ node-notifier@^5.2.1:
shellwords "^0.1.1"
which "^1.3.0"
node-polyglot@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/node-polyglot/-/node-polyglot-2.3.0.tgz#e97cc9354e87e648f04858647c6e3be38ad36ce1"
dependencies:
for-each "^0.3.3"
has "^1.0.3"
string.prototype.trim "^1.1.2"
warning "^4.0.1"
node-pre-gyp@^0.10.0:
version "0.10.3"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc"
@ -8895,6 +8910,12 @@ react-onclickoutside@^6.5.0:
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz#6a5b5b8b4eae6b776259712c89c8a2b36b17be93"
integrity sha512-p84kBqGaMoa7VYT0vZ/aOYRfJB+gw34yjpda1Z5KeLflg70HipZOT+MXQenEhdkPAABuE2Astq4zEPdMqUQxcg==
react-polyglot@^0.2.6:
version "0.2.6"
resolved "https://registry.yarnpkg.com/react-polyglot/-/react-polyglot-0.2.6.tgz#5a7065aa82ee00a6d92a9c89447b2cc5b939986c"
dependencies:
prop-types "^15.5.8"
react-portal@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-3.2.0.tgz#4224e19b2b05d5cbe730a7ba0e34ec7585de0043"
@ -10453,6 +10474,14 @@ string.prototype.padend@^3.0.0:
es-abstract "^1.4.3"
function-bind "^1.0.2"
string.prototype.trim@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea"
dependencies:
define-properties "^1.1.2"
es-abstract "^1.5.0"
function-bind "^1.0.2"
string_decoder@^1.0.0, string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"