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`.