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:
parent
4833f33728
commit
096b067d45
@ -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/'),
|
||||
|
@ -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:
|
||||
|
@ -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",
|
||||
|
@ -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') {
|
||||
/**
|
||||
|
4
packages/netlify-cms-app/src/locales.js
Normal file
4
packages/netlify-cms-app/src/locales.js
Normal file
@ -0,0 +1,4 @@
|
||||
import { NetlifyCmsCore as CMS } from 'netlify-cms-core';
|
||||
import { en } from 'netlify-cms-locales';
|
||||
|
||||
CMS.registerLocale('en', en);
|
35
packages/netlify-cms-core/src/bootstrap.js
vendored
35
packages/netlify-cms-core/src/bootstrap.js
vendored
@ -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>
|
||||
</Provider>
|
||||
</ErrorBoundary>
|
||||
</I18n>
|
||||
<Provider store={store}>
|
||||
<ConnectedTranslatedApp />
|
||||
</Provider>
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -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'] },
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
119
packages/netlify-cms-core/src/lib/__tests__/phrases.spec.js
Normal file
119
packages/netlify-cms-core/src/lib/__tests__/phrases.spec.js
Normal 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);
|
||||
});
|
||||
});
|
42
packages/netlify-cms-core/src/lib/__tests__/registry.spec.js
Normal file
42
packages/netlify-cms-core/src/lib/__tests__/registry.spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
7
packages/netlify-cms-core/src/lib/phrases.js
Normal file
7
packages/netlify-cms-core/src/lib/phrases.js
Normal 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;
|
||||
}
|
@ -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];
|
||||
}
|
||||
|
1
packages/netlify-cms-core/src/selectors/config.js
Normal file
1
packages/netlify-cms-core/src/selectors/config.js
Normal file
@ -0,0 +1 @@
|
||||
export const selectLocale = state => state.get('locale', 'en');
|
20
packages/netlify-cms-locales/README.md
Normal file
20
packages/netlify-cms-locales/README.md
Normal 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.
|
21
packages/netlify-cms-locales/package.json
Normal file
21
packages/netlify-cms-locales/package.json
Normal 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": {}
|
||||
}
|
180
packages/netlify-cms-locales/src/de/index.js
Normal file
180
packages/netlify-cms-locales/src/de/index.js
Normal 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;
|
176
packages/netlify-cms-locales/src/en/index.js
Normal file
176
packages/netlify-cms-locales/src/en/index.js
Normal 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;
|
2
packages/netlify-cms-locales/src/index.js
Normal file
2
packages/netlify-cms-locales/src/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as en } from './en';
|
||||
export { default as de } from './de';
|
3
packages/netlify-cms-locales/webpack.config.js
Normal file
3
packages/netlify-cms-locales/webpack.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
const { getConfig } = require('../../scripts/webpack.js');
|
||||
|
||||
module.exports = getConfig();
|
@ -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.
|
||||
|
6
packages/netlify-cms/src/locales.js
Normal file
6
packages/netlify-cms/src/locales.js
Normal 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]);
|
||||
});
|
@ -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`.
|
||||
|
Loading…
x
Reference in New Issue
Block a user