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 media_folder: assets/uploads
collections: # A list of collections the CMS should be able to edit collections: # A list of collections the CMS should be able to edit
- name: "posts" # Used in routes, ie.: /admin/collections/:slug/edit - name: 'posts' # Used in routes, ie.: /admin/collections/:slug/edit
label: "Posts" # Used in the UI label: 'Posts' # Used in the UI
label_singular: "Post" # Used in the UI, ie: "New Post" label_singular: 'Post' # Used in the UI, ie: "New Post"
description: > description: >
The description is a great place for tone setting, high level information, and editing The description is a great place for tone setting, high level information, and editing
guidelines that are specific to a collection. guidelines that are specific to a collection.
folder: "_posts" folder: '_posts'
slug: "{{year}}-{{month}}-{{day}}-{{slug}}" slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
create: true # Allow users to create new documents in this collection create: true # Allow users to create new documents in this collection
fields: # The fields each document in this collection have fields: # The fields each document in this collection have
- {label: "Title", name: "title", widget: "string", tagname: "h1"} - { label: 'Title', name: 'title', widget: 'string', tagname: 'h1' }
- {label: "Publish Date", name: "date", widget: "datetime", format: "YYYY-MM-DD hh:mma"} - { label: 'Publish Date', name: 'date', widget: 'datetime', format: 'YYYY-MM-DD hh:mma' }
- label: "Cover Image" - label: 'Cover Image'
name: "image" name: 'image'
widget: "image" widget: 'image'
required: false 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: 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 - name: 'faq' # Used in routes, ie.: /admin/collections/:slug/edit
label: "FAQ" # Used in the UI label: 'FAQ' # Used in the UI
folder: "_faqs" folder: '_faqs'
create: true # Allow users to create new documents in this collection create: true # Allow users to create new documents in this collection
fields: # The fields each document in this collection have fields: # The fields each document in this collection have
- {label: "Question", name: "title", widget: "string", tagname: "h1"} - { label: 'Question', name: 'title', widget: 'string', tagname: 'h1' }
- {label: "Answer", name: "body", widget: "markdown"} - { label: 'Answer', name: 'body', widget: 'markdown' }
- name: "settings" - name: 'settings'
label: "Settings" label: 'Settings'
delete: false # Prevent users from deleting documents in this collection delete: false # Prevent users from deleting documents in this collection
editor: editor:
preview: false preview: false
files: files:
- name: "general" - name: 'general'
label: "Site Settings" label: 'Site Settings'
file: "_data/settings.json" file: '_data/settings.json'
description: "General Site Settings" description: 'General Site Settings'
fields: fields:
- {label: "Global title", name: "site_title", widget: "string"} - { label: 'Global title', name: 'site_title', widget: 'string' }
- label: "Post Settings" - label: 'Post Settings'
name: posts name: posts
widget: "object" widget: 'object'
fields: fields:
- {label: "Number of posts on frontpage", name: front_limit, widget: number} - { label: 'Number of posts on frontpage', name: front_limit, widget: number }
- {label: "Default Author", name: author, widget: string} - { label: 'Default Author', name: author, widget: string }
- {label: "Default Thumbnail", name: thumb, widget: image, class: "thumb"} - { label: 'Default Thumbnail', name: thumb, widget: image, class: 'thumb' }
- name: "authors" - name: 'authors'
label: "Authors" label: 'Authors'
file: "_data/authors.yml" file: '_data/authors.yml'
description: "Author descriptions" description: 'Author descriptions'
fields: fields:
- name: authors - name: authors
label: Authors label: Authors
label_singular: "Author" label_singular: 'Author'
widget: list widget: list
fields: fields:
- {label: "Name", name: "name", widget: "string", hint: "First and Last"} - { label: 'Name', name: 'name', widget: 'string', hint: 'First and Last' }
- {label: "Description", name: "description", widget: "markdown"} - { label: 'Description', name: 'description', widget: 'markdown' }
- name: "kitchenSink" # all the things in one entry, for documentation and quick testing - name: 'kitchenSink' # all the things in one entry, for documentation and quick testing
label: "Kitchen Sink" label: 'Kitchen Sink'
folder: "_sink" folder: '_sink'
create: true create: true
fields: fields:
- label: "Related Post" - label: 'Related Post'
name: "post" name: 'post'
widget: "relationKitchenSinkPost" widget: 'relationKitchenSinkPost'
collection: "posts" collection: 'posts'
displayFields: ["title", "date"] displayFields: ['title', 'date']
searchFields: ["title", "body"] searchFields: ['title', 'body']
valueField: "title" valueField: 'title'
- {label: "Title", name: "title", widget: "string"} - { label: 'Title', name: 'title', widget: 'string' }
- {label: "Boolean", name: "boolean", widget: "boolean", default: true} - { label: 'Boolean', name: 'boolean', widget: 'boolean', default: true }
- {label: "Text", name: "text", widget: "text", hint: "Plain text, not markdown"} - { label: 'Text', name: 'text', widget: 'text', hint: 'Plain text, not markdown' }
- {label: "Number", name: "number", widget: "number", hint: "To infinity and beyond!"} - { label: 'Number', name: 'number', widget: 'number', hint: 'To infinity and beyond!' }
- {label: "Markdown", name: "markdown", widget: "markdown"} - { label: 'Markdown', name: 'markdown', widget: 'markdown' }
- {label: "Datetime", name: "datetime", widget: "datetime"} - { label: 'Datetime', name: 'datetime', widget: 'datetime' }
- {label: "Date", name: "date", widget: "date"} - { label: 'Date', name: 'date', widget: 'date' }
- {label: "Image", name: "image", widget: "image"} - { label: 'Image', name: 'image', widget: 'image' }
- {label: "File", name: "file", widget: "file"} - { label: 'File', name: 'file', widget: 'file' }
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]} - { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
- {label: "Hidden", name: "hidden", widget: "hidden", default: "hidden"} - { label: 'Hidden', name: 'hidden', widget: 'hidden', default: 'hidden' }
- label: "Object" - label: 'Object'
name: "object" name: 'object'
widget: "object" widget: 'object'
fields: fields:
- label: "Related Post" - label: 'Related Post'
name: "post" name: 'post'
widget: "relationKitchenSinkPost" widget: 'relationKitchenSinkPost'
collection: "posts" collection: 'posts'
searchFields: ["title", "body"] searchFields: ['title', 'body']
valueField: "title" valueField: 'title'
- {label: "String", name: "string", widget: "string"} - { label: 'String', name: 'string', widget: 'string' }
- {label: "Boolean", name: "boolean", widget: "boolean", default: false} - { label: 'Boolean', name: 'boolean', widget: 'boolean', default: false }
- {label: "Text", name: "text", widget: "text"} - { label: 'Text', name: 'text', widget: 'text' }
- {label: "Number", name: "number", widget: "number"} - { label: 'Number', name: 'number', widget: 'number' }
- {label: "Markdown", name: "markdown", widget: "markdown"} - { label: 'Markdown', name: 'markdown', widget: 'markdown' }
- {label: "Datetime", name: "datetime", widget: "datetime"} - { label: 'Datetime', name: 'datetime', widget: 'datetime' }
- {label: "Date", name: "date", widget: "date"} - { label: 'Date', name: 'date', widget: 'date' }
- {label: "Image", name: "image", widget: "image"} - { label: 'Image', name: 'image', widget: 'image' }
- {label: "File", name: "file", widget: "file"} - { label: 'File', name: 'file', widget: 'file' }
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]} - { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
- label: "List" - label: 'List'
name: "list" name: 'list'
widget: "list" widget: 'list'
fields: fields:
- {label: "String", name: "string", widget: "string"} - { label: 'String', name: 'string', widget: 'string' }
- {label: "Boolean", name: "boolean", widget: "boolean"} - { label: 'Boolean', name: 'boolean', widget: 'boolean' }
- {label: "Text", name: "text", widget: "text"} - { label: 'Text', name: 'text', widget: 'text' }
- {label: "Number", name: "number", widget: "number"} - { label: 'Number', name: 'number', widget: 'number' }
- {label: "Markdown", name: "markdown", widget: "markdown"} - { label: 'Markdown', name: 'markdown', widget: 'markdown' }
- {label: "Datetime", name: "datetime", widget: "datetime"} - { label: 'Datetime', name: 'datetime', widget: 'datetime' }
- {label: "Date", name: "date", widget: "date"} - { label: 'Date', name: 'date', widget: 'date' }
- {label: "Image", name: "image", widget: "image"} - { label: 'Image', name: 'image', widget: 'image' }
- {label: "File", name: "file", widget: "file"} - { label: 'File', name: 'file', widget: 'file' }
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]} - { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
- label: "Object" - label: 'Object'
name: "object" name: 'object'
widget: "object" widget: 'object'
fields: fields:
- {label: "String", name: "string", widget: "string"} - { label: 'String', name: 'string', widget: 'string' }
- {label: "Boolean", name: "boolean", widget: "boolean"} - { label: 'Boolean', name: 'boolean', widget: 'boolean' }
- {label: "Text", name: "text", widget: "text"} - { label: 'Text', name: 'text', widget: 'text' }
- {label: "Number", name: "number", widget: "number"} - { label: 'Number', name: 'number', widget: 'number' }
- {label: "Markdown", name: "markdown", widget: "markdown"} - { label: 'Markdown', name: 'markdown', widget: 'markdown' }
- {label: "Datetime", name: "datetime", widget: "datetime"} - { label: 'Datetime', name: 'datetime', widget: 'datetime' }
- {label: "Date", name: "date", widget: "date"} - { label: 'Date', name: 'date', widget: 'date' }
- {label: "Image", name: "image", widget: "image"} - { label: 'Image', name: 'image', widget: 'image' }
- {label: "File", name: "file", widget: "file"} - { label: 'File', name: 'file', widget: 'file' }
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]} - { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
- label: "List" - label: 'List'
name: "list" name: 'list'
widget: "list" widget: 'list'
fields: fields:
- label: "Related Post" - label: 'Related Post'
name: "post" name: 'post'
widget: "relationKitchenSinkPost" widget: 'relationKitchenSinkPost'
collection: "posts" collection: 'posts'
searchFields: ["title", "body"] searchFields: ['title', 'body']
valueField: "title" valueField: 'title'
- {label: "String", name: "string", widget: "string"} - { label: 'String', name: 'string', widget: 'string' }
- {label: "Boolean", name: "boolean", widget: "boolean"} - { label: 'Boolean', name: 'boolean', widget: 'boolean' }
- {label: "Text", name: "text", widget: "text"} - { label: 'Text', name: 'text', widget: 'text' }
- {label: "Number", name: "number", widget: "number"} - { label: 'Number', name: 'number', widget: 'number' }
- {label: "Markdown", name: "markdown", widget: "markdown"} - { label: 'Markdown', name: 'markdown', widget: 'markdown' }
- {label: "Datetime", name: "datetime", widget: "datetime"} - { label: 'Datetime', name: 'datetime', widget: 'datetime' }
- {label: "Date", name: "date", widget: "date"} - { label: 'Date', name: 'date', widget: 'date' }
- {label: "Image", name: "image", widget: "image"} - { label: 'Image', name: 'image', widget: 'image' }
- {label: "File", name: "file", widget: "file"} - { label: 'File', name: 'file', widget: 'file' }
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]} - { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
- {label: "Hidden", name: "hidden", widget: "hidden", default: "hidden"} - { label: 'Hidden', name: 'hidden', widget: 'hidden', default: 'hidden' }
- label: "Object" - label: 'Object'
name: "object" name: 'object'
widget: "object" widget: 'object'
fields: fields:
- {label: "String", name: "string", widget: "string"} - { label: 'String', name: 'string', widget: 'string' }
- {label: "Boolean", name: "boolean", widget: "boolean"} - { label: 'Boolean', name: 'boolean', widget: 'boolean' }
- {label: "Text", name: "text", widget: "text"} - { label: 'Text', name: 'text', widget: 'text' }
- {label: "Number", name: "number", widget: "number"} - { label: 'Number', name: 'number', widget: 'number' }
- {label: "Markdown", name: "markdown", widget: "markdown"} - { label: 'Markdown', name: 'markdown', widget: 'markdown' }
- {label: "Datetime", name: "datetime", widget: "datetime"} - { label: 'Datetime', name: 'datetime', widget: 'datetime' }
- {label: "Date", name: "date", widget: "date"} - { label: 'Date', name: 'date', widget: 'date' }
- {label: "Image", name: "image", widget: "image"} - { label: 'Image', name: 'image', widget: 'image' }
- {label: "File", name: "file", widget: "file"} - { label: 'File', name: 'file', widget: 'file' }
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]} - {
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-auth": "^2.0.4",
"netlify-cms-lib-util": "^2.1.0", "netlify-cms-lib-util": "^2.1.0",
"netlify-cms-ui-default": "^2.0.6", "netlify-cms-ui-default": "^2.0.6",
"node-polyglot": "^2.3.0",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"react": "^16.4.1", "react": "^16.4.1",
"react-dnd": "^2.5.4", "react-dnd": "^2.5.4",
@ -49,6 +50,7 @@
"react-immutable-proptypes": "^2.1.0", "react-immutable-proptypes": "^2.1.0",
"react-is": "16.3.1", "react-is": "16.3.1",
"react-modal": "^3.1.5", "react-modal": "^3.1.5",
"react-polyglot": "^0.2.6",
"react-redux": "^4.4.0", "react-redux": "^4.4.0",
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"react-router-redux": "^5.0.0-alpha.8", "react-router-redux": "^5.0.0-alpha.8",

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import styled, { css } from 'react-emotion'; import styled, { css } from 'react-emotion';
import { translate } from 'react-polyglot';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { import {
Icon, Icon,
@ -99,7 +100,7 @@ const AppHeaderQuickNewButton = styled(StyledDropdownButton)`
} }
`; `;
export default class Header extends React.Component { class Header extends React.Component {
static propTypes = { static propTypes = {
user: ImmutablePropTypes.map.isRequired, user: ImmutablePropTypes.map.isRequired,
collections: ImmutablePropTypes.orderedMap.isRequired, collections: ImmutablePropTypes.orderedMap.isRequired,
@ -108,6 +109,7 @@ export default class Header extends React.Component {
openMediaLibrary: PropTypes.func.isRequired, openMediaLibrary: PropTypes.func.isRequired,
hasWorkflow: PropTypes.bool.isRequired, hasWorkflow: PropTypes.bool.isRequired,
displayUrl: PropTypes.string, displayUrl: PropTypes.string,
t: PropTypes.func.isRequired,
}; };
handleCreatePostClick = collectionName => { handleCreatePostClick = collectionName => {
@ -125,6 +127,7 @@ export default class Header extends React.Component {
openMediaLibrary, openMediaLibrary,
hasWorkflow, hasWorkflow,
displayUrl, displayUrl,
t,
showMediaButton, showMediaButton,
} = this.props; } = this.props;
@ -143,25 +146,27 @@ export default class Header extends React.Component {
isActive={(match, location) => location.pathname.startsWith('/collections/')} isActive={(match, location) => location.pathname.startsWith('/collections/')}
> >
<Icon type="page" /> <Icon type="page" />
Content {t('app.header.content')}
</AppHeaderNavLink> </AppHeaderNavLink>
{hasWorkflow ? ( {hasWorkflow ? (
<AppHeaderNavLink to="/workflow" activeClassName="header-link-active"> <AppHeaderNavLink to="/workflow" activeClassName="header-link-active">
<Icon type="workflow" /> <Icon type="workflow" />
Workflow {t('app.header.workflow')}
</AppHeaderNavLink> </AppHeaderNavLink>
) : null} ) : null}
{showMediaButton ? ( {showMediaButton ? (
<AppHeaderButton onClick={openMediaLibrary}> <AppHeaderButton onClick={openMediaLibrary}>
<Icon type="media-alt" /> <Icon type="media-alt" />
Media {t('app.header.media')}
</AppHeaderButton> </AppHeaderButton>
) : null} ) : null}
</nav> </nav>
<AppHeaderActions> <AppHeaderActions>
{createableCollections.size > 0 && ( {createableCollections.size > 0 && (
<Dropdown <Dropdown
renderButton={() => <AppHeaderQuickNewButton>Quick add</AppHeaderQuickNewButton>} renderButton={() => (
<AppHeaderQuickNewButton> {t('app.header.quickAdd')}</AppHeaderQuickNewButton>
)}
dropdownTopOverlap="30px" dropdownTopOverlap="30px"
dropdownWidth="160px" dropdownWidth="160px"
dropdownPosition="left" 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 React from 'react';
import styled from 'react-emotion'; import styled from 'react-emotion';
import { translate } from 'react-polyglot';
import { lengths } from 'netlify-cms-ui-default'; import { lengths } from 'netlify-cms-ui-default';
import PropTypes from 'prop-types';
const NotFoundContainer = styled.div` const NotFoundContainer = styled.div`
margin: ${lengths.pageMargin}; margin: ${lengths.pageMargin};
`; `;
const NotFoundPage = () => ( const NotFoundPage = ({ t }) => (
<NotFoundContainer> <NotFoundContainer>
<h2>Not Found</h2> <h2>{t('app.notFoundPage.header')}</h2>
</NotFoundContainer> </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 PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import styled from 'react-emotion'; import styled from 'react-emotion';
import { translate } from 'react-polyglot';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Icon, components, buttons, shadows, colors } from 'netlify-cms-ui-default'; import { Icon, components, buttons, shadows, colors } from 'netlify-cms-ui-default';
import { VIEW_STYLE_LIST, VIEW_STYLE_GRID } from 'Constants/collectionViews'; import { VIEW_STYLE_LIST, VIEW_STYLE_GRID } from 'Constants/collectionViews';
@ -70,6 +71,7 @@ const CollectionTop = ({
viewStyle, viewStyle,
onChangeViewStyle, onChangeViewStyle,
newEntryUrl, newEntryUrl,
t,
}) => { }) => {
return ( return (
<CollectionTopContainer> <CollectionTopContainer>
@ -77,7 +79,9 @@ const CollectionTop = ({
<CollectionTopHeading>{collectionLabel}</CollectionTopHeading> <CollectionTopHeading>{collectionLabel}</CollectionTopHeading>
{newEntryUrl ? ( {newEntryUrl ? (
<CollectionTopNewButton to={newEntryUrl}> <CollectionTopNewButton to={newEntryUrl}>
{`New ${collectionLabelSingular || collectionLabel}`} {t('collection.collectionTop.newButton', {
collectionLabel: collectionLabelSingular || collectionLabel,
})}
</CollectionTopNewButton> </CollectionTopNewButton>
) : null} ) : null}
</CollectionTopRow> </CollectionTopRow>
@ -85,7 +89,7 @@ const CollectionTop = ({
<CollectionTopDescription>{collectionDescription}</CollectionTopDescription> <CollectionTopDescription>{collectionDescription}</CollectionTopDescription>
) : null} ) : null}
<ViewControls> <ViewControls>
<ViewControlsText>View as:</ViewControlsText> <ViewControlsText>{t('collection.collectionTop.viewAs')}:</ViewControlsText>
<ViewControlsButton <ViewControlsButton
isActive={viewStyle === VIEW_STYLE_LIST} isActive={viewStyle === VIEW_STYLE_LIST}
onClick={() => onChangeViewStyle(VIEW_STYLE_LIST)} onClick={() => onChangeViewStyle(VIEW_STYLE_LIST)}
@ -110,6 +114,7 @@ CollectionTop.propTypes = {
viewStyle: PropTypes.oneOf([VIEW_STYLE_LIST, VIEW_STYLE_GRID]).isRequired, viewStyle: PropTypes.oneOf([VIEW_STYLE_LIST, VIEW_STYLE_GRID]).isRequired,
onChangeViewStyle: PropTypes.func.isRequired, onChangeViewStyle: PropTypes.func.isRequired,
newEntryUrl: PropTypes.string, 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 PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { translate } from 'react-polyglot';
import { Loader } from 'netlify-cms-ui-default'; import { Loader } from 'netlify-cms-ui-default';
import EntryListing from './EntryListing'; import EntryListing from './EntryListing';
@ -12,8 +13,13 @@ const Entries = ({
viewStyle, viewStyle,
cursor, cursor,
handleCursorActions, 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) { if (entries) {
return ( return (
@ -44,6 +50,7 @@ Entries.propTypes = {
viewStyle: PropTypes.string, viewStyle: PropTypes.string,
cursor: PropTypes.any.isRequired, cursor: PropTypes.any.isRequired,
handleCursorActions: PropTypes.func.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 PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import styled, { css } from 'react-emotion'; import styled, { css } from 'react-emotion';
import { translate } from 'react-polyglot';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { Icon, components, colors, colorsRaw, lengths } from 'netlify-cms-ui-default'; import { Icon, components, colors, colorsRaw, lengths } from 'netlify-cms-ui-default';
import { searchCollections } from 'Actions/collections'; 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 = { static propTypes = {
collections: ImmutablePropTypes.orderedMap.isRequired, collections: ImmutablePropTypes.orderedMap.isRequired,
searchTerm: PropTypes.string, searchTerm: PropTypes.string,
t: PropTypes.func.isRequired,
}; };
static defaultProps = { static defaultProps = {
@ -114,18 +116,18 @@ export default class Sidebar extends React.Component {
}; };
render() { render() {
const { collections } = this.props; const { collections, t } = this.props;
const { query } = this.state; const { query } = this.state;
return ( return (
<SidebarContainer> <SidebarContainer>
<SidebarHeading>Collections</SidebarHeading> <SidebarHeading>{t('collection.sidebar.collections')}</SidebarHeading>
<SearchContainer> <SearchContainer>
<Icon type="search" size="small" /> <Icon type="search" size="small" />
<SearchInput <SearchInput
onChange={e => this.setState({ query: e.target.value })} onChange={e => this.setState({ query: e.target.value })}
onKeyDown={e => e.key === 'Enter' && searchCollections(query)} onKeyDown={e => e.key === 'Enter' && searchCollections(query)}
placeholder="Search all" placeholder={t('collection.sidebar.searchAll')}
value={query} value={query}
/> />
</SearchContainer> </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 ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Loader } from 'netlify-cms-ui-default'; import { Loader } from 'netlify-cms-ui-default';
import { translate } from 'react-polyglot';
import history from 'Routing/history'; import history from 'Routing/history';
import { logoutUser } from 'Actions/auth'; import { logoutUser } from 'Actions/auth';
import { import {
@ -69,6 +70,7 @@ class Editor extends React.Component {
pathname: PropTypes.string, pathname: PropTypes.string,
}), }),
hasChanged: PropTypes.bool, hasChanged: PropTypes.bool,
t: PropTypes.func.isRequired,
}; };
componentDidMount() { componentDidMount() {
@ -80,6 +82,7 @@ class Editor extends React.Component {
createEmptyDraft, createEmptyDraft,
loadEntries, loadEntries,
collectionEntriesLoaded, collectionEntriesLoaded,
t,
} = this.props; } = this.props;
if (newEntry) { if (newEntry) {
@ -88,7 +91,7 @@ class Editor extends React.Component {
loadEntry(collection, slug); loadEntry(collection, slug);
} }
const leaveMessage = 'Are you sure you want to leave this page?'; const leaveMessage = t('editor.editor.onLeavePage');
this.exitBlocker = event => { this.exitBlocker = event => {
if (this.props.entryDraft.get('hasChanged')) { if (this.props.entryDraft.get('hasChanged')) {
@ -190,9 +193,10 @@ class Editor extends React.Component {
collection, collection,
slug, slug,
currentStatus, currentStatus,
t,
} = this.props; } = this.props;
if (entryDraft.get('hasChanged')) { if (entryDraft.get('hasChanged')) {
window.alert('You have unsaved changes, please save before updating status.'); window.alert(t('editor.editor.onUpdatingWithUnsavedChanges'));
return; return;
} }
const newStatus = status.get(newStatusName); const newStatus = status.get(newStatusName);
@ -230,14 +234,15 @@ class Editor extends React.Component {
slug, slug,
currentStatus, currentStatus,
loadEntry, loadEntry,
t,
} = this.props; } = this.props;
if (currentStatus !== status.last()) { if (currentStatus !== status.last()) {
window.alert('Please update status to "Ready" before publishing.'); window.alert(t('editor.editor.onPublishingNotReady'));
return; return;
} else if (entryDraft.get('hasChanged')) { } else if (entryDraft.get('hasChanged')) {
window.alert('You have unsaved changes, please save before publishing.'); window.alert(t('editor.editor.onPublishingWithUnsavedChanges'));
return; return;
} else if (!window.confirm('Are you sure you want to publish this entry?')) { } else if (!window.confirm(t('editor.editor.onPublishing'))) {
return; return;
} }
@ -251,16 +256,12 @@ class Editor extends React.Component {
}; };
handleDeleteEntry = () => { handleDeleteEntry = () => {
const { entryDraft, newEntry, collection, deleteEntry, slug } = this.props; const { entryDraft, newEntry, collection, deleteEntry, slug, t } = this.props;
if (entryDraft.get('hasChanged')) { if (entryDraft.get('hasChanged')) {
if ( if (!window.confirm(t('editor.editor.onDeleteWithUnsavedChanges'))) {
!window.confirm(
'Are you sure you want to delete this published entry, as well as your unsaved changes from the current session?',
)
) {
return; return;
} }
} else if (!window.confirm('Are you sure you want to delete this published entry?')) { } else if (!window.confirm(t('editor.editor.onDeletePublishedEntry'))) {
return; return;
} }
if (newEntry) { if (newEntry) {
@ -281,19 +282,14 @@ class Editor extends React.Component {
deleteUnpublishedEntry, deleteUnpublishedEntry,
loadEntry, loadEntry,
isModification, isModification,
t,
} = this.props; } = this.props;
if ( if (
entryDraft.get('hasChanged') && entryDraft.get('hasChanged') &&
!window.confirm( !window.confirm(t('editor.editor.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?',
)
) { ) {
return; return;
} else if ( } else if (!window.confirm(t('editor.editor.onDeleteUnpublishedChanges'))) {
!window.confirm(
'All unpublished changes to this entry will be deleted. Do you still want to delete?',
)
) {
return; return;
} }
await deleteUnpublishedEntry(collection.get('name'), slug); await deleteUnpublishedEntry(collection.get('name'), slug);
@ -323,6 +319,7 @@ class Editor extends React.Component {
isModification, isModification,
currentStatus, currentStatus,
logoutUser, logoutUser,
t,
} = this.props; } = this.props;
if (entry && entry.get('error')) { if (entry && entry.get('error')) {
@ -336,7 +333,7 @@ class Editor extends React.Component {
entryDraft.get('entry') === undefined || entryDraft.get('entry') === undefined ||
(entry && entry.get('isFetching')) (entry && entry.get('isFetching'))
) { ) {
return <Loader active>Loading entry...</Loader>; return <Loader active>{t('editor.editor.loadingEntry')}</Loader>;
} }
return ( return (
@ -422,4 +419,4 @@ export default connect(
deleteUnpublishedEntry, deleteUnpublishedEntry,
logoutUser, logoutUser,
}, },
)(withWorkflow(Editor)); )(withWorkflow(translate()(Editor)));

View File

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

View File

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

View File

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

View File

@ -1,15 +1,19 @@
import React from 'react'; import React from 'react';
import { translate } from 'react-polyglot';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
export default function UnknownPreview({ field }) { const UnknownPreview = ({ field, t }) => {
return ( return (
<div className="nc-widgetPreview"> <div className="nc-widgetPreview">
No preview for widget {field.get('widget')} {t('editor.editorWidgets.unknownPreview.noPreview', field.get('widget'))}
.
</div> </div>
); );
} };
UnknownPreview.propTypes = { UnknownPreview.propTypes = {
field: ImmutablePropTypes.map, 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 { connect } from 'react-redux';
import { orderBy, map } from 'lodash'; import { orderBy, map } from 'lodash';
import { Map } from 'immutable'; import { Map } from 'immutable';
import { translate } from 'react-polyglot';
import fuzzy from 'fuzzy'; import fuzzy from 'fuzzy';
import { resolvePath, fileExtension } from 'netlify-cms-lib-util'; import { resolvePath, fileExtension } from 'netlify-cms-lib-util';
import { import {
@ -56,6 +57,7 @@ class MediaLibrary extends React.Component {
insertMedia: PropTypes.func.isRequired, insertMedia: PropTypes.func.isRequired,
publicFolder: PropTypes.string, publicFolder: PropTypes.string,
closeMediaLibrary: PropTypes.func.isRequired, closeMediaLibrary: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
}; };
/** /**
@ -212,8 +214,8 @@ class MediaLibrary extends React.Component {
*/ */
handleDelete = () => { handleDelete = () => {
const { selectedFile } = this.state; const { selectedFile } = this.state;
const { files, deleteMedia, privateUpload } = this.props; const { files, deleteMedia, privateUpload, t } = this.props;
if (!window.confirm('Are you sure you want to delete selected media?')) { if (!window.confirm(t('mediaLibrary.mediaLibrary.onDelete'))) {
return; return;
} }
const file = files.find(file => selectedFile.key === file.key); const file = files.find(file => selectedFile.key === file.key);
@ -285,6 +287,7 @@ class MediaLibrary extends React.Component {
hasNextPage, hasNextPage,
isPaginating, isPaginating,
privateUpload, privateUpload,
t,
} = this.props; } = this.props;
return ( return (
@ -316,6 +319,7 @@ class MediaLibrary extends React.Component {
handleAssetClick={this.handleAssetClick} handleAssetClick={this.handleAssetClick}
handleLoadMore={this.handleLoadMore} handleLoadMore={this.handleLoadMore}
getDisplayURL={this.getDisplayURL} getDisplayURL={this.getDisplayURL}
t={t}
/> />
); );
} }
@ -358,4 +362,4 @@ const mapDispatchToProps = {
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps, mapDispatchToProps,
)(MediaLibrary); )(translate()(MediaLibrary));

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled from 'react-emotion'; import styled from 'react-emotion';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { translate } from 'react-polyglot';
import { Modal } from 'UI'; import { Modal } from 'UI';
import MediaLibrarySearch from './MediaLibrarySearch'; import MediaLibrarySearch from './MediaLibrarySearch';
import MediaLibraryHeader from './MediaLibraryHeader'; import MediaLibraryHeader from './MediaLibraryHeader';
@ -93,6 +94,7 @@ const MediaLibraryModal = ({
handleAssetClick, handleAssetClick,
handleLoadMore, handleLoadMore,
getDisplayURL, getDisplayURL,
t,
}) => { }) => {
const filteredFiles = forImage ? handleFilter(files) : files; const filteredFiles = forImage ? handleFilter(files) : files;
const queriedFiles = !dynamicSearch && query ? handleQuery(query, filteredFiles) : filteredFiles; const queriedFiles = !dynamicSearch && query ? handleQuery(query, filteredFiles) : filteredFiles;
@ -103,11 +105,11 @@ const MediaLibraryModal = ({
const hasMedia = hasSearchResults; const hasMedia = hasSearchResults;
const shouldShowEmptyMessage = !hasMedia; const shouldShowEmptyMessage = !hasMedia;
const emptyMessage = const emptyMessage =
(isLoading && !hasMedia && 'Loading...') || (isLoading && !hasMedia && t('mediaLibrary.mediaLibraryModal.loading')) ||
(dynamicSearchActive && 'No results.') || (dynamicSearchActive && t('mediaLibrary.mediaLibraryModal.noResults')) ||
(!hasFiles && 'No assets found.') || (!hasFiles && t('mediaLibrary.mediaLibraryModal.noAssetsFound')) ||
(!hasFilteredFiles && 'No images found.') || (!hasFilteredFiles && t('mediaLibrary.mediaLibraryModal.noImagesFound')) ||
(!hasSearchResults && 'No results.'); (!hasSearchResults && t('mediaLibrary.mediaLibraryModal.noResults'));
const hasSelection = hasMedia && !isEmpty(selectedFile); const hasSelection = hasMedia && !isEmpty(selectedFile);
const shouldShowButtonLoader = isPersisting || isDeleting; const shouldShowButtonLoader = isPersisting || isDeleting;
@ -117,21 +119,33 @@ const MediaLibraryModal = ({
<div> <div>
<MediaLibraryHeader <MediaLibraryHeader
onClose={handleClose} 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} isPrivate={privateUpload}
/> />
<MediaLibrarySearch <MediaLibrarySearch
value={query} value={query}
onChange={handleSearchChange} onChange={handleSearchChange}
onKeyDown={handleSearchKeyDown} onKeyDown={handleSearchKeyDown}
placeholder="Search..." placeholder={t('mediaLibrary.mediaLibraryModal.search')}
disabled={!dynamicSearchActive && !hasFilteredFiles} disabled={!dynamicSearchActive && !hasFilteredFiles}
/> />
</div> </div>
<MediaLibraryActions <MediaLibraryActions
uploadButtonLabel={isPersisting ? 'Uploading...' : 'Upload new'} uploadButtonLabel={
deleteButtonLabel={isDeleting ? 'Deleting...' : 'Delete selected'} isPersisting
insertButtonLabel="Choose selected" ? 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} uploadEnabled={!shouldShowButtonLoader}
deleteEnabled={!shouldShowButtonLoader && hasSelection} deleteEnabled={!shouldShowButtonLoader && hasSelection}
insertEnabled={hasSelection} insertEnabled={hasSelection}
@ -153,7 +167,7 @@ const MediaLibraryModal = ({
canLoadMore={hasNextPage} canLoadMore={hasNextPage}
onLoadMore={handleLoadMore} onLoadMore={handleLoadMore}
isPaginating={isPaginating} isPaginating={isPaginating}
paginatingMessage="Loading..." paginatingMessage={t('mediaLibrary.mediaLibraryModal.loading')}
cardWidth={cardWidth} cardWidth={cardWidth}
cardMargin={cardMargin} cardMargin={cardMargin}
isPrivate={privateUpload} isPrivate={privateUpload}
@ -200,6 +214,7 @@ MediaLibraryModal.propTypes = {
handleAssetClick: PropTypes.func.isRequired, handleAssetClick: PropTypes.func.isRequired,
handleLoadMore: PropTypes.func.isRequired, handleLoadMore: PropTypes.func.isRequired,
getDisplayURL: 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 React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { translate } from 'react-polyglot';
import { css } from 'react-emotion'; import { css } from 'react-emotion';
import { colors } from 'netlify-cms-ui-default'; 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 = { static propTypes = {
children: PropTypes.node, children: PropTypes.node,
t: PropTypes.func.isRequired,
}; };
state = { state = {
@ -34,23 +36,25 @@ export class ErrorBoundary extends React.Component {
if (!hasError) { if (!hasError) {
return this.props.children; return this.props.children;
} }
const t = this.props.t;
return ( return (
<div className={styles.errorBoundary}> <div className={styles.errorBoundary}>
<h1 className={styles.errorBoundaryText}>Sorry!</h1> <h1 className={styles.errorBoundaryText}>{t('ui.errorBoundary.title')}</h1>
<p> <p>
<span>{"There's been an error - please "}</span> <span>{t('ui.errorBoundary.details')}</span>
<a <a
href={ISSUE_URL} href={ISSUE_URL}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={styles.errorBoundaryText} className={styles.errorBoundaryText}
> >
report it {t('ui.errorBoundary.reportIt')}
</a> </a>
!
</p> </p>
<p>{errorMessage}</p> <p>{errorMessage}</p>
</div> </div>
); );
} }
} }
export default translate()(ErrorBoundary);

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled, { css } from 'react-emotion'; import styled, { css } from 'react-emotion';
import { translate } from 'react-polyglot';
import { Icon, Dropdown, DropdownItem, DropdownButton, colors } from 'netlify-cms-ui-default'; import { Icon, Dropdown, DropdownItem, DropdownButton, colors } from 'netlify-cms-ui-default';
import { stripProtocol } from 'Lib/urlHelper'; import { stripProtocol } from 'Lib/urlHelper';
@ -47,7 +48,7 @@ Avatar.propTypes = {
imageUrl: PropTypes.string, imageUrl: PropTypes.string,
}; };
const SettingsDropdown = ({ displayUrl, imageUrl, onLogoutClick }) => ( const SettingsDropdown = ({ displayUrl, imageUrl, onLogoutClick, t }) => (
<React.Fragment> <React.Fragment>
{displayUrl ? ( {displayUrl ? (
<AppHeaderSiteLink href={displayUrl} target="_blank"> <AppHeaderSiteLink href={displayUrl} target="_blank">
@ -64,7 +65,7 @@ const SettingsDropdown = ({ displayUrl, imageUrl, onLogoutClick }) => (
</DropdownButton> </DropdownButton>
)} )}
> >
<DropdownItem label="Log Out" onClick={onLogoutClick} /> <DropdownItem label={t('ui.settingsDropdown.logOut')} onClick={onLogoutClick} />
</Dropdown> </Dropdown>
</React.Fragment> </React.Fragment>
); );
@ -73,6 +74,7 @@ SettingsDropdown.propTypes = {
displayUrl: PropTypes.string, displayUrl: PropTypes.string,
imageUrl: PropTypes.string, imageUrl: PropTypes.string,
onLogoutClick: PropTypes.func.isRequired, 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 React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { css, injectGlobal, cx } from 'react-emotion'; import { css, injectGlobal, cx } from 'react-emotion';
import { translate } from 'react-polyglot';
import reduxNotificationsStyles from 'redux-notifications/lib/styles.css'; import reduxNotificationsStyles from 'redux-notifications/lib/styles.css';
import { shadows, colors, lengths } from 'netlify-cms-ui-default'; import { shadows, colors, lengths } from 'netlify-cms-ui-default';
@ -41,11 +42,16 @@ const styles = {
`, `,
}; };
export const Toast = ({ kind, message }) => ( const Toast = ({ kind, message, t }) => (
<div className={cx(styles.toast, styles[kind])}>{message}</div> <div className={cx(styles.toast, styles[kind])}>
{t(message.key, { details: message.details })}
</div>
); );
Toast.propTypes = { Toast.propTypes = {
kind: PropTypes.oneOf(['info', 'success', 'warning', 'danger']).isRequired, 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 { DragSource, DropTarget, HTML5DragDrop } from './DragDrop';
export { ErrorBoundary } from './ErrorBoundary'; export ErrorBoundary from './ErrorBoundary';
export { FileUploadButton } from './FileUploadButton'; export { FileUploadButton } from './FileUploadButton';
export { Modal } from './Modal'; 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 ImmutablePropTypes from 'react-immutable-proptypes';
import styled from 'react-emotion'; import styled from 'react-emotion';
import { OrderedMap } from 'immutable'; import { OrderedMap } from 'immutable';
import { translate } from 'react-polyglot';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
Dropdown, Dropdown,
@ -60,6 +61,7 @@ class Workflow extends Component {
updateUnpublishedEntryStatus: PropTypes.func.isRequired, updateUnpublishedEntryStatus: PropTypes.func.isRequired,
publishUnpublishedEntry: PropTypes.func.isRequired, publishUnpublishedEntry: PropTypes.func.isRequired,
deleteUnpublishedEntry: PropTypes.func.isRequired, deleteUnpublishedEntry: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
}; };
componentDidMount() { componentDidMount() {
@ -78,10 +80,11 @@ class Workflow extends Component {
publishUnpublishedEntry, publishUnpublishedEntry,
deleteUnpublishedEntry, deleteUnpublishedEntry,
collections, collections,
t,
} = this.props; } = this.props;
if (!isEditorialWorkflow) return null; 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 reviewCount = unpublishedEntries.get('pending_review').size;
const readyCount = unpublishedEntries.get('pending_publish').size; const readyCount = unpublishedEntries.get('pending_publish').size;
@ -89,12 +92,14 @@ class Workflow extends Component {
<WorkflowContainer> <WorkflowContainer>
<WorkflowTop> <WorkflowTop>
<WorkflowTopRow> <WorkflowTopRow>
<WorkflowTopHeading>Editorial Workflow</WorkflowTopHeading> <WorkflowTopHeading>{t('workflow.workflow.workflowHeading')}</WorkflowTopHeading>
<Dropdown <Dropdown
dropdownWidth="160px" dropdownWidth="160px"
dropdownPosition="left" dropdownPosition="left"
dropdownTopOverlap="40px" dropdownTopOverlap="40px"
renderButton={() => <StyledDropdownButton>New Post</StyledDropdownButton>} renderButton={() => (
<StyledDropdownButton>{t('workflow.workflow.newPost')}</StyledDropdownButton>
)}
> >
{collections {collections
.filter(collection => collection.get('create')) .filter(collection => collection.get('create'))
@ -109,8 +114,10 @@ class Workflow extends Component {
</Dropdown> </Dropdown>
</WorkflowTopRow> </WorkflowTopRow>
<WorkflowTopDescription> <WorkflowTopDescription>
{reviewCount} {reviewCount === 1 ? 'entry' : 'entries'} waiting for review, {readyCount}{' '} {t('workflow.workflow.description', {
ready to go live. smart_count: reviewCount,
readyCount: readyCount,
})}
</WorkflowTopDescription> </WorkflowTopDescription>
</WorkflowTop> </WorkflowTop>
<WorkflowList <WorkflowList
@ -153,4 +160,4 @@ export default connect(
publishUnpublishedEntry, publishUnpublishedEntry,
deleteUnpublishedEntry, deleteUnpublishedEntry,
}, },
)(Workflow); )(translate()(Workflow));

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled, { css } from 'react-emotion'; import styled, { css } from 'react-emotion';
import { translate } from 'react-polyglot';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { components, colors, colorsRaw, transitions, buttons } from 'netlify-cms-ui-default'; import { components, colors, colorsRaw, transitions, buttons } from 'netlify-cms-ui-default';
@ -107,6 +108,7 @@ const WorkflowCard = ({
onDelete, onDelete,
canPublish, canPublish,
onPublish, onPublish,
t,
}) => ( }) => (
<WorkflowCardContainer> <WorkflowCardContainer>
<WorkflowLink to={editLink}> <WorkflowLink to={editLink}>
@ -119,10 +121,14 @@ const WorkflowCard = ({
</WorkflowLink> </WorkflowLink>
<CardButtonContainer> <CardButtonContainer>
<DeleteButton onClick={onDelete}> <DeleteButton onClick={onDelete}>
{isModification ? 'Delete changes' : 'Delete new entry'} {isModification
? t('workflow.workflowCard.deleteChanges')
: t('workflow.workflowCard.deleteNewEntry')}
</DeleteButton> </DeleteButton>
<PublishButton disabled={!canPublish} onClick={onPublish}> <PublishButton disabled={!canPublish} onClick={onPublish}>
{isModification ? 'Publish changes' : 'Publish new entry'} {isModification
? t('workflow.workflowCard.publishChanges')
: t('workflow.workflowCard.publishNewEntry')}
</PublishButton> </PublishButton>
</CardButtonContainer> </CardButtonContainer>
</WorkflowCardContainer> </WorkflowCardContainer>
@ -139,6 +145,7 @@ WorkflowCard.propTypes = {
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
canPublish: PropTypes.bool.isRequired, canPublish: PropTypes.bool.isRequired,
onPublish: PropTypes.func.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 ImmutablePropTypes from 'react-immutable-proptypes';
import styled, { css, cx } from 'react-emotion'; import styled, { css, cx } from 'react-emotion';
import moment from 'moment'; import moment from 'moment';
import { translate } from 'react-polyglot';
import { colors, lengths } from 'netlify-cms-ui-default'; import { colors, lengths } from 'netlify-cms-ui-default';
import { status } from 'Constants/publishModes'; import { status } from 'Constants/publishModes';
import { DragSource, DropTarget, HTML5DragDrop } from 'UI'; 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 // This is a namespace so that we can only drop these elements on a DropTarget with the same
const DNDNamespace = 'cms-workflow'; const DNDNamespace = 'cms-workflow';
const getColumnHeaderText = columnName => { const getColumnHeaderText = (columnName, t) => {
switch (columnName) { switch (columnName) {
case 'draft': case 'draft':
return 'Drafts'; return t('workflow.workflowList.draftHeader');
case 'pending_review': case 'pending_review':
return 'In Review'; return t('workflow.workflowList.inReviewHeader');
case 'pending_publish': case 'pending_publish':
return 'Ready'; return t('workflow.workflowList.readyHeader');
} }
}; };
@ -113,6 +114,7 @@ class WorkflowList extends React.Component {
handleChangeStatus: PropTypes.func.isRequired, handleChangeStatus: PropTypes.func.isRequired,
handlePublish: PropTypes.func.isRequired, handlePublish: PropTypes.func.isRequired,
handleDelete: PropTypes.func.isRequired, handleDelete: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
}; };
handleChangeStatus = (newStatus, dragProps) => { handleChangeStatus = (newStatus, dragProps) => {
@ -123,20 +125,16 @@ class WorkflowList extends React.Component {
}; };
requestDelete = (collection, slug, ownStatus) => { 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); this.props.handleDelete(collection, slug, ownStatus);
} }
}; };
requestPublish = (collection, slug, ownStatus) => { requestPublish = (collection, slug, ownStatus) => {
if (ownStatus !== status.last()) { if (ownStatus !== status.last()) {
window.alert( window.alert(this.props.t('workflow.workflowList.onPublishingNotReadyEntry'));
`Only items with a "Ready" status can be published.
Please drag the card to the "Ready" column to enable publishing.`,
);
return; 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; return;
} }
this.props.handlePublish(collection, slug); this.props.handlePublish(collection, slug);
@ -155,9 +153,13 @@ Please drag the card to the "Ready" column to enable publishing.`,
{(connect, { isHovered }) => {(connect, { isHovered }) =>
connect( connect(
<div className={cx(styles.column, { [styles.columnHovered]: isHovered })}> <div className={cx(styles.column, { [styles.columnHovered]: isHovered })}>
<ColumnHeader name={currColumn}>{getColumnHeaderText(currColumn)}</ColumnHeader> <ColumnHeader name={currColumn}>
{getColumnHeaderText(currColumn, this.props.t)}
</ColumnHeader>
<ColumnCount> <ColumnCount>
{currEntries.size} {currEntries.size === 1 ? 'entry' : 'entries'} {this.props.t('workflow.workflowList.currentEntries', {
smart_count: currEntries.size,
})}
</ColumnCount> </ColumnCount>
{this.renderColumns(currEntries, currColumn)} {this.renderColumns(currEntries, currColumn)}
</div>, </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: dependencies:
stackframe "^1.0.4" 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" version "1.12.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165"
integrity sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA== integrity sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==
@ -4548,6 +4548,12 @@ follow-redirects@^1.0.0:
dependencies: dependencies:
debug "^3.1.0" 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: for-in@^1.0.1, for-in@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" 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" shellwords "^0.1.1"
which "^1.3.0" 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: node-pre-gyp@^0.10.0:
version "0.10.3" version "0.10.3"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" 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" resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz#6a5b5b8b4eae6b776259712c89c8a2b36b17be93"
integrity sha512-p84kBqGaMoa7VYT0vZ/aOYRfJB+gw34yjpda1Z5KeLflg70HipZOT+MXQenEhdkPAABuE2Astq4zEPdMqUQxcg== 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: react-portal@^3.1.0:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-3.2.0.tgz#4224e19b2b05d5cbe730a7ba0e34ec7585de0043" 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" es-abstract "^1.4.3"
function-bind "^1.0.2" 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: string_decoder@^1.0.0, string_decoder@~1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"