From c2e21ff9db954c1bdec25531ee2132bd31453b78 Mon Sep 17 00:00:00 2001 From: Nahuel Dealbera Date: Fri, 2 Nov 2018 14:19:49 -0300 Subject: [PATCH] improvement(i18n): extract core UI texts to external file (#1708) --- dev-test/config.yml | 277 +++++++++--------- packages/netlify-cms-core/package.json | 2 + .../src/actions/editorialWorkflow.js | 40 ++- .../netlify-cms-core/src/actions/entries.js | 33 ++- packages/netlify-cms-core/src/bootstrap.js | 18 +- .../src/components/App/App.js | 20 +- .../src/components/App/Header.js | 17 +- .../src/components/App/NotFoundPage.js | 12 +- .../components/Collection/CollectionTop.js | 11 +- .../components/Collection/Entries/Entries.js | 11 +- .../src/components/Collection/Sidebar.js | 12 +- .../src/components/Editor/Editor.js | 41 ++- .../Editor/EditorControlPane/EditorControl.js | 6 +- .../Editor/EditorControlPane/Widget.js | 22 +- .../src/components/Editor/EditorToolbar.js | 83 ++++-- .../EditorWidgets/Unknown/UnknownControl.js | 13 +- .../EditorWidgets/Unknown/UnknownPreview.js | 12 +- .../components/MediaLibrary/MediaLibrary.js | 10 +- .../MediaLibrary/MediaLibraryModal.js | 39 ++- .../src/components/UI/ErrorBoundary.js | 14 +- .../src/components/UI/SettingsDropdown.js | 8 +- .../src/components/UI/Toast.js | 12 +- .../src/components/UI/index.js | 4 +- .../src/components/Workflow/Workflow.js | 19 +- .../src/components/Workflow/WorkflowCard.js | 13 +- .../src/components/Workflow/WorkflowList.js | 30 +- .../src/constants/defaultPhrases.js | 160 ++++++++++ yarn.lock | 31 +- 28 files changed, 673 insertions(+), 297 deletions(-) create mode 100644 packages/netlify-cms-core/src/constants/defaultPhrases.js diff --git a/dev-test/config.yml b/dev-test/config.yml index f2923606..261b1f03 100644 --- a/dev-test/config.yml +++ b/dev-test/config.yml @@ -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'], + } diff --git a/packages/netlify-cms-core/package.json b/packages/netlify-cms-core/package.json index d87ad485..3892d9e4 100644 --- a/packages/netlify-cms-core/package.json +++ b/packages/netlify-cms-core/package.json @@ -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", diff --git a/packages/netlify-cms-core/src/actions/editorialWorkflow.js b/packages/netlify-cms-core/src/actions/editorialWorkflow.js index 495d7a4c..56223a01 100644 --- a/packages/netlify-cms-core/src/actions/editorialWorkflow.js +++ b/packages/netlify-cms-core/src/actions/editorialWorkflow.js @@ -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, }), diff --git a/packages/netlify-cms-core/src/actions/entries.js b/packages/netlify-cms-core/src/actions/entries.js index 4a79d4a6..13f2a0f3 100644 --- a/packages/netlify-cms-core/src/actions/entries.js +++ b/packages/netlify-cms-core/src/actions/entries.js @@ -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, }), diff --git a/packages/netlify-cms-core/src/bootstrap.js b/packages/netlify-cms-core/src/bootstrap.js index 29b5effa..262ffcfe 100644 --- a/packages/netlify-cms-core/src/bootstrap.js +++ b/packages/netlify-cms-core/src/bootstrap.js @@ -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 = () => ( - - - - - - - + + + + + + + + + ); /** diff --git a/packages/netlify-cms-core/src/components/App/App.js b/packages/netlify-cms-core/src/components/App/App.js index 16bcd82f..a514de80 100644 --- a/packages/netlify-cms-core/src/components/App/App.js +++ b/packages/netlify-cms-core/src/components/App/App.js @@ -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 ( -

Error loading the CMS configuration

+

{t('app.app.errorHeader')}

- Config Errors: + {t('app.app.configErrors')}: {config.get('error')} - Check your config.yml file. + {t('app.app.checkConfigYml')}
); @@ -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 (
-

Waiting for backend...

+

{t('app.app.waitingBackend')}

); } @@ -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 Loading configuration...; + return {t('app.app.loadingConfig')}; } 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)), ); diff --git a/packages/netlify-cms-core/src/components/App/Header.js b/packages/netlify-cms-core/src/components/App/Header.js index db260a2f..b3301db0 100644 --- a/packages/netlify-cms-core/src/components/App/Header.js +++ b/packages/netlify-cms-core/src/components/App/Header.js @@ -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/')} > - Content + {t('app.header.content')} {hasWorkflow ? ( - Workflow + {t('app.header.workflow')} ) : null} {showMediaButton ? ( - Media + {t('app.header.media')} ) : null} {createableCollections.size > 0 && ( Quick add} + renderButton={() => ( + {t('app.header.quickAdd')} + )} dropdownTopOverlap="30px" dropdownWidth="160px" dropdownPosition="left" @@ -187,3 +192,5 @@ export default class Header extends React.Component { ); } } + +export default translate()(Header); diff --git a/packages/netlify-cms-core/src/components/App/NotFoundPage.js b/packages/netlify-cms-core/src/components/App/NotFoundPage.js index 8ca9eac1..90002d59 100644 --- a/packages/netlify-cms-core/src/components/App/NotFoundPage.js +++ b/packages/netlify-cms-core/src/components/App/NotFoundPage.js @@ -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 }) => ( -

Not Found

+

{t('app.notFoundPage.header')}

); -export default NotFoundPage; +NotFoundPage.propTypes = { + t: PropTypes.func.isRequired, +}; + +export default translate()(NotFoundPage); diff --git a/packages/netlify-cms-core/src/components/Collection/CollectionTop.js b/packages/netlify-cms-core/src/components/Collection/CollectionTop.js index 6704301e..4dc2df0d 100644 --- a/packages/netlify-cms-core/src/components/Collection/CollectionTop.js +++ b/packages/netlify-cms-core/src/components/Collection/CollectionTop.js @@ -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 ( @@ -77,7 +79,9 @@ const CollectionTop = ({ {collectionLabel} {newEntryUrl ? ( - {`New ${collectionLabelSingular || collectionLabel}`} + {t('collection.collectionTop.newButton', { + collectionLabel: collectionLabelSingular || collectionLabel, + })} ) : null} @@ -85,7 +89,7 @@ const CollectionTop = ({ {collectionDescription} ) : null} - View as: + {t('collection.collectionTop.viewAs')}: 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); diff --git a/packages/netlify-cms-core/src/components/Collection/Entries/Entries.js b/packages/netlify-cms-core/src/components/Collection/Entries/Entries.js index b3188099..ece43f67 100644 --- a/packages/netlify-cms-core/src/components/Collection/Entries/Entries.js +++ b/packages/netlify-cms-core/src/components/Collection/Entries/Entries.js @@ -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); diff --git a/packages/netlify-cms-core/src/components/Collection/Sidebar.js b/packages/netlify-cms-core/src/components/Collection/Sidebar.js index 5cf1ce8f..a56f0461 100644 --- a/packages/netlify-cms-core/src/components/Collection/Sidebar.js +++ b/packages/netlify-cms-core/src/components/Collection/Sidebar.js @@ -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 ( - Collections + {t('collection.sidebar.collections')} this.setState({ query: e.target.value })} onKeyDown={e => e.key === 'Enter' && searchCollections(query)} - placeholder="Search all" + placeholder={t('collection.sidebar.searchAll')} value={query} /> @@ -134,3 +136,5 @@ export default class Sidebar extends React.Component { ); } } + +export default translate()(Sidebar); diff --git a/packages/netlify-cms-core/src/components/Editor/Editor.js b/packages/netlify-cms-core/src/components/Editor/Editor.js index 7bbb9d2c..be0607b8 100644 --- a/packages/netlify-cms-core/src/components/Editor/Editor.js +++ b/packages/netlify-cms-core/src/components/Editor/Editor.js @@ -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 Loading entry...; + return {t('editor.editor.loadingEntry')}; } return ( @@ -422,4 +419,4 @@ export default connect( deleteUnpublishedEntry, logoutUser, }, -)(withWorkflow(Editor)); +)(withWorkflow(translate()(Editor))); diff --git a/packages/netlify-cms-core/src/components/Editor/EditorControlPane/EditorControl.js b/packages/netlify-cms-core/src/components/Editor/EditorControlPane/EditorControl.js index aa70b204..210d0658 100644 --- a/packages/netlify-cms-core/src/components/Editor/EditorControlPane/EditorControl.js +++ b/packages/netlify-cms-core/src/components/Editor/EditorControlPane/EditorControl.js @@ -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 && ( @@ -266,6 +270,6 @@ const ConnectedEditorControl = connect( mapDispatchToProps, null, { withRef: true }, -)(EditorControl); +)(translate()(EditorControl)); export default ConnectedEditorControl; diff --git a/packages/netlify-cms-core/src/components/Editor/EditorControlPane/Widget.js b/packages/netlify-cms-core/src/components/Editor/EditorControlPane/Widget.js index 3824a868..a34901cd 100644 --- a/packages/netlify-cms-core/src/components/Editor/EditorControlPane/Widget.js +++ b/packages/netlify-cms-core/src/components/Editor/EditorControlPane/Widget.js @@ -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, }); } } diff --git a/packages/netlify-cms-core/src/components/Editor/EditorToolbar.js b/packages/netlify-cms-core/src/components/Editor/EditorToolbar.js index e4980f9e..116be463 100644 --- a/packages/netlify-cms-core/src/components/Editor/EditorToolbar.js +++ b/packages/netlify-cms-core/src/components/Editor/EditorToolbar.js @@ -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 ( -
{showDelete ? Delete entry : null}
+
+ {showDelete ? ( + {t('editor.editorToolbar.deleteEntry')} + ) : null} +
); }; @@ -207,9 +213,10 @@ export default class EditorToolbar extends React.Component { isPersisting, hasChanged, isNewEntry, + t, } = this.props; if (!isNewEntry && !hasChanged) { - return Published; + return {t('editor.editorToolbar.published')}; } return (
@@ -217,7 +224,11 @@ export default class EditorToolbar extends React.Component { dropdownTopOverlap="40px" dropdownWidth="150px" renderButton={() => ( - {isPersisting ? 'Publishing...' : 'Publish'} + + {isPersisting + ? t('editor.editorToolbar.publishing') + : t('editor.editorToolbar.publish')} + )} > {collection.get('create') ? ( - + ) : null}
@@ -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 [ hasChanged && onPersist()}> - {isPersisting ? 'Saving...' : 'Save'} + {isPersisting ? t('editor.editorToolbar.saving') : t('editor.editorToolbar.save')} , isNewEntry || !deleteLabel ? null : ( - {isDeleting ? 'Deleting...' : deleteLabel} + {isDeleting ? t('editor.editorToolbar.deleting') : deleteLabel} ), ]; @@ -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={() => ( - {isUpdatingStatus ? 'Updating...' : 'Set status'} + + {isUpdatingStatus + ? t('editor.editorToolbar.updating') + : t('editor.editorToolbar.setStatus')} + )} > onChangeStatus('DRAFT')} icon={currentStatus === status.get('DRAFT') && 'check'} /> onChangeStatus('PENDING_REVIEW')} icon={currentStatus === status.get('PENDING_REVIEW') && 'check'} /> 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={() => ( - {isPublishing ? 'Publishing...' : 'Publish'} + + {isPublishing + ? t('editor.editorToolbar.publishing') + : t('editor.editorToolbar.publish')} + )} > {collection.get('create') ? ( - + ) : null} @@ -326,12 +359,12 @@ export default class EditorToolbar extends React.Component { } if (!isNewEntry) { - return Published; + return {t('editor.editorToolbar.published')}; } }; render() { - const { user, hasChanged, displayUrl, collection, hasWorkflow, onLogoutClick } = this.props; + const { user, hasChanged, displayUrl, collection, hasWorkflow, onLogoutClick, t } = this.props; return ( @@ -339,12 +372,14 @@ export default class EditorToolbar extends React.Component {
- Writing in {collection.get('label')} collection + {t('editor.editorToolbar.backCollection', { + collectionLabel: collection.get('label'), + })} {hasChanged ? ( - Unsaved Changes + {t('editor.editorToolbar.unsavedChanges')} ) : ( - Changes saved + {t('editor.editorToolbar.changesSaved')} )}
@@ -369,3 +404,5 @@ export default class EditorToolbar extends React.Component { ); } } + +export default translate()(EditorToolbar); diff --git a/packages/netlify-cms-core/src/components/EditorWidgets/Unknown/UnknownControl.js b/packages/netlify-cms-core/src/components/EditorWidgets/Unknown/UnknownControl.js index f0930514..20cb27e4 100644 --- a/packages/netlify-cms-core/src/components/EditorWidgets/Unknown/UnknownControl.js +++ b/packages/netlify-cms-core/src/components/EditorWidgets/Unknown/UnknownControl.js @@ -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
{`No control for widget '${field.get('widget')}'.`}
; -} +const UnknownControl = ({ field, t }) => { + return ( +
{t('editor.editorWidgets.unknownControl.noControl', { widget: field.get('widget') })}
+ ); +}; UnknownControl.propTypes = { field: ImmutablePropTypes.map, + t: PropTypes.func.isRequired, }; + +export default translate()(UnknownControl); diff --git a/packages/netlify-cms-core/src/components/EditorWidgets/Unknown/UnknownPreview.js b/packages/netlify-cms-core/src/components/EditorWidgets/Unknown/UnknownPreview.js index 4f62d0ac..db29a0c5 100644 --- a/packages/netlify-cms-core/src/components/EditorWidgets/Unknown/UnknownPreview.js +++ b/packages/netlify-cms-core/src/components/EditorWidgets/Unknown/UnknownPreview.js @@ -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 (
- No preview for widget “{field.get('widget')} - ”. + {t('editor.editorWidgets.unknownPreview.noPreview', field.get('widget'))}
); -} +}; UnknownPreview.propTypes = { field: ImmutablePropTypes.map, + t: PropTypes.func.isRequired, }; + +export default translate()(UnknownPreview); diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index daf3dc12..1640399e 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -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)); diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js index d572928a..0df45936 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js @@ -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 = ({
-

Sorry!

+

{t('ui.errorBoundary.title')}

- {"There's been an error - please "} + {t('ui.errorBoundary.details')} - report it + {t('ui.errorBoundary.reportIt')} - !

{errorMessage}

); } } + +export default translate()(ErrorBoundary); diff --git a/packages/netlify-cms-core/src/components/UI/SettingsDropdown.js b/packages/netlify-cms-core/src/components/UI/SettingsDropdown.js index ac121e75..34b7c611 100644 --- a/packages/netlify-cms-core/src/components/UI/SettingsDropdown.js +++ b/packages/netlify-cms-core/src/components/UI/SettingsDropdown.js @@ -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 }) => ( {displayUrl ? ( @@ -64,7 +65,7 @@ const SettingsDropdown = ({ displayUrl, imageUrl, onLogoutClick }) => ( )} > - +
); @@ -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); diff --git a/packages/netlify-cms-core/src/components/UI/Toast.js b/packages/netlify-cms-core/src/components/UI/Toast.js index 15370de2..4b647832 100644 --- a/packages/netlify-cms-core/src/components/UI/Toast.js +++ b/packages/netlify-cms-core/src/components/UI/Toast.js @@ -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 }) => ( -
{message}
+const Toast = ({ kind, message, t }) => ( +
+ {t(message.key, { details: message.details })} +
); Toast.propTypes = { kind: PropTypes.oneOf(['info', 'success', 'warning', 'danger']).isRequired, - message: PropTypes.string, + message: PropTypes.object, + t: PropTypes.func.isRequired, }; + +export default translate()(Toast); diff --git a/packages/netlify-cms-core/src/components/UI/index.js b/packages/netlify-cms-core/src/components/UI/index.js index a973833c..0e94ccf9 100644 --- a/packages/netlify-cms-core/src/components/UI/index.js +++ b/packages/netlify-cms-core/src/components/UI/index.js @@ -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'; diff --git a/packages/netlify-cms-core/src/components/Workflow/Workflow.js b/packages/netlify-cms-core/src/components/Workflow/Workflow.js index 2f60e653..17d99750 100644 --- a/packages/netlify-cms-core/src/components/Workflow/Workflow.js +++ b/packages/netlify-cms-core/src/components/Workflow/Workflow.js @@ -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 Loading Editorial Workflow Entries; + if (isFetching) return {t('workflow.workflow.loading')}; const reviewCount = unpublishedEntries.get('pending_review').size; const readyCount = unpublishedEntries.get('pending_publish').size; @@ -89,12 +92,14 @@ class Workflow extends Component { - Editorial Workflow + {t('workflow.workflow.workflowHeading')} New Post} + renderButton={() => ( + {t('workflow.workflow.newPost')} + )} > {collections .filter(collection => collection.get('create')) @@ -109,8 +114,10 @@ class Workflow extends Component { - {reviewCount} {reviewCount === 1 ? 'entry' : 'entries'} waiting for review, {readyCount}{' '} - ready to go live. + {t('workflow.workflow.description', { + smart_count: reviewCount, + readyCount: readyCount, + })} ( @@ -119,10 +121,14 @@ const WorkflowCard = ({ - {isModification ? 'Delete changes' : 'Delete new entry'} + {isModification + ? t('workflow.workflowCard.deleteChanges') + : t('workflow.workflowCard.deleteNewEntry')} - {isModification ? 'Publish changes' : 'Publish new entry'} + {isModification + ? t('workflow.workflowCard.publishChanges') + : t('workflow.workflowCard.publishNewEntry')} @@ -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); diff --git a/packages/netlify-cms-core/src/components/Workflow/WorkflowList.js b/packages/netlify-cms-core/src/components/Workflow/WorkflowList.js index 42af5bc5..ada5adaa 100644 --- a/packages/netlify-cms-core/src/components/Workflow/WorkflowList.js +++ b/packages/netlify-cms-core/src/components/Workflow/WorkflowList.js @@ -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(
- {getColumnHeaderText(currColumn)} + + {getColumnHeaderText(currColumn, this.props.t)} + - {currEntries.size} {currEntries.size === 1 ? 'entry' : 'entries'} + {this.props.t('workflow.workflowList.currentEntries', { + smart_count: currEntries.size, + })} {this.renderColumns(currEntries, currColumn)}
, @@ -218,4 +220,4 @@ Please drag the card to the "Ready" column to enable publishing.`, } } -export default HTML5DragDrop(WorkflowList); +export default HTML5DragDrop(translate()(WorkflowList)); diff --git a/packages/netlify-cms-core/src/constants/defaultPhrases.js b/packages/netlify-cms-core/src/constants/defaultPhrases.js new file mode 100644 index 00000000..d353cf03 --- /dev/null +++ b/packages/netlify-cms-core/src/constants/defaultPhrases.js @@ -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', + }, + }, + }; +} diff --git a/yarn.lock b/yarn.lock index 23f9938c..486a2d19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"