diff --git a/babel.config.js b/babel.config.js
index 34983862..29e460af 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -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/'),
diff --git a/cypress/utils/dismiss-local-backup.js b/cypress/utils/dismiss-local-backup.js
index 8cdb5015..365721f0 100644
--- a/cypress/utils/dismiss-local-backup.js
+++ b/cypress/utils/dismiss-local-backup.js
@@ -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:
diff --git a/packages/netlify-cms-app/package.json b/packages/netlify-cms-app/package.json
index 0c29c96c..8eb09762 100644
--- a/packages/netlify-cms-app/package.json
+++ b/packages/netlify-cms-app/package.json
@@ -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",
diff --git a/packages/netlify-cms-app/src/index.js b/packages/netlify-cms-app/src/index.js
index 7c772f77..e62e2077 100644
--- a/packages/netlify-cms-app/src/index.js
+++ b/packages/netlify-cms-app/src/index.js
@@ -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') {
/**
diff --git a/packages/netlify-cms-app/src/locales.js b/packages/netlify-cms-app/src/locales.js
new file mode 100644
index 00000000..51f7c759
--- /dev/null
+++ b/packages/netlify-cms-app/src/locales.js
@@ -0,0 +1,4 @@
+import { NetlifyCmsCore as CMS } from 'netlify-cms-core';
+import { en } from 'netlify-cms-locales';
+
+CMS.registerLocale('en', en);
diff --git a/packages/netlify-cms-core/src/bootstrap.js b/packages/netlify-cms-core/src/bootstrap.js
index 29ec8cf2..e43561e6 100644
--- a/packages/netlify-cms-core/src/bootstrap.js
+++ b/packages/netlify-cms-core/src/bootstrap.js
@@ -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 (
+
+
+
+
+
+
+
+ );
+};
+
+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 = () => (
<>
-
-
-
-
-
-
-
-
-
+
+
+
>
);
diff --git a/packages/netlify-cms-core/src/constants/configSchema.js b/packages/netlify-cms-core/src/constants/configSchema.js
index c4ad28ad..359b0ddf 100644
--- a/packages/netlify-cms-core/src/constants/configSchema.js
+++ b/packages/netlify-cms-core/src/constants/configSchema.js
@@ -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'] },
diff --git a/packages/netlify-cms-core/src/constants/defaultPhrases.js b/packages/netlify-cms-core/src/constants/defaultPhrases.js
deleted file mode 100644
index a1bfe0a1..00000000
--- a/packages/netlify-cms-core/src/constants/defaultPhrases.js
+++ /dev/null
@@ -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',
- },
- },
- };
-}
diff --git a/packages/netlify-cms-core/src/lib/__tests__/phrases.spec.js b/packages/netlify-cms-core/src/lib/__tests__/phrases.spec.js
new file mode 100644
index 00000000..2adcd3fb
--- /dev/null
+++ b/packages/netlify-cms-core/src/lib/__tests__/phrases.spec.js
@@ -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);
+ });
+});
diff --git a/packages/netlify-cms-core/src/lib/__tests__/registry.spec.js b/packages/netlify-cms-core/src/lib/__tests__/registry.spec.js
new file mode 100644
index 00000000..d09f41dc
--- /dev/null
+++ b/packages/netlify-cms-core/src/lib/__tests__/registry.spec.js
@@ -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);
+ });
+ });
+});
diff --git a/packages/netlify-cms-core/src/lib/phrases.js b/packages/netlify-cms-core/src/lib/phrases.js
new file mode 100644
index 00000000..2e98e5a2
--- /dev/null
+++ b/packages/netlify-cms-core/src/lib/phrases.js
@@ -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;
+}
diff --git a/packages/netlify-cms-core/src/lib/registry.js b/packages/netlify-cms-core/src/lib/registry.js
index c50c7c1a..dee15b78 100644
--- a/packages/netlify-cms-core/src/lib/registry.js
+++ b/packages/netlify-cms-core/src/lib/registry.js
@@ -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];
+}
diff --git a/packages/netlify-cms-core/src/selectors/config.js b/packages/netlify-cms-core/src/selectors/config.js
new file mode 100644
index 00000000..58752647
--- /dev/null
+++ b/packages/netlify-cms-core/src/selectors/config.js
@@ -0,0 +1 @@
+export const selectLocale = state => state.get('locale', 'en');
diff --git a/packages/netlify-cms-locales/README.md b/packages/netlify-cms-locales/README.md
new file mode 100644
index 00000000..14eba4ce
--- /dev/null
+++ b/packages/netlify-cms-locales/README.md
@@ -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.
diff --git a/packages/netlify-cms-locales/package.json b/packages/netlify-cms-locales/package.json
new file mode 100644
index 00000000..164e587e
--- /dev/null
+++ b/packages/netlify-cms-locales/package.json
@@ -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": {}
+}
diff --git a/packages/netlify-cms-locales/src/de/index.js b/packages/netlify-cms-locales/src/de/index.js
new file mode 100644
index 00000000..90b773c9
--- /dev/null
+++ b/packages/netlify-cms-locales/src/de/index.js
@@ -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;
diff --git a/packages/netlify-cms-locales/src/en/index.js b/packages/netlify-cms-locales/src/en/index.js
new file mode 100644
index 00000000..50d2943c
--- /dev/null
+++ b/packages/netlify-cms-locales/src/en/index.js
@@ -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;
diff --git a/packages/netlify-cms-locales/src/index.js b/packages/netlify-cms-locales/src/index.js
new file mode 100644
index 00000000..969b5787
--- /dev/null
+++ b/packages/netlify-cms-locales/src/index.js
@@ -0,0 +1,2 @@
+export { default as en } from './en';
+export { default as de } from './de';
diff --git a/packages/netlify-cms-locales/webpack.config.js b/packages/netlify-cms-locales/webpack.config.js
new file mode 100644
index 00000000..42edd361
--- /dev/null
+++ b/packages/netlify-cms-locales/webpack.config.js
@@ -0,0 +1,3 @@
+const { getConfig } = require('../../scripts/webpack.js');
+
+module.exports = getConfig();
diff --git a/packages/netlify-cms/src/index.js b/packages/netlify-cms/src/index.js
index 391a3a8f..5bb2504a 100644
--- a/packages/netlify-cms/src/index.js
+++ b/packages/netlify-cms/src/index.js
@@ -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.
diff --git a/packages/netlify-cms/src/locales.js b/packages/netlify-cms/src/locales.js
new file mode 100644
index 00000000..3f568e99
--- /dev/null
+++ b/packages/netlify-cms/src/locales.js
@@ -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]);
+});
diff --git a/website/content/docs/configuration-options.md b/website/content/docs/configuration-options.md
index dba94945..695c9a37 100644
--- a/website/content/docs/configuration-options.md
+++ b/website/content/docs/configuration-options.md
@@ -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`.