feat: add translation support (#2870)

* feat: add translation support

* test(cypress): fix locale import

* docs: add locale documentation

* feat: add german translation (#2877)

* fix: locales package version, register all locales in netlify-cms
This commit is contained in:
Erez Rokah 2019-11-14 11:25:04 +02:00 committed by GitHub
parent 4833f33728
commit 096b067d45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 660 additions and 191 deletions

View File

@ -37,6 +37,7 @@ const defaultPlugins = [
Lib: './src/lib',
MediaLibrary: './src/components/MediaLibrary',
Reducers: './src/reducers',
Selectors: './src/selectors',
ReduxStore: './src/redux',
Routing: './src/routing',
UI: './src/components/UI',
@ -56,6 +57,7 @@ const defaultPlugins = [
Integrations: path.join(__dirname, 'packages/netlify-cms-core/src/integrations/'),
Lib: path.join(__dirname, 'packages/netlify-cms-core/src/lib/'),
Reducers: path.join(__dirname, 'packages/netlify-cms-core/src/reducers/'),
Selectors: path.join(__dirname, 'packages/netlify-cms-core/src/selectors/'),
ReduxStore: path.join(__dirname, 'packages/netlify-cms-core/src/redux/'),
Routing: path.join(__dirname, 'packages/netlify-cms-core/src/routing/'),
ValueObjects: path.join(__dirname, 'packages/netlify-cms-core/src/valueObjects/'),

View File

@ -1,4 +1,4 @@
import { getPhrases } from 'Constants/defaultPhrases';
import { en } from '../../packages/netlify-cms-locales/src';
// Prevents unsaved changes in dev local storage from being used
Cypress.on('window:confirm', message => {
@ -6,7 +6,7 @@ Cypress.on('window:confirm', message => {
editor: {
editor: { confirmLoadBackup },
},
} = getPhrases();
} = en;
switch (message) {
case confirmLoadBackup:

View File

@ -34,6 +34,7 @@
"netlify-cms-editor-component-image": "^2.4.3",
"netlify-cms-lib-auth": "^2.2.4",
"netlify-cms-lib-util": "^2.4.0-beta.4",
"netlify-cms-locales": "^1.0.0",
"netlify-cms-ui-default": "^2.7.0-beta.1",
"netlify-cms-widget-boolean": "^2.2.3",
"netlify-cms-widget-date": "^2.3.5",

View File

@ -2,6 +2,7 @@ import { NetlifyCmsCore as CMS } from 'netlify-cms-core';
import './backends';
import './widgets';
import './editor-components';
import './locales';
if (typeof window !== 'undefined') {
/**

View File

@ -0,0 +1,4 @@
import { NetlifyCmsCore as CMS } from 'netlify-cms-core';
import { en } from 'netlify-cms-locales';
CMS.registerLocale('en', en);

View File

@ -1,12 +1,13 @@
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { Provider, connect } from 'react-redux';
import { Route } from 'react-router-dom';
import { ConnectedRouter } from 'react-router-redux';
import history from 'Routing/history';
import store from 'ReduxStore';
import { mergeConfig } from 'Actions/config';
import { getPhrases } from 'Constants/defaultPhrases';
import { getPhrases } from 'Lib/phrases';
import { selectLocale } from 'Selectors/config';
import { I18n } from 'react-polyglot';
import { GlobalStyles } from 'netlify-cms-ui-default';
import { ErrorBoundary } from 'UI';
@ -17,6 +18,24 @@ import 'what-input';
const ROOT_ID = 'nc-root';
const TranslatedApp = ({ locale }) => {
return (
<I18n locale={locale} messages={getPhrases(locale)}>
<ErrorBoundary showBackup>
<ConnectedRouter history={history}>
<Route component={App} />
</ConnectedRouter>
</ErrorBoundary>
</I18n>
);
};
const mapDispatchToProps = state => {
return { locale: selectLocale(state.config) };
};
const ConnectedTranslatedApp = connect(mapDispatchToProps)(TranslatedApp);
function bootstrap(opts = {}) {
const { config } = opts;
@ -63,15 +82,9 @@ function bootstrap(opts = {}) {
const Root = () => (
<>
<GlobalStyles />
<I18n locale={'en'} messages={getPhrases()}>
<ErrorBoundary showBackup>
<Provider store={store}>
<ConnectedRouter history={history}>
<Route component={App} />
</ConnectedRouter>
<ConnectedTranslatedApp />
</Provider>
</ErrorBoundary>
</I18n>
</>
);

View File

@ -42,6 +42,7 @@ const getConfigSchema = () => ({
},
required: ['name'],
},
locale: { type: 'string', examples: ['en', 'fr', 'de'] },
site_url: { type: 'string', examples: ['https://example.com'] },
display_url: { type: 'string', examples: ['https://example.com'] },
logo_url: { type: 'string', examples: ['https://example.com/images/logo.svg'] },

View File

@ -1,178 +0,0 @@
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.',
range: '%{fieldLabel} must be between %{minValue} and %{maxValue}.',
min: '%{fieldLabel} must be at least %{minValue}.',
max: '%{fieldLabel} must be %{maxValue} or less.',
},
},
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...',
confirmLoadBackup: 'A local backup was recovered for this entry, would you like to use it?',
},
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',
deployPreviewPendingButtonLabel: 'Check for Preview',
deployPreviewButtonLabel: 'View Preview',
deployButtonLabel: 'View Live',
},
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: 'Error',
details: "There's been an error - please ",
reportIt: 'report it.',
detailsHeading: 'Details',
recoveredEntry: {
heading: 'Recovered document',
warning: 'Please copy/paste this somewhere before navigating away!',
copyButtonLabel: 'Copy to clipboard',
},
},
settingsDropdown: {
logOut: 'Log Out',
},
toast: {
onFailToLoadEntries: 'Failed to load entry: %{details}',
onFailToLoadDeployPreview: 'Failed to load preview: %{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',
onFailToAuth: '%{details}',
},
},
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: {
lastChange: '%{date} by %{author}',
lastChangeNoAuthor: '%{date}',
lastChangeNoDate: 'by %{author}',
deleteChanges: 'Delete changes',
deleteNewEntry: 'Delete new entry',
publishChanges: 'Publish changes',
publishNewEntry: 'Publish new entry',
},
workflowList: {
onDeleteEntry: 'Are you sure you want to delete this entry?',
onPublishingNotReadyEntry:
'Only items with a "Ready" status can be published. Please drag the card to the "Ready" column to enable publishing.',
onPublishEntry: 'Are you sure you want to publish this entry?',
draftHeader: 'Drafts',
inReviewHeader: 'In Review',
readyHeader: 'Ready',
currentEntries: '%{smart_count} entry |||| %{smart_count} entries',
},
},
};
}

View File

@ -0,0 +1,119 @@
import { getPhrases } from '../phrases';
jest.mock('../registry');
describe('defaultPhrases', () => {
it('should merge en locale with given locale', () => {
const { getLocale } = require('../registry');
const locales = {
en: {
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',
},
},
},
de: {
app: {
header: {
content: 'Inhalt',
},
},
},
};
getLocale.mockImplementation(locale => locales[locale]);
expect(getPhrases('de')).toEqual({
app: {
header: {
content: 'Inhalt',
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',
},
},
});
});
it('should not mutate default phrases', () => {
const { getLocale } = require('../registry');
const locales = {
en: {
app: {
header: {
content: 'Contents',
},
},
},
de: {
app: {
header: {
content: 'Inhalt',
},
},
},
};
getLocale.mockImplementation(locale => locales[locale]);
const result = getPhrases('de');
expect(result === locales['en']).toBe(false);
});
});

View File

@ -0,0 +1,42 @@
import { registerLocale, getLocale } from '../registry';
jest.spyOn(console, 'error').mockImplementation(() => {});
describe('registry', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
});
describe('registerLocale', () => {
it('should log error when name is empty', () => {
registerLocale();
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledWith(
"Locale parameters invalid. example: CMS.registerLocale('locale', phrases)",
);
});
it('should log error when phrases are undefined', () => {
registerLocale('fr');
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledWith(
"Locale parameters invalid. example: CMS.registerLocale('locale', phrases)",
);
});
it('should register locale', () => {
const phrases = {
app: {
header: {
content: 'Inhalt',
},
},
};
registerLocale('de', phrases);
expect(getLocale('de')).toBe(phrases);
});
});
});

View File

@ -0,0 +1,7 @@
import { merge } from 'lodash';
import { getLocale } from './registry';
export function getPhrases(locale) {
const phrases = merge({}, getLocale('en'), getLocale(locale));
return phrases;
}

View File

@ -13,6 +13,7 @@ const registry = {
editorComponents: Map(),
widgetValueSerializers: {},
mediaLibraries: [],
locales: {},
};
export default {
@ -31,6 +32,8 @@ export default {
getBackend,
registerMediaLibrary,
getMediaLibrary,
registerLocale,
getLocale,
};
/**
@ -156,3 +159,18 @@ export function registerMediaLibrary(mediaLibrary, options) {
export function getMediaLibrary(name) {
return registry.mediaLibraries.find(ml => ml.name === name);
}
/**
* Locales
*/
export function registerLocale(locale, phrases) {
if (!locale || !phrases) {
console.error("Locale parameters invalid. example: CMS.registerLocale('locale', phrases)");
} else {
registry.locales[locale] = phrases;
}
}
export function getLocale(locale) {
return registry.locales[locale];
}

View File

@ -0,0 +1 @@
export const selectLocale = state => state.get('locale', 'en');

View File

@ -0,0 +1,20 @@
# Netlify CMS Locales
## Default translations for Netlify CMS
The English translation is loaded by default.
To register another locale you can use the following code:
```js
import CMS from 'netlify-cms-app';
import { de } from 'netlify-cms-locales';
CMS.registerLocale('de', de);
```
> When importing `netlify-cms` all locales are registered by default.
Make sure the specific locale exists in the package - if not, we will happily accept a pull request for it.
The configured locale will be merge into the english one so don't worry about missing some phrases.

View File

@ -0,0 +1,21 @@
{
"name": "netlify-cms-locales",
"description": "Locales for Netlify CMS.",
"version": "1.0.0",
"repository": "https://github.com/netlify/netlify-cms/tree/master/packages/netlify-cms-locales",
"bugs": "https://github.com/netlify/netlify-cms/issues",
"license": "MIT",
"module": "dist/esm/index.js",
"main": "dist/netlify-cms-locales.js",
"keywords": [
"netlify-cms"
],
"sideEffects": false,
"scripts": {
"develop": "yarn build:esm --watch",
"build": "cross-env NODE_ENV=production webpack",
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward"
},
"dependencies": {},
"peerDependencies": {}
}

View File

@ -0,0 +1,180 @@
const de = {
app: {
header: {
content: 'Inhalt',
workflow: 'Arbeitsablauf',
media: 'Medien',
quickAdd: 'Schnell-Erstellung',
},
app: {
errorHeader: 'Fehler beim laden der CMS-Konfiguration.',
configErrors: 'Konfigurationsfehler',
checkConfigYml: 'Überprüfen Sie die config.yml Konfigurationsdatei.',
loadingConfig: 'Konfiguration laden...',
waitingBackend: 'Auf Server warten...',
},
notFoundPage: {
header: 'Nicht gefunden',
},
},
collection: {
sidebar: {
collections: 'Inhaltstypen',
searchAll: 'Alles durchsuchen',
},
collectionTop: {
viewAs: 'Anzeigen als',
newButton: 'Neuer %{collectionLabel}',
},
entries: {
loadingEntries: 'Beiträge laden',
cachingEntries: 'Beiträge zwischenspeichern',
longerLoading: 'Diese Aktion kann einige Minuten in Anspruch nehmen',
},
},
editor: {
editorControlPane: {
widget: {
required: '%{fieldLabel} ist erforderlich.',
regexPattern: '%{fieldLabel} entspricht nicht dem Muster: %{pattern}.',
processing: '%{fieldLabel} wird verarbeitet.',
range: '%{fieldLabel} muss zwischen %{minValue} und %{maxValue} liegen.',
min: '%{fieldLabel} muss größer als %{minValue} sein.',
max: '%{fieldLabel} darf nicht größer als %{maxValue} sein.',
},
},
editor: {
onLeavePage: 'Möchten Sie diese Seite wirklich verlassen?',
onUpdatingWithUnsavedChanges:
'Es sind noch ungespeicherte Änderungen vorhanden. Bitte speichern Sie diese, bevor Sie den Status aktualisieren.',
onPublishingNotReady:
'Bitte setzten die den Status auf "Abgeschlossen" vor dem Veröffentlichen.',
onPublishingWithUnsavedChanges:
'Es sind noch ungespeicherte Änderungen vorhanden. Bitte speicheren Sie vor dem Veröffentlichen.',
onPublishing: 'Soll dieser Beitrag wirklich veröffentlich werden?',
onDeleteWithUnsavedChanges:
'Möchten Sie diesen veröffentlichten Beitrag, sowie Ihre nicht gespeicherten Änderungen löschen?',
onDeletePublishedEntry: 'Soll dieser veröffentlichte Beitrag wirklich gelöscht werden?',
onDeleteUnpublishedChangesWithUnsavedChanges:
'Möchten Sie diesen unveröffentlichten Beitrag, sowie Ihre nicht gespeicherten Änderungen löschen?',
onDeleteUnpublishedChanges:
'Alle unveröffentlichten Änderungen werden gelöscht. Möchten Sie wirklich löschen?',
loadingEntry: 'Beitrag laden...',
confirmLoadBackup:
'Für diesen Beitrag ist ein lokales Backup vorhanden. Möchten Sie dieses benutzen?',
},
editorToolbar: {
publishing: 'Veröffentlichen...',
publish: 'Veröffentlichen',
published: 'Veröffentlicht',
publishAndCreateNew: 'Veröffentlichen und neuen Beitrag erstellen',
deleteUnpublishedChanges: 'Unveröffentlichte Änderungen verwerfen',
deleteUnpublishedEntry: 'Lösche unveröffentlichten Beitrag',
deletePublishedEntry: 'Lösche veröffentlichten Beitrag',
deleteEntry: 'Lösche Beitrag',
saving: 'Speichern...',
save: 'Speichern',
deleting: 'Entfernen...',
updating: 'Aktualisieren...',
setStatus: 'Status setzen',
backCollection: 'Zurück zu allen %{collectionLabel}',
unsavedChanges: 'Ungespeicherte Änderungen',
changesSaved: 'Änderungen gespeichert',
draft: 'Entwurf',
inReview: 'Zur Überprüfung',
ready: 'Abgeschlossen',
publishNow: 'Jetzt veröffentlichen',
deployPreviewPendingButtonLabel: 'Überprüfen ob eine Vorschau vorhanden ist',
deployPreviewButtonLabel: 'Vorschau anzeigen',
deployButtonLabel: 'Live ansehen',
},
editorWidgets: {
unknownControl: {
noControl: "Kein Bedienelement für Widget '%{widget}'.",
},
unknownPreview: {
noPreview: "Keine Vorschau für Widget '%{widget}'.",
},
},
},
mediaLibrary: {
mediaLibrary: {
onDelete: 'Soll das ausgewählte Medium wirklich gelöscht werden?',
},
mediaLibraryModal: {
loading: 'Laden...',
noResults: 'Keine Egebnisse.',
noAssetsFound: 'Keine Medien gefunden.',
noImagesFound: 'Keine Bilder gefunden.',
private: 'Privat ',
images: 'Bilder',
mediaAssets: 'Medien',
search: 'Suchen...',
uploading: 'Hochladen...',
uploadNew: 'Hochladen',
deleting: 'Löschen...',
deleteSelected: 'Ausgewähltes Medium löschen',
chooseSelected: 'Ausgewähltes Medium verwenden',
},
},
ui: {
errorBoundary: {
title: 'Fehler',
details: 'Ein Fehler ist aufgetreten - bitte ',
reportIt: 'berichte ihn.',
detailsHeading: 'Details',
recoveredEntry: {
heading: 'Widerhergestellter Beitrag',
warning: 'Bitte speichern Sie sich das bevor Sie die Seite verlassen!',
copyButtonLabel: 'In Zwischenablage speichern',
},
},
settingsDropdown: {
logOut: 'Abmelden',
},
toast: {
onFailToLoadEntries: 'Beitrag konnte nicht geladen werden: %{details}',
onFailToLoadDeployPreview: 'Vorschau konnte nicht geladen werden: %{details}',
onFailToPersist: 'Beitrag speichern fehlgeschlagen: %{details}',
onFailToDelete: 'Beitrag löschen fehlgeschlagen: %{details}',
onFailToUpdateStatus: 'Status aktualisieren fehlgeschlagen: %{details}',
missingRequiredField: 'Oops, einige zwingend erforderliche Felder sind nicht ausgefüllt.',
entrySaved: 'Beitrag gespeichert',
entryPublished: 'Beitrag veröffentlicht',
onFailToPublishEntry: 'Veröffentlichen fehlgeschlagen: %{details}',
entryUpdated: 'Beitragsstatus aktualisiert',
onDeleteUnpublishedChanges: 'Unveröffentlichte Änderungen verworfen',
onFailToAuth: '%{details}',
},
},
workflow: {
workflow: {
loading: 'Arbeitsablauf Beiträge laden',
workflowHeading: 'Redaktioneller Arbeitsablauf',
newPost: 'Neuer Beitrag',
description:
'%{smart_count} Beitrag zur Überprüfung bereit, %{readyCount} bereit zur Veröffentlichung. |||| %{smart_count} Beiträge zur Überprüfung bereit, %{readyCount} bereit zur Veröffentlichung. ',
},
workflowCard: {
lastChange: '%{date} von %{author}',
lastChangeNoAuthor: '%{date}',
lastChangeNoDate: 'von %{author}',
deleteChanges: 'Änderungen verwerfen',
deleteNewEntry: 'Lösche neuen Beitrag',
publishChanges: 'Veröffentliche Änderungen',
publishNewEntry: 'Veröffentliche neuen Beitrag',
},
workflowList: {
onDeleteEntry: 'Soll dieser Beitrag wirklich gelöscht werden?',
onPublishingNotReadyEntry:
'Nur Beiträge im Status "Abgeschlossen" können veröffentlicht werden. Bitte ziehen Sie den Beitrag in die "Abgeschlossen" Spalte um die Veröffentlichung zu aktivieren.',
onPublishEntry: 'Soll dieser Beitrag wirklich veröffentlicht werden soll?',
draftHeader: 'Entwurf',
inReviewHeader: 'Zur Überprüfung',
readyHeader: 'Abgeschlossen',
currentEntries: '%{smart_count} Beitrag |||| %{smart_count} Beiträge',
},
},
};
export default de;

View File

@ -0,0 +1,176 @@
const en = {
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.',
range: '%{fieldLabel} must be between %{minValue} and %{maxValue}.',
min: '%{fieldLabel} must be at least %{minValue}.',
max: '%{fieldLabel} must be %{maxValue} or less.',
},
},
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...',
confirmLoadBackup: 'A local backup was recovered for this entry, would you like to use it?',
},
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',
deployPreviewPendingButtonLabel: 'Check for Preview',
deployPreviewButtonLabel: 'View Preview',
deployButtonLabel: 'View Live',
},
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: 'Error',
details: "There's been an error - please ",
reportIt: 'report it.',
detailsHeading: 'Details',
recoveredEntry: {
heading: 'Recovered document',
warning: 'Please copy/paste this somewhere before navigating away!',
copyButtonLabel: 'Copy to clipboard',
},
},
settingsDropdown: {
logOut: 'Log Out',
},
toast: {
onFailToLoadEntries: 'Failed to load entry: %{details}',
onFailToLoadDeployPreview: 'Failed to load preview: %{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',
onFailToAuth: '%{details}',
},
},
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: {
lastChange: '%{date} by %{author}',
lastChangeNoAuthor: '%{date}',
lastChangeNoDate: 'by %{author}',
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',
},
},
};
export default en;

View File

@ -0,0 +1,2 @@
export { default as en } from './en';
export { default as de } from './de';

View File

@ -0,0 +1,3 @@
const { getConfig } = require('../../scripts/webpack.js');
module.exports = getConfig();

View File

@ -2,6 +2,7 @@ import createReactClass from 'create-react-class';
import React from 'react';
import { NetlifyCmsApp as CMS } from 'netlify-cms-app/dist/esm';
import './media-libraries';
import './locales';
/**
* Load Netlify CMS automatically if `window.CMS_MANUAL_INIT` is set.

View File

@ -0,0 +1,6 @@
import { NetlifyCmsApp as CMS } from 'netlify-cms-app/dist/esm';
import * as locales from 'netlify-cms-locales';
Object.keys(locales).forEach(locale => {
CMS.registerLocale(locale, locales[locale]);
});

View File

@ -120,6 +120,35 @@ When the `logo_url` setting is specified, the CMS UI will change the logo displa
logo_url: https://your-site.com/images/logo.svg
```
## Locale
The CMS locale.
Defaults to `en`.
Other languages than English must be registered manually.
**Example**
In your `config.yml`:
```yaml
locale: 'de'
```
And in your custom JavaScript code:
```js
import CMS from 'netlify-cms-app';
import { de } from 'netlify-cms-locales';
CMS.registerLocale('de', de);
```
When a translation for the selected locale is missing the English one will be used.
> When importing `netlify-cms` all locales are registered by default (so you only need to update your `config.yml`).
## Show Preview Links
[Deploy preview links](../deploy-preview-links) can be disabled by setting `show_preview_links` to `false`.