improvement(i18n): extract core UI texts to external file (#1708)
This commit is contained in:
parent
d538554c88
commit
c2e21ff9db
@ -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'],
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
}),
|
||||
|
@ -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,
|
||||
}),
|
||||
|
18
packages/netlify-cms-core/src/bootstrap.js
vendored
18
packages/netlify-cms-core/src/bootstrap.js
vendored
@ -6,6 +6,8 @@ import { ConnectedRouter } from 'react-router-redux';
|
||||
import history from 'Routing/history';
|
||||
import store from 'Redux';
|
||||
import { mergeConfig } from 'Actions/config';
|
||||
import { getPhrases } from 'Constants/defaultPhrases';
|
||||
import { I18n } from 'react-polyglot';
|
||||
import { ErrorBoundary } from 'UI';
|
||||
import App from 'App/App';
|
||||
import 'EditorWidgets';
|
||||
@ -60,13 +62,15 @@ function bootstrap(opts = {}) {
|
||||
* Create connected root component.
|
||||
*/
|
||||
const Root = () => (
|
||||
<ErrorBoundary>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<Route component={App} />
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</ErrorBoundary>
|
||||
<I18n locale={'en'} messages={getPhrases()}>
|
||||
<ErrorBoundary>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<Route component={App} />
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</ErrorBoundary>
|
||||
</I18n>
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { translate } from 'react-polyglot';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled from 'react-emotion';
|
||||
import { connect } from 'react-redux';
|
||||
@ -63,17 +64,19 @@ class App extends React.Component {
|
||||
useMediaLibrary: PropTypes.bool,
|
||||
openMediaLibrary: PropTypes.func.isRequired,
|
||||
showMediaButton: PropTypes.bool,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static configError(config) {
|
||||
const t = this.props.t;
|
||||
return (
|
||||
<ErrorContainer>
|
||||
<h1>Error loading the CMS configuration</h1>
|
||||
<h1>{t('app.app.errorHeader')}</h1>
|
||||
|
||||
<div>
|
||||
<strong>Config Errors:</strong>
|
||||
<strong>{t('app.app.configErrors')}:</strong>
|
||||
<ErrorCodeBlock>{config.get('error')}</ErrorCodeBlock>
|
||||
<span>Check your config.yml file.</span>
|
||||
<span>{t('app.app.checkConfigYml')}</span>
|
||||
</div>
|
||||
</ErrorContainer>
|
||||
);
|
||||
@ -89,13 +92,13 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
authenticating() {
|
||||
const { auth } = this.props;
|
||||
const { auth, t } = this.props;
|
||||
const backend = currentBackend(this.props.config);
|
||||
|
||||
if (backend == null) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Waiting for backend...</h1>
|
||||
<h1>{t('app.app.waitingBackend')}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -133,6 +136,7 @@ class App extends React.Component {
|
||||
publishMode,
|
||||
useMediaLibrary,
|
||||
openMediaLibrary,
|
||||
t,
|
||||
showMediaButton,
|
||||
} = this.props;
|
||||
|
||||
@ -145,11 +149,11 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
if (config.get('isFetching')) {
|
||||
return <Loader active>Loading configuration...</Loader>;
|
||||
return <Loader active>{t('app.app.loadingConfig')}</Loader>;
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
return this.authenticating();
|
||||
return this.authenticating(t);
|
||||
}
|
||||
|
||||
const defaultPath = `/collections/${collections.first().get('name')}`;
|
||||
@ -225,5 +229,5 @@ export default hot(module)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(App),
|
||||
)(translate()(App)),
|
||||
);
|
||||
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled, { css } from 'react-emotion';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import {
|
||||
Icon,
|
||||
@ -99,7 +100,7 @@ const AppHeaderQuickNewButton = styled(StyledDropdownButton)`
|
||||
}
|
||||
`;
|
||||
|
||||
export default class Header extends React.Component {
|
||||
class Header extends React.Component {
|
||||
static propTypes = {
|
||||
user: ImmutablePropTypes.map.isRequired,
|
||||
collections: ImmutablePropTypes.orderedMap.isRequired,
|
||||
@ -108,6 +109,7 @@ export default class Header extends React.Component {
|
||||
openMediaLibrary: PropTypes.func.isRequired,
|
||||
hasWorkflow: PropTypes.bool.isRequired,
|
||||
displayUrl: PropTypes.string,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleCreatePostClick = collectionName => {
|
||||
@ -125,6 +127,7 @@ export default class Header extends React.Component {
|
||||
openMediaLibrary,
|
||||
hasWorkflow,
|
||||
displayUrl,
|
||||
t,
|
||||
showMediaButton,
|
||||
} = this.props;
|
||||
|
||||
@ -143,25 +146,27 @@ export default class Header extends React.Component {
|
||||
isActive={(match, location) => location.pathname.startsWith('/collections/')}
|
||||
>
|
||||
<Icon type="page" />
|
||||
Content
|
||||
{t('app.header.content')}
|
||||
</AppHeaderNavLink>
|
||||
{hasWorkflow ? (
|
||||
<AppHeaderNavLink to="/workflow" activeClassName="header-link-active">
|
||||
<Icon type="workflow" />
|
||||
Workflow
|
||||
{t('app.header.workflow')}
|
||||
</AppHeaderNavLink>
|
||||
) : null}
|
||||
{showMediaButton ? (
|
||||
<AppHeaderButton onClick={openMediaLibrary}>
|
||||
<Icon type="media-alt" />
|
||||
Media
|
||||
{t('app.header.media')}
|
||||
</AppHeaderButton>
|
||||
) : null}
|
||||
</nav>
|
||||
<AppHeaderActions>
|
||||
{createableCollections.size > 0 && (
|
||||
<Dropdown
|
||||
renderButton={() => <AppHeaderQuickNewButton>Quick add</AppHeaderQuickNewButton>}
|
||||
renderButton={() => (
|
||||
<AppHeaderQuickNewButton> {t('app.header.quickAdd')}</AppHeaderQuickNewButton>
|
||||
)}
|
||||
dropdownTopOverlap="30px"
|
||||
dropdownWidth="160px"
|
||||
dropdownPosition="left"
|
||||
@ -187,3 +192,5 @@ export default class Header extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate()(Header);
|
||||
|
@ -1,15 +1,21 @@
|
||||
import React from 'react';
|
||||
import styled from 'react-emotion';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { lengths } from 'netlify-cms-ui-default';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const NotFoundContainer = styled.div`
|
||||
margin: ${lengths.pageMargin};
|
||||
`;
|
||||
|
||||
const NotFoundPage = () => (
|
||||
const NotFoundPage = ({ t }) => (
|
||||
<NotFoundContainer>
|
||||
<h2>Not Found</h2>
|
||||
<h2>{t('app.notFoundPage.header')}</h2>
|
||||
</NotFoundContainer>
|
||||
);
|
||||
|
||||
export default NotFoundPage;
|
||||
NotFoundPage.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default translate()(NotFoundPage);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import styled from 'react-emotion';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Icon, components, buttons, shadows, colors } from 'netlify-cms-ui-default';
|
||||
import { VIEW_STYLE_LIST, VIEW_STYLE_GRID } from 'Constants/collectionViews';
|
||||
@ -70,6 +71,7 @@ const CollectionTop = ({
|
||||
viewStyle,
|
||||
onChangeViewStyle,
|
||||
newEntryUrl,
|
||||
t,
|
||||
}) => {
|
||||
return (
|
||||
<CollectionTopContainer>
|
||||
@ -77,7 +79,9 @@ const CollectionTop = ({
|
||||
<CollectionTopHeading>{collectionLabel}</CollectionTopHeading>
|
||||
{newEntryUrl ? (
|
||||
<CollectionTopNewButton to={newEntryUrl}>
|
||||
{`New ${collectionLabelSingular || collectionLabel}`}
|
||||
{t('collection.collectionTop.newButton', {
|
||||
collectionLabel: collectionLabelSingular || collectionLabel,
|
||||
})}
|
||||
</CollectionTopNewButton>
|
||||
) : null}
|
||||
</CollectionTopRow>
|
||||
@ -85,7 +89,7 @@ const CollectionTop = ({
|
||||
<CollectionTopDescription>{collectionDescription}</CollectionTopDescription>
|
||||
) : null}
|
||||
<ViewControls>
|
||||
<ViewControlsText>View as:</ViewControlsText>
|
||||
<ViewControlsText>{t('collection.collectionTop.viewAs')}:</ViewControlsText>
|
||||
<ViewControlsButton
|
||||
isActive={viewStyle === VIEW_STYLE_LIST}
|
||||
onClick={() => onChangeViewStyle(VIEW_STYLE_LIST)}
|
||||
@ -110,6 +114,7 @@ CollectionTop.propTypes = {
|
||||
viewStyle: PropTypes.oneOf([VIEW_STYLE_LIST, VIEW_STYLE_GRID]).isRequired,
|
||||
onChangeViewStyle: PropTypes.func.isRequired,
|
||||
newEntryUrl: PropTypes.string,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CollectionTop;
|
||||
export default translate()(CollectionTop);
|
||||
|
@ -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);
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled, { css } from 'react-emotion';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { Icon, components, colors, colorsRaw, lengths } from 'netlify-cms-ui-default';
|
||||
import { searchCollections } from 'Actions/collections';
|
||||
@ -87,10 +88,11 @@ const SidebarNavLink = styled(NavLink)`
|
||||
}
|
||||
`;
|
||||
|
||||
export default class Sidebar extends React.Component {
|
||||
class Sidebar extends React.Component {
|
||||
static propTypes = {
|
||||
collections: ImmutablePropTypes.orderedMap.isRequired,
|
||||
searchTerm: PropTypes.string,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -114,18 +116,18 @@ export default class Sidebar extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { collections } = this.props;
|
||||
const { collections, t } = this.props;
|
||||
const { query } = this.state;
|
||||
|
||||
return (
|
||||
<SidebarContainer>
|
||||
<SidebarHeading>Collections</SidebarHeading>
|
||||
<SidebarHeading>{t('collection.sidebar.collections')}</SidebarHeading>
|
||||
<SearchContainer>
|
||||
<Icon type="search" size="small" />
|
||||
<SearchInput
|
||||
onChange={e => this.setState({ query: e.target.value })}
|
||||
onKeyDown={e => e.key === 'Enter' && searchCollections(query)}
|
||||
placeholder="Search all"
|
||||
placeholder={t('collection.sidebar.searchAll')}
|
||||
value={query}
|
||||
/>
|
||||
</SearchContainer>
|
||||
@ -134,3 +136,5 @@ export default class Sidebar extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate()(Sidebar);
|
||||
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
import { Loader } from 'netlify-cms-ui-default';
|
||||
import { translate } from 'react-polyglot';
|
||||
import history from 'Routing/history';
|
||||
import { logoutUser } from 'Actions/auth';
|
||||
import {
|
||||
@ -69,6 +70,7 @@ class Editor extends React.Component {
|
||||
pathname: PropTypes.string,
|
||||
}),
|
||||
hasChanged: PropTypes.bool,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@ -80,6 +82,7 @@ class Editor extends React.Component {
|
||||
createEmptyDraft,
|
||||
loadEntries,
|
||||
collectionEntriesLoaded,
|
||||
t,
|
||||
} = this.props;
|
||||
|
||||
if (newEntry) {
|
||||
@ -88,7 +91,7 @@ class Editor extends React.Component {
|
||||
loadEntry(collection, slug);
|
||||
}
|
||||
|
||||
const leaveMessage = 'Are you sure you want to leave this page?';
|
||||
const leaveMessage = t('editor.editor.onLeavePage');
|
||||
|
||||
this.exitBlocker = event => {
|
||||
if (this.props.entryDraft.get('hasChanged')) {
|
||||
@ -190,9 +193,10 @@ class Editor extends React.Component {
|
||||
collection,
|
||||
slug,
|
||||
currentStatus,
|
||||
t,
|
||||
} = this.props;
|
||||
if (entryDraft.get('hasChanged')) {
|
||||
window.alert('You have unsaved changes, please save before updating status.');
|
||||
window.alert(t('editor.editor.onUpdatingWithUnsavedChanges'));
|
||||
return;
|
||||
}
|
||||
const newStatus = status.get(newStatusName);
|
||||
@ -230,14 +234,15 @@ class Editor extends React.Component {
|
||||
slug,
|
||||
currentStatus,
|
||||
loadEntry,
|
||||
t,
|
||||
} = this.props;
|
||||
if (currentStatus !== status.last()) {
|
||||
window.alert('Please update status to "Ready" before publishing.');
|
||||
window.alert(t('editor.editor.onPublishingNotReady'));
|
||||
return;
|
||||
} else if (entryDraft.get('hasChanged')) {
|
||||
window.alert('You have unsaved changes, please save before publishing.');
|
||||
window.alert(t('editor.editor.onPublishingWithUnsavedChanges'));
|
||||
return;
|
||||
} else if (!window.confirm('Are you sure you want to publish this entry?')) {
|
||||
} else if (!window.confirm(t('editor.editor.onPublishing'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -251,16 +256,12 @@ class Editor extends React.Component {
|
||||
};
|
||||
|
||||
handleDeleteEntry = () => {
|
||||
const { entryDraft, newEntry, collection, deleteEntry, slug } = this.props;
|
||||
const { entryDraft, newEntry, collection, deleteEntry, slug, t } = this.props;
|
||||
if (entryDraft.get('hasChanged')) {
|
||||
if (
|
||||
!window.confirm(
|
||||
'Are you sure you want to delete this published entry, as well as your unsaved changes from the current session?',
|
||||
)
|
||||
) {
|
||||
if (!window.confirm(t('editor.editor.onDeleteWithUnsavedChanges'))) {
|
||||
return;
|
||||
}
|
||||
} else if (!window.confirm('Are you sure you want to delete this published entry?')) {
|
||||
} else if (!window.confirm(t('editor.editor.onDeletePublishedEntry'))) {
|
||||
return;
|
||||
}
|
||||
if (newEntry) {
|
||||
@ -281,19 +282,14 @@ class Editor extends React.Component {
|
||||
deleteUnpublishedEntry,
|
||||
loadEntry,
|
||||
isModification,
|
||||
t,
|
||||
} = this.props;
|
||||
if (
|
||||
entryDraft.get('hasChanged') &&
|
||||
!window.confirm(
|
||||
'This will delete all unpublished changes to this entry, as well as your unsaved changes from the current session. Do you still want to delete?',
|
||||
)
|
||||
!window.confirm(t('editor.editor.onDeleteUnpublishedChangesWithUnsavedChanges'))
|
||||
) {
|
||||
return;
|
||||
} else if (
|
||||
!window.confirm(
|
||||
'All unpublished changes to this entry will be deleted. Do you still want to delete?',
|
||||
)
|
||||
) {
|
||||
} else if (!window.confirm(t('editor.editor.onDeleteUnpublishedChanges'))) {
|
||||
return;
|
||||
}
|
||||
await deleteUnpublishedEntry(collection.get('name'), slug);
|
||||
@ -323,6 +319,7 @@ class Editor extends React.Component {
|
||||
isModification,
|
||||
currentStatus,
|
||||
logoutUser,
|
||||
t,
|
||||
} = this.props;
|
||||
|
||||
if (entry && entry.get('error')) {
|
||||
@ -336,7 +333,7 @@ class Editor extends React.Component {
|
||||
entryDraft.get('entry') === undefined ||
|
||||
(entry && entry.get('isFetching'))
|
||||
) {
|
||||
return <Loader active>Loading entry...</Loader>;
|
||||
return <Loader active>{t('editor.editor.loadingEntry')}</Loader>;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -422,4 +419,4 @@ export default connect(
|
||||
deleteUnpublishedEntry,
|
||||
logoutUser,
|
||||
},
|
||||
)(withWorkflow(Editor));
|
||||
)(withWorkflow(translate()(Editor)));
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { translate } from 'react-polyglot';
|
||||
import styled, { css, cx } from 'react-emotion';
|
||||
import { partial, uniqueId } from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
@ -142,6 +143,7 @@ class EditorControl extends React.Component {
|
||||
queryHits: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
isFetching: PropTypes.bool,
|
||||
clearSearch: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -168,6 +170,7 @@ class EditorControl extends React.Component {
|
||||
queryHits,
|
||||
isFetching,
|
||||
clearSearch,
|
||||
t,
|
||||
} = this.props;
|
||||
const widgetName = field.get('widget');
|
||||
const widget = resolveWidget(widgetName);
|
||||
@ -233,6 +236,7 @@ class EditorControl extends React.Component {
|
||||
queryHits={queryHits}
|
||||
clearSearch={clearSearch}
|
||||
isFetching={isFetching}
|
||||
t={t}
|
||||
/>
|
||||
{fieldHint && (
|
||||
<ControlHint active={this.state.styleActive} error={!!errors}>
|
||||
@ -266,6 +270,6 @@ const ConnectedEditorControl = connect(
|
||||
mapDispatchToProps,
|
||||
null,
|
||||
{ withRef: true },
|
||||
)(EditorControl);
|
||||
)(translate()(EditorControl));
|
||||
|
||||
export default ConnectedEditorControl;
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled, { css } from 'react-emotion';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Icon,
|
||||
@ -165,7 +166,7 @@ const StatusDropdownItem = styled(DropdownItem)`
|
||||
}
|
||||
`;
|
||||
|
||||
export default class EditorToolbar extends React.Component {
|
||||
class EditorToolbar extends React.Component {
|
||||
static propTypes = {
|
||||
isPersisting: PropTypes.bool,
|
||||
isPublishing: PropTypes.bool,
|
||||
@ -190,12 +191,17 @@ export default class EditorToolbar extends React.Component {
|
||||
isModification: PropTypes.bool,
|
||||
currentStatus: PropTypes.string,
|
||||
onLogoutClick: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
renderSimpleSaveControls = () => {
|
||||
const { showDelete, onDelete } = this.props;
|
||||
const { showDelete, onDelete, t } = this.props;
|
||||
return (
|
||||
<div>{showDelete ? <DeleteButton onClick={onDelete}>Delete entry</DeleteButton> : null}</div>
|
||||
<div>
|
||||
{showDelete ? (
|
||||
<DeleteButton onClick={onDelete}>{t('editor.editorToolbar.deleteEntry')}</DeleteButton>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -207,9 +213,10 @@ export default class EditorToolbar extends React.Component {
|
||||
isPersisting,
|
||||
hasChanged,
|
||||
isNewEntry,
|
||||
t,
|
||||
} = this.props;
|
||||
if (!isNewEntry && !hasChanged) {
|
||||
return <StatusPublished>Published</StatusPublished>;
|
||||
return <StatusPublished>{t('editor.editorToolbar.published')}</StatusPublished>;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
@ -217,7 +224,11 @@ export default class EditorToolbar extends React.Component {
|
||||
dropdownTopOverlap="40px"
|
||||
dropdownWidth="150px"
|
||||
renderButton={() => (
|
||||
<PublishButton>{isPersisting ? 'Publishing...' : 'Publish'}</PublishButton>
|
||||
<PublishButton>
|
||||
{isPersisting
|
||||
? t('editor.editorToolbar.publishing')
|
||||
: t('editor.editorToolbar.publish')}
|
||||
</PublishButton>
|
||||
)}
|
||||
>
|
||||
<DropdownItem
|
||||
@ -227,7 +238,11 @@ export default class EditorToolbar extends React.Component {
|
||||
onClick={onPersist}
|
||||
/>
|
||||
{collection.get('create') ? (
|
||||
<DropdownItem label="Publish and create new" icon="add" onClick={onPersistAndNew} />
|
||||
<DropdownItem
|
||||
label={t('editor.editorToolbar.publishAndCreateNew')}
|
||||
icon="add"
|
||||
onClick={onPersistAndNew}
|
||||
/>
|
||||
) : null}
|
||||
</ToolbarDropdown>
|
||||
</div>
|
||||
@ -245,23 +260,28 @@ export default class EditorToolbar extends React.Component {
|
||||
isDeleting,
|
||||
isNewEntry,
|
||||
isModification,
|
||||
t,
|
||||
} = this.props;
|
||||
|
||||
const deleteLabel =
|
||||
(hasUnpublishedChanges && isModification && 'Delete unpublished changes') ||
|
||||
(hasUnpublishedChanges && (isNewEntry || !isModification) && 'Delete unpublished entry') ||
|
||||
(!hasUnpublishedChanges && !isModification && 'Delete published entry');
|
||||
(hasUnpublishedChanges &&
|
||||
isModification &&
|
||||
t('editor.editorToolbar.deleteUnpublishedChanges')) ||
|
||||
(hasUnpublishedChanges &&
|
||||
(isNewEntry || !isModification) &&
|
||||
t('editor.editorToolbar.deleteUnpublishedEntry')) ||
|
||||
(!hasUnpublishedChanges && !isModification && t('editor.editorToolbar.deletePublishedEntry'));
|
||||
|
||||
return [
|
||||
<SaveButton key="save-button" onClick={() => hasChanged && onPersist()}>
|
||||
{isPersisting ? 'Saving...' : 'Save'}
|
||||
{isPersisting ? t('editor.editorToolbar.saving') : t('editor.editorToolbar.save')}
|
||||
</SaveButton>,
|
||||
isNewEntry || !deleteLabel ? null : (
|
||||
<DeleteButton
|
||||
key="delete-button"
|
||||
onClick={hasUnpublishedChanges ? onDeleteUnpublishedChanges : onDelete}
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : deleteLabel}
|
||||
{isDeleting ? t('editor.editorToolbar.deleting') : deleteLabel}
|
||||
</DeleteButton>
|
||||
),
|
||||
];
|
||||
@ -277,6 +297,7 @@ export default class EditorToolbar extends React.Component {
|
||||
onPublishAndNew,
|
||||
currentStatus,
|
||||
isNewEntry,
|
||||
t,
|
||||
} = this.props;
|
||||
if (currentStatus) {
|
||||
return (
|
||||
@ -285,21 +306,25 @@ export default class EditorToolbar extends React.Component {
|
||||
dropdownTopOverlap="40px"
|
||||
dropdownWidth="120px"
|
||||
renderButton={() => (
|
||||
<StatusButton>{isUpdatingStatus ? 'Updating...' : 'Set status'}</StatusButton>
|
||||
<StatusButton>
|
||||
{isUpdatingStatus
|
||||
? t('editor.editorToolbar.updating')
|
||||
: t('editor.editorToolbar.setStatus')}
|
||||
</StatusButton>
|
||||
)}
|
||||
>
|
||||
<StatusDropdownItem
|
||||
label="Draft"
|
||||
label={t('editor.editorToolbar.draft')}
|
||||
onClick={() => onChangeStatus('DRAFT')}
|
||||
icon={currentStatus === status.get('DRAFT') && 'check'}
|
||||
/>
|
||||
<StatusDropdownItem
|
||||
label="In review"
|
||||
label={t('editor.editorToolbar.inReview')}
|
||||
onClick={() => onChangeStatus('PENDING_REVIEW')}
|
||||
icon={currentStatus === status.get('PENDING_REVIEW') && 'check'}
|
||||
/>
|
||||
<StatusDropdownItem
|
||||
label="Ready"
|
||||
label={t('editor.editorToolbar.ready')}
|
||||
onClick={() => onChangeStatus('PENDING_PUBLISH')}
|
||||
icon={currentStatus === status.get('PENDING_PUBLISH') && 'check'}
|
||||
/>
|
||||
@ -308,17 +333,25 @@ export default class EditorToolbar extends React.Component {
|
||||
dropdownTopOverlap="40px"
|
||||
dropdownWidth="150px"
|
||||
renderButton={() => (
|
||||
<PublishButton>{isPublishing ? 'Publishing...' : 'Publish'}</PublishButton>
|
||||
<PublishButton>
|
||||
{isPublishing
|
||||
? t('editor.editorToolbar.publishing')
|
||||
: t('editor.editorToolbar.publish')}
|
||||
</PublishButton>
|
||||
)}
|
||||
>
|
||||
<DropdownItem
|
||||
label="Publish now"
|
||||
label={t('editor.editorToolbar.publishNow')}
|
||||
icon="arrow"
|
||||
iconDirection="right"
|
||||
onClick={onPublish}
|
||||
/>
|
||||
{collection.get('create') ? (
|
||||
<DropdownItem label="Publish and create new" icon="add" onClick={onPublishAndNew} />
|
||||
<DropdownItem
|
||||
label={t('editor.editorToolbar.publishAndCreateNew')}
|
||||
icon="add"
|
||||
onClick={onPublishAndNew}
|
||||
/>
|
||||
) : null}
|
||||
</ToolbarDropdown>
|
||||
</>
|
||||
@ -326,12 +359,12 @@ export default class EditorToolbar extends React.Component {
|
||||
}
|
||||
|
||||
if (!isNewEntry) {
|
||||
return <StatusPublished>Published</StatusPublished>;
|
||||
return <StatusPublished>{t('editor.editorToolbar.published')}</StatusPublished>;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { user, hasChanged, displayUrl, collection, hasWorkflow, onLogoutClick } = this.props;
|
||||
const { user, hasChanged, displayUrl, collection, hasWorkflow, onLogoutClick, t } = this.props;
|
||||
|
||||
return (
|
||||
<ToolbarContainer>
|
||||
@ -339,12 +372,14 @@ export default class EditorToolbar extends React.Component {
|
||||
<BackArrow>←</BackArrow>
|
||||
<div>
|
||||
<BackCollection>
|
||||
Writing in <strong>{collection.get('label')}</strong> collection
|
||||
{t('editor.editorToolbar.backCollection', {
|
||||
collectionLabel: collection.get('label'),
|
||||
})}
|
||||
</BackCollection>
|
||||
{hasChanged ? (
|
||||
<BackStatusChanged>Unsaved Changes</BackStatusChanged>
|
||||
<BackStatusChanged>{t('editor.editorToolbar.unsavedChanges')}</BackStatusChanged>
|
||||
) : (
|
||||
<BackStatusUnchanged>Changes saved</BackStatusUnchanged>
|
||||
<BackStatusUnchanged>{t('editor.editorToolbar.changesSaved')}</BackStatusUnchanged>
|
||||
)}
|
||||
</div>
|
||||
</ToolbarSectionBackLink>
|
||||
@ -369,3 +404,5 @@ export default class EditorToolbar extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate()(EditorToolbar);
|
||||
|
@ -1,10 +1,17 @@
|
||||
import React from 'react';
|
||||
import { translate } from 'react-polyglot';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function UnknownControl({ field }) {
|
||||
return <div>{`No control for widget '${field.get('widget')}'.`}</div>;
|
||||
}
|
||||
const UnknownControl = ({ field, t }) => {
|
||||
return (
|
||||
<div>{t('editor.editorWidgets.unknownControl.noControl', { widget: field.get('widget') })}</div>
|
||||
);
|
||||
};
|
||||
|
||||
UnknownControl.propTypes = {
|
||||
field: ImmutablePropTypes.map,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default translate()(UnknownControl);
|
||||
|
@ -1,15 +1,19 @@
|
||||
import React from 'react';
|
||||
import { translate } from 'react-polyglot';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function UnknownPreview({ field }) {
|
||||
const UnknownPreview = ({ field, t }) => {
|
||||
return (
|
||||
<div className="nc-widgetPreview">
|
||||
No preview for widget “{field.get('widget')}
|
||||
”.
|
||||
{t('editor.editorWidgets.unknownPreview.noPreview', field.get('widget'))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
UnknownPreview.propTypes = {
|
||||
field: ImmutablePropTypes.map,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default translate()(UnknownPreview);
|
||||
|
@ -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));
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'react-emotion';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { Modal } from 'UI';
|
||||
import MediaLibrarySearch from './MediaLibrarySearch';
|
||||
import MediaLibraryHeader from './MediaLibraryHeader';
|
||||
@ -93,6 +94,7 @@ const MediaLibraryModal = ({
|
||||
handleAssetClick,
|
||||
handleLoadMore,
|
||||
getDisplayURL,
|
||||
t,
|
||||
}) => {
|
||||
const filteredFiles = forImage ? handleFilter(files) : files;
|
||||
const queriedFiles = !dynamicSearch && query ? handleQuery(query, filteredFiles) : filteredFiles;
|
||||
@ -103,11 +105,11 @@ const MediaLibraryModal = ({
|
||||
const hasMedia = hasSearchResults;
|
||||
const shouldShowEmptyMessage = !hasMedia;
|
||||
const emptyMessage =
|
||||
(isLoading && !hasMedia && 'Loading...') ||
|
||||
(dynamicSearchActive && 'No results.') ||
|
||||
(!hasFiles && 'No assets found.') ||
|
||||
(!hasFilteredFiles && 'No images found.') ||
|
||||
(!hasSearchResults && 'No results.');
|
||||
(isLoading && !hasMedia && t('mediaLibrary.mediaLibraryModal.loading')) ||
|
||||
(dynamicSearchActive && t('mediaLibrary.mediaLibraryModal.noResults')) ||
|
||||
(!hasFiles && t('mediaLibrary.mediaLibraryModal.noAssetsFound')) ||
|
||||
(!hasFilteredFiles && t('mediaLibrary.mediaLibraryModal.noImagesFound')) ||
|
||||
(!hasSearchResults && t('mediaLibrary.mediaLibraryModal.noResults'));
|
||||
const hasSelection = hasMedia && !isEmpty(selectedFile);
|
||||
const shouldShowButtonLoader = isPersisting || isDeleting;
|
||||
|
||||
@ -117,21 +119,33 @@ const MediaLibraryModal = ({
|
||||
<div>
|
||||
<MediaLibraryHeader
|
||||
onClose={handleClose}
|
||||
title={`${privateUpload ? 'Private ' : ''}${forImage ? 'Images' : 'Media assets'}`}
|
||||
title={`${privateUpload ? t('mediaLibrary.mediaLibraryModal.private') : ''}${
|
||||
forImage
|
||||
? t('mediaLibrary.mediaLibraryModal.images')
|
||||
: t('mediaLibrary.mediaLibraryModal.mediaAssets')
|
||||
}`}
|
||||
isPrivate={privateUpload}
|
||||
/>
|
||||
<MediaLibrarySearch
|
||||
value={query}
|
||||
onChange={handleSearchChange}
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
placeholder="Search..."
|
||||
placeholder={t('mediaLibrary.mediaLibraryModal.search')}
|
||||
disabled={!dynamicSearchActive && !hasFilteredFiles}
|
||||
/>
|
||||
</div>
|
||||
<MediaLibraryActions
|
||||
uploadButtonLabel={isPersisting ? 'Uploading...' : 'Upload new'}
|
||||
deleteButtonLabel={isDeleting ? 'Deleting...' : 'Delete selected'}
|
||||
insertButtonLabel="Choose selected"
|
||||
uploadButtonLabel={
|
||||
isPersisting
|
||||
? t('mediaLibrary.mediaLibraryModal.uploading')
|
||||
: t('mediaLibrary.mediaLibraryModal.uploadNew')
|
||||
}
|
||||
deleteButtonLabel={
|
||||
isDeleting
|
||||
? t('mediaLibrary.mediaLibraryModal.deleting')
|
||||
: t('mediaLibrary.mediaLibraryModal.deleteSelected')
|
||||
}
|
||||
insertButtonLabel={t('mediaLibrary.mediaLibraryModal.chooseSelected')}
|
||||
uploadEnabled={!shouldShowButtonLoader}
|
||||
deleteEnabled={!shouldShowButtonLoader && hasSelection}
|
||||
insertEnabled={hasSelection}
|
||||
@ -153,7 +167,7 @@ const MediaLibraryModal = ({
|
||||
canLoadMore={hasNextPage}
|
||||
onLoadMore={handleLoadMore}
|
||||
isPaginating={isPaginating}
|
||||
paginatingMessage="Loading..."
|
||||
paginatingMessage={t('mediaLibrary.mediaLibraryModal.loading')}
|
||||
cardWidth={cardWidth}
|
||||
cardMargin={cardMargin}
|
||||
isPrivate={privateUpload}
|
||||
@ -200,6 +214,7 @@ MediaLibraryModal.propTypes = {
|
||||
handleAssetClick: PropTypes.func.isRequired,
|
||||
handleLoadMore: PropTypes.func.isRequired,
|
||||
getDisplayURL: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default MediaLibraryModal;
|
||||
export default translate()(MediaLibraryModal);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { css } from 'react-emotion';
|
||||
import { colors } from 'netlify-cms-ui-default';
|
||||
|
||||
@ -14,9 +15,10 @@ const styles = {
|
||||
`,
|
||||
};
|
||||
|
||||
export class ErrorBoundary extends React.Component {
|
||||
class ErrorBoundary extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -34,23 +36,25 @@ export class ErrorBoundary extends React.Component {
|
||||
if (!hasError) {
|
||||
return this.props.children;
|
||||
}
|
||||
const t = this.props.t;
|
||||
return (
|
||||
<div className={styles.errorBoundary}>
|
||||
<h1 className={styles.errorBoundaryText}>Sorry!</h1>
|
||||
<h1 className={styles.errorBoundaryText}>{t('ui.errorBoundary.title')}</h1>
|
||||
<p>
|
||||
<span>{"There's been an error - please "}</span>
|
||||
<span>{t('ui.errorBoundary.details')}</span>
|
||||
<a
|
||||
href={ISSUE_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.errorBoundaryText}
|
||||
>
|
||||
report it
|
||||
{t('ui.errorBoundary.reportIt')}
|
||||
</a>
|
||||
!
|
||||
</p>
|
||||
<p>{errorMessage}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate()(ErrorBoundary);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled, { css } from 'react-emotion';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { Icon, Dropdown, DropdownItem, DropdownButton, colors } from 'netlify-cms-ui-default';
|
||||
import { stripProtocol } from 'Lib/urlHelper';
|
||||
|
||||
@ -47,7 +48,7 @@ Avatar.propTypes = {
|
||||
imageUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
const SettingsDropdown = ({ displayUrl, imageUrl, onLogoutClick }) => (
|
||||
const SettingsDropdown = ({ displayUrl, imageUrl, onLogoutClick, t }) => (
|
||||
<React.Fragment>
|
||||
{displayUrl ? (
|
||||
<AppHeaderSiteLink href={displayUrl} target="_blank">
|
||||
@ -64,7 +65,7 @@ const SettingsDropdown = ({ displayUrl, imageUrl, onLogoutClick }) => (
|
||||
</DropdownButton>
|
||||
)}
|
||||
>
|
||||
<DropdownItem label="Log Out" onClick={onLogoutClick} />
|
||||
<DropdownItem label={t('ui.settingsDropdown.logOut')} onClick={onLogoutClick} />
|
||||
</Dropdown>
|
||||
</React.Fragment>
|
||||
);
|
||||
@ -73,6 +74,7 @@ SettingsDropdown.propTypes = {
|
||||
displayUrl: PropTypes.string,
|
||||
imageUrl: PropTypes.string,
|
||||
onLogoutClick: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SettingsDropdown;
|
||||
export default translate()(SettingsDropdown);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { css, injectGlobal, cx } from 'react-emotion';
|
||||
import { translate } from 'react-polyglot';
|
||||
import reduxNotificationsStyles from 'redux-notifications/lib/styles.css';
|
||||
import { shadows, colors, lengths } from 'netlify-cms-ui-default';
|
||||
|
||||
@ -41,11 +42,16 @@ const styles = {
|
||||
`,
|
||||
};
|
||||
|
||||
export const Toast = ({ kind, message }) => (
|
||||
<div className={cx(styles.toast, styles[kind])}>{message}</div>
|
||||
const Toast = ({ kind, message, t }) => (
|
||||
<div className={cx(styles.toast, styles[kind])}>
|
||||
{t(message.key, { details: message.details })}
|
||||
</div>
|
||||
);
|
||||
|
||||
Toast.propTypes = {
|
||||
kind: PropTypes.oneOf(['info', 'success', 'warning', 'danger']).isRequired,
|
||||
message: PropTypes.string,
|
||||
message: PropTypes.object,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default translate()(Toast);
|
||||
|
@ -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';
|
||||
|
@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled from 'react-emotion';
|
||||
import { OrderedMap } from 'immutable';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
Dropdown,
|
||||
@ -60,6 +61,7 @@ class Workflow extends Component {
|
||||
updateUnpublishedEntryStatus: PropTypes.func.isRequired,
|
||||
publishUnpublishedEntry: PropTypes.func.isRequired,
|
||||
deleteUnpublishedEntry: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@ -78,10 +80,11 @@ class Workflow extends Component {
|
||||
publishUnpublishedEntry,
|
||||
deleteUnpublishedEntry,
|
||||
collections,
|
||||
t,
|
||||
} = this.props;
|
||||
|
||||
if (!isEditorialWorkflow) return null;
|
||||
if (isFetching) return <Loader active>Loading Editorial Workflow Entries</Loader>;
|
||||
if (isFetching) return <Loader active>{t('workflow.workflow.loading')}</Loader>;
|
||||
const reviewCount = unpublishedEntries.get('pending_review').size;
|
||||
const readyCount = unpublishedEntries.get('pending_publish').size;
|
||||
|
||||
@ -89,12 +92,14 @@ class Workflow extends Component {
|
||||
<WorkflowContainer>
|
||||
<WorkflowTop>
|
||||
<WorkflowTopRow>
|
||||
<WorkflowTopHeading>Editorial Workflow</WorkflowTopHeading>
|
||||
<WorkflowTopHeading>{t('workflow.workflow.workflowHeading')}</WorkflowTopHeading>
|
||||
<Dropdown
|
||||
dropdownWidth="160px"
|
||||
dropdownPosition="left"
|
||||
dropdownTopOverlap="40px"
|
||||
renderButton={() => <StyledDropdownButton>New Post</StyledDropdownButton>}
|
||||
renderButton={() => (
|
||||
<StyledDropdownButton>{t('workflow.workflow.newPost')}</StyledDropdownButton>
|
||||
)}
|
||||
>
|
||||
{collections
|
||||
.filter(collection => collection.get('create'))
|
||||
@ -109,8 +114,10 @@ class Workflow extends Component {
|
||||
</Dropdown>
|
||||
</WorkflowTopRow>
|
||||
<WorkflowTopDescription>
|
||||
{reviewCount} {reviewCount === 1 ? 'entry' : 'entries'} waiting for review, {readyCount}{' '}
|
||||
ready to go live.
|
||||
{t('workflow.workflow.description', {
|
||||
smart_count: reviewCount,
|
||||
readyCount: readyCount,
|
||||
})}
|
||||
</WorkflowTopDescription>
|
||||
</WorkflowTop>
|
||||
<WorkflowList
|
||||
@ -153,4 +160,4 @@ export default connect(
|
||||
publishUnpublishedEntry,
|
||||
deleteUnpublishedEntry,
|
||||
},
|
||||
)(Workflow);
|
||||
)(translate()(Workflow));
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled, { css } from 'react-emotion';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { components, colors, colorsRaw, transitions, buttons } from 'netlify-cms-ui-default';
|
||||
|
||||
@ -107,6 +108,7 @@ const WorkflowCard = ({
|
||||
onDelete,
|
||||
canPublish,
|
||||
onPublish,
|
||||
t,
|
||||
}) => (
|
||||
<WorkflowCardContainer>
|
||||
<WorkflowLink to={editLink}>
|
||||
@ -119,10 +121,14 @@ const WorkflowCard = ({
|
||||
</WorkflowLink>
|
||||
<CardButtonContainer>
|
||||
<DeleteButton onClick={onDelete}>
|
||||
{isModification ? 'Delete changes' : 'Delete new entry'}
|
||||
{isModification
|
||||
? t('workflow.workflowCard.deleteChanges')
|
||||
: t('workflow.workflowCard.deleteNewEntry')}
|
||||
</DeleteButton>
|
||||
<PublishButton disabled={!canPublish} onClick={onPublish}>
|
||||
{isModification ? 'Publish changes' : 'Publish new entry'}
|
||||
{isModification
|
||||
? t('workflow.workflowCard.publishChanges')
|
||||
: t('workflow.workflowCard.publishNewEntry')}
|
||||
</PublishButton>
|
||||
</CardButtonContainer>
|
||||
</WorkflowCardContainer>
|
||||
@ -139,6 +145,7 @@ WorkflowCard.propTypes = {
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
canPublish: PropTypes.bool.isRequired,
|
||||
onPublish: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default WorkflowCard;
|
||||
export default translate()(WorkflowCard);
|
||||
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled, { css, cx } from 'react-emotion';
|
||||
import moment from 'moment';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { colors, lengths } from 'netlify-cms-ui-default';
|
||||
import { status } from 'Constants/publishModes';
|
||||
import { DragSource, DropTarget, HTML5DragDrop } from 'UI';
|
||||
@ -96,14 +97,14 @@ const ColumnCount = styled.p`
|
||||
// This is a namespace so that we can only drop these elements on a DropTarget with the same
|
||||
const DNDNamespace = 'cms-workflow';
|
||||
|
||||
const getColumnHeaderText = columnName => {
|
||||
const getColumnHeaderText = (columnName, t) => {
|
||||
switch (columnName) {
|
||||
case 'draft':
|
||||
return 'Drafts';
|
||||
return t('workflow.workflowList.draftHeader');
|
||||
case 'pending_review':
|
||||
return 'In Review';
|
||||
return t('workflow.workflowList.inReviewHeader');
|
||||
case 'pending_publish':
|
||||
return 'Ready';
|
||||
return t('workflow.workflowList.readyHeader');
|
||||
}
|
||||
};
|
||||
|
||||
@ -113,6 +114,7 @@ class WorkflowList extends React.Component {
|
||||
handleChangeStatus: PropTypes.func.isRequired,
|
||||
handlePublish: PropTypes.func.isRequired,
|
||||
handleDelete: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChangeStatus = (newStatus, dragProps) => {
|
||||
@ -123,20 +125,16 @@ class WorkflowList extends React.Component {
|
||||
};
|
||||
|
||||
requestDelete = (collection, slug, ownStatus) => {
|
||||
if (window.confirm('Are you sure you want to delete this entry?')) {
|
||||
if (window.confirm(this.props.t('workflow.workflowList.onDeleteEntry'))) {
|
||||
this.props.handleDelete(collection, slug, ownStatus);
|
||||
}
|
||||
};
|
||||
|
||||
requestPublish = (collection, slug, ownStatus) => {
|
||||
if (ownStatus !== status.last()) {
|
||||
window.alert(
|
||||
`Only items with a "Ready" status can be published.
|
||||
|
||||
Please drag the card to the "Ready" column to enable publishing.`,
|
||||
);
|
||||
window.alert(this.props.t('workflow.workflowList.onPublishingNotReadyEntry'));
|
||||
return;
|
||||
} else if (!window.confirm('Are you sure you want to publish this entry?')) {
|
||||
} else if (!window.confirm(this.props.t('workflow.workflowList.onPublishEntry'))) {
|
||||
return;
|
||||
}
|
||||
this.props.handlePublish(collection, slug);
|
||||
@ -155,9 +153,13 @@ Please drag the card to the "Ready" column to enable publishing.`,
|
||||
{(connect, { isHovered }) =>
|
||||
connect(
|
||||
<div className={cx(styles.column, { [styles.columnHovered]: isHovered })}>
|
||||
<ColumnHeader name={currColumn}>{getColumnHeaderText(currColumn)}</ColumnHeader>
|
||||
<ColumnHeader name={currColumn}>
|
||||
{getColumnHeaderText(currColumn, this.props.t)}
|
||||
</ColumnHeader>
|
||||
<ColumnCount>
|
||||
{currEntries.size} {currEntries.size === 1 ? 'entry' : 'entries'}
|
||||
{this.props.t('workflow.workflowList.currentEntries', {
|
||||
smart_count: currEntries.size,
|
||||
})}
|
||||
</ColumnCount>
|
||||
{this.renderColumns(currEntries, currColumn)}
|
||||
</div>,
|
||||
@ -218,4 +220,4 @@ Please drag the card to the "Ready" column to enable publishing.`,
|
||||
}
|
||||
}
|
||||
|
||||
export default HTML5DragDrop(WorkflowList);
|
||||
export default HTML5DragDrop(translate()(WorkflowList));
|
||||
|
160
packages/netlify-cms-core/src/constants/defaultPhrases.js
Normal file
160
packages/netlify-cms-core/src/constants/defaultPhrases.js
Normal 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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
31
yarn.lock
31
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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user