[Feature] i18n (#1106)

* i18n first steps

* i18n internal, toolbox, api for tools

* namespaced api

* tn, t

* tn in block tunes

* join toolbox and inlineTools under toolNames

* translations

* make enum toolTypes

* Update block.ts

* Update src/components/core.ts

Co-Authored-By: George Berezhnoy <gohabereg@users.noreply.github.com>

* add more types

* rm tn

* export i18n types

* upd bundle

* fix tabulation

* Add type-safe namespaces

* upd

* Improve example

* Update toolbox.ts

* improve examplle

* upd

* fix typo

* Add comments for complex types

Co-authored-by: George Berezhnoy <gohabereg@users.noreply.github.com>
Co-authored-by: Georgy Berezhnoy <gohabereg@gmail.com>
This commit is contained in:
Peter Savchenko 2020-04-20 21:25:33 +03:00 committed by GitHub
parent 4c0d806a12
commit 21cac86e42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1014 additions and 76 deletions

2
dist/editor.js vendored

File diff suppressed because one or more lines are too long

View file

@ -33,6 +33,15 @@
* @author CodeX-Team <https://ifmo.su>
*/
/**
* Base Paragraph Block for the Editor.js.
* Represents simple paragraph
*
* @author CodeX (team@codex.so)
* @copyright CodeX 2018
* @license The MIT License (MIT)
*/
/*!
* Codex JavaScript Notification module
* https://github.com/codex-team/js-notifier
@ -46,15 +55,6 @@
* @version 1.1.1
*/
/**
* Base Paragraph Block for the Editor.js.
* Represents simple paragraph
*
* @author CodeX (team@codex.so)
* @copyright CodeX 2018
* @license The MIT License (MIT)
*/
/*!
* CodeX.Tooltips
*

View file

@ -4,7 +4,8 @@
- `Improvements` - TSLint (deprecated) replaced with ESLint, old config changed to [CodeX ESLint Config](https://github.com/codex-team/eslint-config).
- `Improvements` - Fix many code-style issues, add missed annotations.
- `Improvements` - Adjusted GitHub action for ESLint.
- `Improvements` - Adjusted GitHub Action for ESLint.
- `New` *I18n API* — Ability to provide internalization for Editor.js core and tools. [#751](https://github.com/codex-team/editor.js/issues/751)
### 2.17

View file

@ -1,5 +1,12 @@
# Editor.js API
---
Most actual API described by [this interface](../types/api/index.d.ts).
---
📃 See official API documentation [https://editorjs.io/api](https://editorjs.io/api)
---
Blocks have access to the public methods provided by Editor.js API Module. Plugin and Tune Developers
can use Editor\`s API as they want.
@ -42,7 +49,7 @@ use 'move' instead)
`stretchBlock(index: number, status: boolean)` - make Block stretched
`insertNewBlock()` - __Deprecated__ insert new Block after working place
`insertNewBlock()` - __Deprecated__ insert new Block after working place
`insert(type?: string, data?: BlockToolData, config?: ToolConfig, index?: number, needToFocus?: boolean)` - insert new Block with passed parameters
@ -100,11 +107,11 @@ Each method accept `position` and `offset` parameters. `Offset` should be used t
`Position` can be one of the following values:
| Value | Description
| --------- | -----------
| Value | Description
| --------- | -----------
| `start` | Caret will be set at the Block's beginning
| `end` | Caret will be set at the Block end
| `default` | More or less emulates browser behaviour, in most cases behaves as `start`
| `default` | More or less emulates browser behaviour, in most cases behaves as `start`
Each method returns `boolean` value: true if caret is set successfully or false otherwise (e.g. when there is no Block at index);
@ -148,7 +155,7 @@ this.api.notifier.show({
![](https://capella.pics/14fcdbe4-d6eb-41d4-b66e-e0e86ccf1a4b.jpg)
Check out [`codex-notifier` package page](https://github.com/codex-team/js-notifier) on GitHub to find docs, params and examples.
Check out [`codex-notifier` package page](https://github.com/codex-team/js-notifier) on GitHub to find docs, params and examples.
### Destroy API
@ -173,10 +180,10 @@ Methods for showing Tooltip helper near your elements. Parameters are the same a
#### Show
Method shows tooltip with custom content on passed element
```js
this.api.tooltip.show(element, content, options);
```
```
| parameter | type | description |
| -- | -- | -- |
@ -184,17 +191,17 @@ this.api.tooltip.show(element, content, options);
| `content` | _String_ or _Node_ | Content that will be appended to the Tooltip |
| `options` | _Object_ | Some displaying options, see below |
Available showing options
Available showing options
| name | type | action |
| -- | -- | -- |
| placement | `top`, `bottom`, `left`, `right` | Where to place the tooltip. Default value is `bottom' |
| marginTop | _Number_ | Offset above the tooltip with `top` placement |
| marginBottom | _Number_ | Offset below the tooltip with `bottom` placement |
| marginLeft | _Number_ | Offset at left from the tooltip with `left` placement |
| marginRight | _Number_ | Offset at right from the tooltip with `right` placement |
| delay | _Number_ | Delay before showing, in ms. Default is `70` |
| hidingDelay | _Number_ | Delay before hiding, in ms. Default is `0` |
| placement | `top`, `bottom`, `left`, `right` | Where to place the tooltip. Default value is `bottom' |
| marginTop | _Number_ | Offset above the tooltip with `top` placement |
| marginBottom | _Number_ | Offset below the tooltip with `bottom` placement |
| marginLeft | _Number_ | Offset at left from the tooltip with `left` placement |
| marginRight | _Number_ | Offset at right from the tooltip with `right` placement |
| delay | _Number_ | Delay before showing, in ms. Default is `70` |
| hidingDelay | _Number_ | Delay before hiding, in ms. Default is `0` |
#### Hide
@ -206,7 +213,7 @@ this.api.tooltip.hide();
#### onHover
Decorator for showing tooltip near some element by "mouseenter" and hide by "mouseleave".
Decorator for showing tooltip near some element by "mouseenter" and hide by "mouseleave".
```js
this.api.tooltip.onHover(element, content, options);
@ -214,7 +221,7 @@ this.api.tooltip.onHover(element, content, options);
### API Shorthands
Editor`s API provides some shorthands for API methods.
Editor`s API provides some shorthands for API methods.
| Alias | Method |
| ------ | --------------- |

View file

@ -276,9 +276,10 @@
},
onChange: function() {
console.log('something changed');
}
},
});
/**
* Saving example
*/

381
example/example-i18n.html Normal file
View file

@ -0,0 +1,381 @@
<!--
This page contains example of editor.js internalization.
See <script> section -> i18n property of the configuration object
\ (•◡•) /
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Editor.js 🤩🧦🤨 example</title>
<link href="https://fonts.googleapis.com/css?family=PT+Mono" rel="stylesheet">
<link href="assets/demo.css" rel="stylesheet">
<script src="assets/json-preview.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head>
<body>
<div class="ce-example">
<div class="ce-example__header">
<a class="ce-example__header-logo" href="https://codex.so/editor">Editor.js 🤩🧦🤨</a>
<div class="ce-example__header-menu">
<a href="https://github.com/editor-js" target="_blank">Plugins</a>
<a href="https://editorjs.io/usage" target="_blank">Usage</a>
<a href="https://editorjs.io/configuration" target="_blank">Configuration</a>
<a href="https://editorjs.io/creating-a-block-tool" target="_blank">API</a>
</div>
</div>
<div class="ce-example__content _ce-example__content--small">
<div id="editorjs"></div>
<div class="ce-example__button" id="saveButton">
editor.save()
</div>
</div>
<div class="ce-example__output">
<pre class="ce-example__output-content" id="output"></pre>
<div class="ce-example__output-footer">
<a href="https://codex.so" style="font-weight: bold;">Made by CodeX</a>
</div>
</div>
</div>
<!-- Load Tools -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script><!-- Header -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/simple-image@latest"></script><!-- Image -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/delimiter@latest"></script><!-- Delimiter -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/list@latest"></script><!-- List -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/checklist@latest"></script><!-- Checklist -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@latest"></script><!-- Quote -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/code@latest"></script><!-- Code -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script><!-- Embed -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/table@latest"></script><!-- Table -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/link@latest"></script><!-- Link -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/warning@latest"></script><!-- Warning -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@latest"></script><!-- Marker -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/inline-code@latest"></script><!-- Inline Code -->
<!-- Load Editor.js's Core -->
<script src="../dist/editor.js"></script>
<!-- Initialization -->
<script>
/**
* Saving button
*/
const saveButton = document.getElementById('saveButton');
/**
* To initialize the Editor, create a new instance with configuration object
* @see docs/installation.md for mode details
*/
var editor = new EditorJS({
/**
* Wrapper of Editor
*/
holder: 'editorjs',
/**
* Tools list
*/
tools: {
/**
* Each Tool is a Plugin. Pass them via 'class' option with necessary settings {@link docs/tools.md}
*/
header: {
class: Header,
inlineToolbar: ['link'],
config: {
placeholder: 'Header'
},
shortcut: 'CMD+SHIFT+H'
},
/**
* Or pass class directly without any configuration
*/
image: SimpleImage,
list: {
class: List,
inlineToolbar: true,
shortcut: 'CMD+SHIFT+L'
},
checklist: {
class: Checklist,
inlineToolbar: true,
},
quote: {
class: Quote,
inlineToolbar: true,
config: {
quotePlaceholder: 'Enter a quote',
captionPlaceholder: 'Quote\'s author',
},
shortcut: 'CMD+SHIFT+O'
},
warning: Warning,
marker: {
class: Marker,
shortcut: 'CMD+SHIFT+M'
},
code: {
class: CodeTool,
shortcut: 'CMD+SHIFT+C'
},
delimiter: Delimiter,
inlineCode: {
class: InlineCode,
shortcut: 'CMD+SHIFT+C'
},
linkTool: LinkTool,
embed: Embed,
table: {
class: Table,
inlineToolbar: true,
shortcut: 'CMD+ALT+T'
},
},
/**
* To provide localization of the editor.js you need to provide 'i18n' option with 'messages' dictionary:
*
* 1. At the 'ui' section of 'messages' there are translations for the internal editor.js UI elements.
* You can create or find/download a dictionary for your language
*
* 2. As long as tools list is a user-specific thing (we do not know which tools you use and under which names),
* so we can't provide a ready-to-use tool names dictionary.
* There is a 'toolNames' section for that reason. Put translations for the names of your tools there.
*
* 3. Also, the UI of the tools you use is also invisible to editor.js core.
* To pass translations for specific tools (that supports I18n API), there are 'tools' and 'blockTunes' section.
* Pass dictionaries for specific plugins through them.
*/
i18n: {
/**
* @type {I18nDictionary}
*/
messages: {
/**
* Other below: translation of different UI components of the editor.js core
*/
"ui": {
"blockTunes": {
"toggler": {
"Click to tune": "Нажмите, чтобы настроить",
"or drag to move": "или перетащите"
},
},
"inlineToolbar": {
"converter": {
"Convert to": "Конвертировать в"
}
},
"toolbar": {
"toolbox": {
"Add": "Добавить"
}
}
},
/**
* Section for translation Tool Names: both block and inline tools
*/
"toolNames": {
"Text": "Параграф",
"Heading": "Заголовок",
"List": "Список",
"Warning": "Примечание",
"Checklist": "Чеклист",
"Quote": "Цитата",
"Code": "Код",
"Delimiter": "Разделитель",
"Raw HTML": "HTML-фрагмент",
"Table": "Таблица",
"Link": "Ссылка",
"Marker": "Маркер",
"Bold": "Полужирный",
"Italic": "Курсив",
"InlineCode": "Моноширинный",
},
/**
* Section for passing translations to the external tools classes
*/
"tools": {
/**
* Each subsection is the i18n dictionary that will be passed to the corresponded plugin
* The name of a plugin should be equal the name you specify in the 'tool' section for that plugin
*/
"warning": { // <-- 'Warning' tool will accept this dictionary section
"Title": "Название",
"Message": "Сообщение",
},
/**
* Link is the internal Inline Tool
*/
"link": {
"Add a link": "Вставьте ссылку"
},
/**
* The "stub" is an internal block tool, used to fit blocks that does not have the corresponded plugin
*/
"stub": {
'The block can not be displayed correctly.': 'Блок не может быть отображен'
}
},
/**
* Section allows to translate Block Tunes
*/
"blockTunes": {
/**
* Each subsection is the i18n dictionary that will be passed to the corresponded Block Tune plugin
* The name of a plugin should be equal the name you specify in the 'tunes' section for that plugin
*
* Also, there are few internal block tunes: "delete", "moveUp" and "moveDown"
*/
"delete": {
"Delete": "Удалить"
},
"moveUp": {
"Move up": "Переместить вверх"
},
"moveDown": {
"Move down": "Переместить вниз"
}
},
}
},
/**
* Initial Editor data
*/
data: {
blocks: [
{
type: "header",
data: {
text: "Editor.js",
level: 2
}
},
{
type : 'paragraph',
data : {
text : 'Hey. Meet the new Editor. On this page you can see it in action — try to edit this text. Source code of the page contains the example of connection and configuration.'
}
},
{
type: "header",
data: {
text: "Key features",
level: 3
}
},
{
type : 'list',
data : {
items : [
'It is a block-styled editor',
'It returns clean data output in JSON',
'Designed to be extendable and pluggable with a simple API',
],
style: 'unordered'
}
},
{
type: "header",
data: {
text: "What does it mean «block-styled editor»",
level: 3
}
},
{
type : 'paragraph',
data : {
text : 'Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js <mark class=\"cdx-marker\">workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc</mark>. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor\'s Core.'
}
},
{
type : 'paragraph',
data : {
text : `There are dozens of <a href="https://github.com/editor-js">ready-to-use Blocks</a> and the <a href="https://editorjs.io/creating-a-block-tool">simple API</a> for creation any Block you need. For example, you can implement Blocks for Tweets, Instagram posts, surveys and polls, CTA-buttons and even games.`
}
},
{
type: "header",
data: {
text: "What does it mean clean data output",
level: 3
}
},
{
type : 'paragraph',
data : {
text : 'Classic WYSIWYG-editors produce raw HTML-markup with both content data and content appearance. On the contrary, Editor.js outputs JSON object with data of each Block. You can see an example below'
}
},
{
type : 'paragraph',
data : {
text : `Given data can be used as you want: render with HTML for <code class="inline-code">Web clients</code>, render natively for <code class="inline-code">mobile apps</code>, create markup for <code class="inline-code">Facebook Instant Articles</code> or <code class="inline-code">Google AMP</code>, generate an <code class="inline-code">audio version</code> and so on.`
}
},
{
type : 'paragraph',
data : {
text : 'Clean data is useful to sanitize, validate and process on the backend.'
}
},
{
type : 'delimiter',
data : {}
},
{
type : 'paragraph',
data : {
text : 'We have been working on this project more than three years. Several large media projects help us to test and debug the Editor, to make its core more stable. At the same time we significantly improved the API. Now, it can be used to create any plugin for any task. Hope you enjoy. 😏'
}
},
{
type: 'image',
data: {
url: 'assets/codex2x.png',
caption: '',
stretched: false,
withBorder: true,
withBackground: false,
}
},
]
},
onReady: function(){
saveButton.click();
},
});
/**
* Saving example
*/
saveButton.addEventListener('click', function () {
editor.save().then((savedData) => {
cPreview.show(savedData, document.getElementById("output"));
});
});
</script>
</body>
</html>

View file

@ -70,7 +70,7 @@ export default class DeleteTune implements BlockTune {
/**
* Enable tooltip module
*/
this.api.tooltip.onHover(this.nodes.button, 'Delete');
this.api.tooltip.onHover(this.nodes.button, this.api.i18n.t('Delete'));
return this.nodes.button;
}

View file

@ -58,7 +58,7 @@ export default class MoveDownTune implements BlockTune {
/**
* Enable tooltip module on button
*/
this.api.tooltip.onHover(moveDownButton, 'Move down');
this.api.tooltip.onHover(moveDownButton, this.api.i18n.t('Move down'));
return moveDownButton;
}

View file

@ -57,7 +57,7 @@ export default class MoveUpTune implements BlockTune {
/**
* Enable tooltip module on button
*/
this.api.tooltip.onHover(moveUpButton, 'Move up');
this.api.tooltip.onHover(moveUpButton, this.api.i18n.t('Move up'));
return moveUpButton;
}

View file

@ -1,5 +1,4 @@
import {
API,
BlockTool,
BlockToolConstructable,
BlockToolData,
@ -12,6 +11,13 @@ import {
import { SavedData } from '../types-internal/block-data';
import $ from './dom';
import * as _ from './utils';
import ApiModule from './../components/modules/api';
/** Import default tunes */
import MoveUpTune from './block-tunes/block-tune-move-up';
import DeleteTune from './block-tunes/block-tune-delete';
import MoveDownTune from './block-tunes/block-tune-move-down';
import SelectionUtils from './selection';
import { ToolType } from './modules/tools';
/**
* @class Block
@ -22,12 +28,6 @@ import * as _ from './utils';
*
*/
/** Import default tunes */
import MoveUpTune from './block-tunes/block-tune-move-up';
import DeleteTune from './block-tunes/block-tune-delete';
import MoveDownTune from './block-tunes/block-tune-move-down';
import SelectionUtils from './selection';
/**
* Available Block Tool API methods
*/
@ -106,9 +106,9 @@ export default class Block {
private cachedInputs: HTMLElement[] = [];
/**
* Editor`s API
* Editor`s API module
*/
private readonly api: API;
private readonly api: ApiModule;
/**
* Focused input index
@ -154,20 +154,20 @@ export default class Block {
* @param {object} toolInstance passed Tool`s instance that rendered the Block
* @param {object} toolClass Tool's class
* @param {object} settings - default settings
* @param {object} apiMethods - Editor API
* @param {ApiModule} apiModule - Editor API module for pass it to the Block Tunes
*/
constructor(
toolName: string,
toolInstance: BlockTool,
toolClass: BlockToolConstructable,
settings: ToolConfig,
apiMethods: API
apiModule: ApiModule
) {
this.name = toolName;
this.tool = toolInstance;
this.class = toolClass;
this.settings = settings;
this.api = apiMethods;
this.api = apiModule;
this.holder = this.compose();
this.mutationObserver = new MutationObserver(this.didMutated);
@ -519,12 +519,25 @@ export default class Block {
* @returns {BlockTune[]}
*/
public makeTunes(): BlockTune[] {
const tunesList = [MoveUpTune, DeleteTune, MoveDownTune];
const tunesList = [
{
name: 'moveUp',
Tune: MoveUpTune,
},
{
name: 'delete',
Tune: DeleteTune,
},
{
name: 'moveDown',
Tune: MoveDownTune,
},
];
// Pluck tunes list and return tune instances with passed Editor API and settings
return tunesList.map((Tune: BlockTuneConstructable) => {
return tunesList.map(({ name, Tune }: {name: string; Tune: BlockTuneConstructable}) => {
return new Tune({
api: this.api,
api: this.api.getMethodsForTool(name, ToolType.Tune),
settings: this.settings,
});
});

View file

@ -5,6 +5,7 @@ import * as _ from './utils';
import { LogLevels } from './utils';
import { EditorConfig, OutputData, SanitizerConfig } from '../../types';
import { EditorModules } from '../types-internal/editor-modules';
import I18n from './i18n';
/**
* @typedef {Core} Core - editor core class
@ -203,6 +204,13 @@ export default class Core {
this.config.data.blocks = [ initialBlockData ];
}
}
/**
* Adjust i18n
*/
if (config.i18n && config.i18n.messages) {
I18n.setDictionary(config.i18n.messages);
}
}
/**

View file

@ -0,0 +1,90 @@
import defaultDictionary from './locales/en/messages.json';
import * as _ from '../utils';
import { I18nDictionary, Dictionary } from '../../../types/configs';
import { LeavesDictKeys } from '../../types-internal/i18n-internal-namespace';
/**
* Type for all available internal dictionary strings
*/
type DictKeys = LeavesDictKeys<typeof defaultDictionary>;
/**
* This class will responsible for the translation through the language dictionary
*/
export default class I18n {
/**
* Property that stores messages dictionary
*/
private static currentDictionary: I18nDictionary = defaultDictionary;
/**
* Type-safe translation for internal UI texts:
* Perform translation of the string by namespace and a key
*
* @example I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune')
*
* @param internalNamespace - path to translated string in dictionary
* @param dictKey - dictionary key. Better to use default locale original text
*/
public static ui(internalNamespace: string, dictKey: DictKeys): string {
return I18n._t(internalNamespace, dictKey);
}
/**
* Translate for external strings that is not presented in default dictionary.
* For example, for user-specified tool names
*
* @param namespace - path to translated string in dictionary
* @param dictKey - dictionary key. Better to use default locale original text
*/
public static t(namespace: string, dictKey: string): string {
return I18n._t(namespace, dictKey);
}
/**
* Adjust module for using external dictionary
*
* @param dictionary - new messages list to override default
*/
public static setDictionary(dictionary: I18nDictionary): void {
I18n.currentDictionary = dictionary;
}
/**
* Perform translation both for internal and external namespaces
* If there is no translation found, returns passed key as a translated message
*
* @param namespace - path to translated string in dictionary
* @param dictKey - dictionary key. Better to use default locale original text
*/
private static _t(namespace: string, dictKey: string): string {
const section = I18n.getNamespace(namespace);
if (section === undefined) {
_.logLabeled('I18n: section %o was not found in current dictionary', 'log', namespace);
}
if (!section || !section[dictKey]) {
return dictKey;
}
return section[dictKey] as string;
}
/**
* Find messages section by namespace path
*
* @param namespace - path to section
*/
private static getNamespace(namespace: string): Dictionary {
const parts = namespace.split('.');
return parts.reduce((section, part) => {
if (!section || !Object.keys(section).length) {
return {};
}
return section[part];
}, I18n.currentDictionary);
}
}

View file

@ -0,0 +1,45 @@
{
"ui": {
"blockTunes": {
"toggler": {
"Click to tune": "",
"or drag to move": ""
}
},
"inlineToolbar": {
"converter": {
"Convert to": ""
}
},
"toolbar": {
"toolbox": {
"Add": ""
}
}
},
"toolNames": {
"Text": "",
"Link": "",
"Bold": "",
"Italic": ""
},
"tools": {
"link": {
"Add a link": ""
},
"stub": {
"The block can not be displayed correctly.": ""
}
},
"blockTunes": {
"delete": {
"Delete": ""
},
"moveUp": {
"Move up": ""
},
"moveDown": {
"Move down": ""
}
}
}

View file

@ -0,0 +1,52 @@
import defaultDictionary from './locales/en/messages.json';
import { DictNamespaces } from '../../types-internal/i18n-internal-namespace';
import { typeOf } from '../utils';
/**
* Evaluate messages dictionary and return object for namespace chaining
*
* @param dict - Messages dictionary
* @param [keyPath] - subsection path (used in recursive call)
*/
function getNamespaces(dict: object, keyPath?: string): DictNamespaces<typeof defaultDictionary> {
const result = {};
Object.entries(dict).forEach(([key, section]) => {
if (typeOf(section) === 'object') {
const newPath = keyPath ? `${keyPath}.${key}` : key;
/**
* Check current section values, if all of them are strings, so there is the last section
*/
const isLastSection = Object.values(section).every((sectionValue) => {
return typeOf(sectionValue) === 'string';
});
/**
* In last section, we substitute namespace path instead of object with translates
*
* ui.toolbar.toolbox "ui.toolbar.toolbox"
* instead of
* ui.toolbar.toolbox {"Add": ""}
*/
if (isLastSection) {
result[key] = newPath;
} else {
result[key] = getNamespaces(section, newPath);
}
return;
}
result[key] = section;
});
return result as DictNamespaces<typeof defaultDictionary>;
}
/**
* Type safe access to the internal messages dictionary sections
*
* @example I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune');
*/
export const I18nInternalNS = getNamespaces(defaultDictionary);

View file

@ -3,7 +3,7 @@ import SelectionUtils from '../selection';
import $ from '../dom';
import * as _ from '../utils';
import { API, InlineTool, SanitizerConfig } from '../../../types';
import { Notifier, Toolbar } from '../../../types/api';
import { Notifier, Toolbar, I18n } from '../../../types/api';
/**
* Link Tool
@ -100,6 +100,11 @@ export default class LinkInlineTool implements InlineTool {
*/
private notifier: Notifier;
/**
* I18n API
*/
private i18n: I18n;
/**
* @param {API} api - Editor.js API
*/
@ -107,6 +112,7 @@ export default class LinkInlineTool implements InlineTool {
this.toolbar = api.toolbar;
this.inlineToolbar = api.inlineToolbar;
this.notifier = api.notifier;
this.i18n = api.i18n;
this.selection = new SelectionUtils();
}
@ -128,7 +134,7 @@ export default class LinkInlineTool implements InlineTool {
*/
public renderActions(): HTMLElement {
this.nodes.input = document.createElement('input') as HTMLInputElement;
this.nodes.input.placeholder = 'Add a link';
this.nodes.input.placeholder = this.i18n.t('Add a link');
this.nodes.input.classList.add(this.CSS.input);
this.nodes.input.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.keyCode === this.ENTER_KEY) {

View file

@ -0,0 +1,55 @@
import Module from '../../__module';
import { I18n } from '../../../../types/api';
import I18nInternal from '../../i18n';
import { ToolType } from '../tools';
import { logLabeled } from '../../utils';
/**
* Provides methods for working with i18n
*/
export default class I18nAPI extends Module {
/**
* Return namespace section for tool or block tune
*
* @param toolName - name of tool. Used to provide dictionary only for this tool
* @param toolType - 'block' for Block Tool, 'inline' for Inline Tool, 'tune' for Block Tunes
*/
private static getNamespace(toolName: string, toolType: ToolType): string {
switch (toolType) {
case ToolType.Block:
case ToolType.Inline:
return `tools.${toolName}`;
case ToolType.Tune:
return `blockTunes.${toolName}`;
}
}
/**
* Return I18n API methods with global dictionary access
*/
public get methods(): I18n {
return {
t: (): string | undefined => {
logLabeled('I18n.t() method can be accessed only from Tools', 'warn');
return undefined;
},
};
}
/**
* Return I18n API methods with tool namespaced dictionary
*
* @param toolName - name of tool. Used to provide dictionary only for this tool
* @param toolType - 'block' for Block Tool, 'inline' for Inline Tool, 'tune' for Block Tunes
*/
public getMethodsForTool(toolName: string, toolType: ToolType): I18n {
return Object.assign(
this.methods,
{
t: (dictKey: string): string => {
return I18nInternal.t(I18nAPI.getNamespace(toolName, toolType), dictKey);
},
});
}
}

View file

@ -1,12 +1,13 @@
/**
* @module API
* @copyright <CodeX Team> 2018
* @copyright <CodeX> 2018
*
* Each block has an Editor API instance to use provided public methods
* if you cant to read more about how API works, please see docs
*/
import Module from '../../__module';
import { API as APIInterfaces } from '../../../../types';
import { ToolType } from '../tools';
/**
* @class API
@ -29,6 +30,24 @@ export default class API extends Module {
toolbar: this.Editor.ToolbarAPI.methods,
inlineToolbar: this.Editor.InlineToolbarAPI.methods,
tooltip: this.Editor.TooltipAPI.methods,
i18n: this.Editor.I18nAPI.methods,
} as APIInterfaces;
}
/**
* Returns Editor.js Core API methods for passed tool
*
* @param toolName - how user name tool. It can be used in some API logic,
* for example in i18n to provide namespaced dictionary
*
* @param toolType - 'block' for Block Tool, 'inline' for Inline Tool, 'tune' for Block Tunes
*/
public getMethodsForTool(toolName: string, toolType = ToolType.Block): APIInterfaces {
return Object.assign(
this.methods,
{
i18n: this.Editor.I18nAPI.getMethodsForTool(toolName, toolType),
}
) as APIInterfaces;
}
}

View file

@ -213,7 +213,7 @@ export default class BlockManager extends Module {
public composeBlock(toolName: string, data: BlockToolData = {}, settings: ToolConfig = {}): Block {
const toolInstance = this.Editor.Tools.construct(toolName, data) as BlockTool;
const toolClass = this.Editor.Tools.available[toolName] as BlockToolConstructable;
const block = new Block(toolName, toolInstance, toolClass, settings, this.Editor.API.methods);
const block = new Block(toolName, toolInstance, toolClass, settings, this.Editor.API);
this.bindEvents(block);

View file

@ -273,7 +273,7 @@ export default class Paste extends Module {
private processTool = ([name, tool]: [string, BlockToolConstructable]): void => {
try {
const toolInstance = new this.Editor.Tools.blockTools[name]({
api: this.Editor.API.methods,
api: this.Editor.API.getMethodsForTool(name),
config: {},
data: {},
}) as BlockTool;

View file

@ -4,6 +4,8 @@ import { BlockToolConstructable } from '../../../../types';
import * as _ from '../../utils';
import { SavedData } from '../../../types-internal/block-data';
import Flipper from '../../flipper';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/**
* Block Converter
@ -67,7 +69,7 @@ export default class ConversionToolbar extends Module {
this.nodes.tools = $.make('div', ConversionToolbar.CSS.conversionToolbarTools);
const label = $.make('div', ConversionToolbar.CSS.conversionToolbarLabel, {
textContent: 'Convert to',
textContent: I18n.ui(I18nInternalNS.ui.inlineToolbar.converter, 'Convert to'),
});
/**
@ -292,7 +294,7 @@ export default class ConversionToolbar extends Module {
icon.innerHTML = toolIcon;
$.append(tool, icon);
$.append(tool, $.text(title || _.capitalize(toolName)));
$.append(tool, $.text(I18n.t(I18nInternalNS.toolNames, title || _.capitalize(toolName))));
$.append(this.nodes.tools, tool);
this.tools[toolName] = tool;

View file

@ -1,6 +1,8 @@
import Module from '../../__module';
import $ from '../../dom';
import * as _ from '../../utils';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/**
*
@ -131,7 +133,7 @@ export default class Toolbar extends Module {
*/
const tooltipContent = $.make('div');
tooltipContent.appendChild(document.createTextNode('Add'));
tooltipContent.appendChild(document.createTextNode(I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Add')));
tooltipContent.appendChild($.make('div', this.CSS.plusButtonShortcut, {
textContent: '⇥ Tab',
}));
@ -157,9 +159,13 @@ export default class Toolbar extends Module {
$.append(this.nodes.blockActionsButtons, this.nodes.settingsToggler);
$.append(this.nodes.actions, this.nodes.blockActionsButtons);
this.Editor.Tooltip.onHover(this.nodes.settingsToggler, 'Click to tune', {
placement: 'top',
});
this.Editor.Tooltip.onHover(
this.nodes.settingsToggler,
I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune'),
{
placement: 'top',
}
);
/**
* Make and append Settings Panel

View file

@ -1,10 +1,11 @@
import Module from '../../__module';
import $ from '../../dom';
import SelectionUtils from '../../selection';
import * as _ from '../../utils';
import { InlineTool, InlineToolConstructable, ToolConstructable, ToolSettings } from '../../../../types';
import Flipper from '../../flipper';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/**
* Inline toolbar with actions that modifies selected text fragment
@ -453,7 +454,7 @@ export default class InlineToolbar extends Module {
});
});
this.Editor.Tooltip.onHover(this.nodes.conversionToggler, 'Convert to', {
this.Editor.Tooltip.onHover(this.nodes.conversionToggler, I18n.ui(I18nInternalNS.ui.inlineToolbar.converter, 'Convert to'), {
placement: 'top',
hidingDelay: 100,
});
@ -588,7 +589,10 @@ export default class InlineToolbar extends Module {
* Enable tooltip module on button
*/
const tooltipContent = $.make('div');
const toolTitle = Tools.toolsClasses[toolName][Tools.INTERNAL_SETTINGS.TITLE] || _.capitalize(toolName);
const toolTitle = I18n.t(
I18nInternalNS.toolNames,
Tools.toolsClasses[toolName][Tools.INTERNAL_SETTINGS.TITLE] || _.capitalize(toolName)
);
tooltipContent.appendChild($.text(toolTitle));
@ -674,7 +678,7 @@ export default class InlineToolbar extends Module {
if (Object.prototype.hasOwnProperty.call(this.Editor.Tools.inline, tool)) {
const toolSettings = this.Editor.Tools.getToolSettings(tool);
result[tool] = this.Editor.Tools.constructInline(this.Editor.Tools.inline[tool], toolSettings);
result[tool] = this.Editor.Tools.constructInline(this.Editor.Tools.inline[tool], tool, toolSettings);
}
}

View file

@ -4,6 +4,8 @@ import * as _ from '../../utils';
import { BlockToolConstructable } from '../../../../types';
import Flipper from '../../flipper';
import { BlockToolAPI } from '../../block';
import I18n from '../../i18n';
import { I18nInternalNS } from '../../i18n/namespace-internal';
/**
* @class Toolbox
@ -233,7 +235,7 @@ export default class Toolbox extends Module {
const toolSettings = this.Editor.Tools.getToolSettings(toolName);
const toolboxSettings = this.Editor.Tools.available[toolName][this.Editor.Tools.INTERNAL_SETTINGS.TOOLBOX] || {};
const userToolboxSettings = toolSettings.toolbox || {};
const name = userToolboxSettings.title || toolboxSettings.title || toolName;
const name = I18n.t(I18nInternalNS.toolNames, userToolboxSettings.title || toolboxSettings.title || toolName);
let shortcut = toolSettings[this.Editor.Tools.USER_SETTINGS.SHORTCUT];

View file

@ -77,7 +77,7 @@ export default class Tools extends Module {
* Some Tools validation
*/
const inlineToolRequiredMethods = ['render', 'surround', 'checkState'];
const notImplementedMethods = inlineToolRequiredMethods.filter((method) => !this.constructInline(tool)[method]);
const notImplementedMethods = inlineToolRequiredMethods.filter((method) => !this.constructInline(tool, name)[method]);
if (notImplementedMethods.length) {
_.log(
@ -347,7 +347,7 @@ export default class Tools extends Module {
}
const constructorOptions = {
api: this.Editor.API.methods,
api: this.Editor.API.getMethodsForTool(tool),
config,
data,
};
@ -359,13 +359,14 @@ export default class Tools extends Module {
* Return Inline Tool's instance
*
* @param {InlineTool} tool - Inline Tool instance
* @param {string} name - tool name
* @param {ToolSettings} toolSettings - tool settings
*
* @returns {InlineTool} instance
*/
public constructInline(tool: InlineToolConstructable, toolSettings: ToolSettings = {} as ToolSettings): InlineTool {
public constructInline(tool: InlineToolConstructable, name: string, toolSettings: ToolSettings = {} as ToolSettings): InlineTool {
const constructorOptions = {
api: this.Editor.API.methods,
api: this.Editor.API.getMethodsForTool(name),
config: (toolSettings[this.USER_SETTINGS.CONFIG] || {}) as ToolSettings,
};
@ -474,3 +475,22 @@ export default class Tools extends Module {
}
}
}
/**
* What kind of plugins developers can create
*/
export enum ToolType {
/**
* Block tool
*/
Block,
/**
* Inline tool
*/
Inline,
/**
* Block tune
*/
Tune,
}

View file

@ -1,5 +1,5 @@
import $ from '../../dom';
import { BlockTool, BlockToolData } from '../../../../types';
import { API, BlockTool, BlockToolData } from '../../../../types';
export interface StubData extends BlockToolData{
title: string;
@ -7,7 +7,8 @@ export interface StubData extends BlockToolData{
}
/**
*
* This tool will be shown in place of a block without corresponding plugin
* It will store its data inside and pass it back with article saving
*/
export default class Stub implements BlockTool {
/**
@ -27,6 +28,11 @@ export default class Stub implements BlockTool {
*/
private readonly wrapper: HTMLElement;
/**
* Editor.js API
*/
private readonly api: API;
/**
* Stub title tool name
*/
@ -43,11 +49,13 @@ export default class Stub implements BlockTool {
private readonly savedData: BlockToolData;
/**
* @param {BlockToolData} data - stub tool data
* @param data - stub tool data
* @param api - Editor.js API
*/
constructor({ data }: {data: StubData}) {
this.title = data.title || 'Error';
this.subtitle = 'The block can not be displayed correctly.';
constructor({ data, api }: {data: StubData; api: API}) {
this.api = api;
this.title = data.title || this.api.i18n.t('Error');
this.subtitle = this.api.i18n.t('The block can not be displayed correctly.');
this.savedData = data.savedData;
this.wrapper = this.make();

View file

@ -35,6 +35,7 @@ import InlineToolbarAPI from '../components/modules/api/inlineToolbar';
import CrossBlockSelection from '../components/modules/crossBlockSelection';
import ConversionToolbar from '../components/modules/toolbar/conversion';
import TooltipAPI from '../components/modules/api/tooltip';
import I18nAPI from '../components/modules/api/i18n';
export interface EditorModules {
UI: UI;
@ -74,4 +75,5 @@ export interface EditorModules {
CrossBlockSelection: CrossBlockSelection;
NotifierAPI: NotifierAPI;
TooltipAPI: TooltipAPI;
I18nAPI: I18nAPI;
}

View file

@ -0,0 +1,68 @@
/**
* Decorator above the type object
*/
type Indexed<T> = { [key: string]: T };
/**
* Type for I18n dictionary values that can be strings or dictionary sub-sections
*
* Can be used as:
* LeavesDictKeys<typeof myDictionary>
*
* where myDictionary is a JSON with messages
*/
export type LeavesDictKeys<D> = D extends string
/**
* If generic type is string, just return it
*/
? D
/**
* If generic type is object that has only one level and contains only strings, return it's keys union
*
* { key: "string", anotherKey: "string" } => "key" | "anotherKey"
*
*/
: D extends Indexed<string>
? keyof D
/**
* If generic type is object, but not the one described above,
* use LeavesDictKey on it's values recursively and union the results
*
* { "rootKey": { "subKey": "string" }, "anotherRootKey": { "anotherSubKey": "string" } } => "subKey" | "anotherSubKey"
*
*/
: D extends Indexed<any>
? { [K in keyof D]: LeavesDictKeys<D[K]> }[keyof D]
/**
* In other cases, return never type
*/
: never;
/**
* Provide type-safe access to the available namespaces of the dictionary
*
* Can be uses as:
* DictNamespaces<typeof myDictionary>
*
* where myDictionary is a JSON with messages
*/
export type DictNamespaces<D extends object> = {
/**
* Iterate through generic type keys
*
* If value under current key is object that has only one level and contains only strings, return string type
*/
[K in keyof D]: D[K] extends Indexed<string>
? string
/**
* If value under current key is object with depth more than one, apply DictNamespaces recursively
*/
: D[K] extends Indexed<any>
? DictNamespaces<D[K]>
/**
* In other cases, return never type
*/
: never;
}

View file

@ -4,6 +4,12 @@
"target": "es2017",
"declaration": false,
"moduleResolution": "node", // This resolution strategy attempts to mimic the Node.js module resolution mechanism at runtime
"lib": ["dom", "es2017", "es2018"]
"lib": ["dom", "es2017", "es2018"],
// allows to import .json files for i18n
"resolveJsonModule": true,
// allows to omit export default in .json files
"allowSyntheticDefaultImports": true
}
}

11
types/api/i18n.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
/**
* Describes Editor`s I18n API
*/
export interface I18n {
/**
* Perform translation with automatically added namespace like `tools.${toolName}` or `blockTunes.${tuneName}`
*
* @param dictKey - what to translate
*/
t(dictKey: string): string;
}

View file

@ -10,3 +10,4 @@ export * from './toolbar';
export * from './notifier';
export * from './tooltip';
export * from './inline-toolbar';
export * from './i18n';

View file

@ -1,6 +1,7 @@
import {ToolConstructable, ToolSettings} from '../tools';
import {LogLevels, OutputData, API} from '../index';
import {SanitizerConfig} from './sanitizer-config';
import {I18nConfig} from './i18n-config';
export interface EditorConfig {
/**
@ -62,6 +63,11 @@ export interface EditorConfig {
*/
logLevel?: LogLevels;
/**
* Internalization config
*/
i18n?: I18nConfig;
/**
* Fires when Editor is ready to work
*/

11
types/configs/i18n-config.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
/**
* Available options of i18n config property
*/
import { I18nDictionary } from './i18n-dictionary';
export interface I18nConfig {
/**
* Dictionary used for translation
*/
messages: I18nDictionary;
}

93
types/configs/i18n-dictionary.d.ts vendored Normal file
View file

@ -0,0 +1,93 @@
/**
* Structure of the i18n dictionary
*/
export interface I18nDictionary {
/**
* Section for translation Tool Names: both block and inline tools
* Example:
* "toolNames": {
* "Text": "Параграф",
* "Heading": "Заголовок",
* "List": "Список",
* ...
* },
*/
toolNames?: Dictionary;
/**
* Section for passing translations to the external tools classes
* The first-level keys of this object should be equal of keys ot the 'tools' property of EditorConfig
* Includes internal tools: "paragraph", "stub"
*
* Example:
* "tools": {
* "warning": {
* "Title": "Название",
* "Message": "Сообщение",
* },
* "link": {
* "Add a link": "Вставьте ссылку"
* },
* },
*/
tools?: Dictionary;
/**
* Section allows to translate Block Tunes
* The first-level keys of this object should be equal of 'name' ot the 'tools.<toolName>.tunes' property of EditorConfig
* Including some internal block-tunes: "delete", "moveUp", "moveDown
*
* Example:
* "blockTunes": {
* "delete": {
* "Delete": "Удалить"
* },
* "moveUp": {
* "Move up": "Переместить вверх"
* },
* "moveDown": {
* "Move down": "Переместить вниз"
* }
* },
*/
blockTunes?: Dictionary;
/**
* Translation of internal UI components of the editor.js core
*/
ui?: Dictionary;
}
/**
* Represent item of the I18nDictionary config
*/
export interface Dictionary {
/**
* The keys of the object can represent two entities:
* 1. Dictionary key usually is an original string from default locale, like "Convert to"
* 2. Sub-namespace section, like "toolbar.converter.<...>"
*
* Example of 1:
* toolbox: {
* "Add": "Добавить",
* }
*
* Example of 2:
* ui: {
* toolbar: {
* toolbox: { <-- Example of 1
* "Add": "Добавить"
* }
* }
* }
*/
[key: string]: DictValue;
}
/**
* The value of the dictionary can be:
* - other dictionary
* - result translate string
*/
export type DictValue = {[key: string]: Dictionary | string} | string;

View file

@ -3,3 +3,5 @@ export * from './sanitizer-config';
export * from './paste-config';
export * from './conversion-config';
export * from './log-levels';
export * from './i18n-config';
export * from './i18n-dictionary';

22
types/index.d.ts vendored
View file

@ -4,7 +4,13 @@
* ------------------------------------
*/
import {EditorConfig} from './configs';
import {
EditorConfig,
I18nDictionary,
Dictionary,
DictValue,
I18nConfig,
} from './configs';
import {
Blocks,
Caret,
@ -18,6 +24,7 @@ import {
Styles,
Toolbar,
Tooltip,
I18n,
} from './api';
import {OutputData} from './data-formats/output-data';
@ -47,7 +54,17 @@ export {
FilePasteEventDetail,
} from './tools';
export {BlockTune, BlockTuneConstructable} from './block-tunes';
export {EditorConfig, SanitizerConfig, PasteConfig, LogLevels, ConversionConfig} from './configs';
export {
EditorConfig,
SanitizerConfig,
PasteConfig,
LogLevels,
ConversionConfig,
I18nDictionary,
Dictionary,
DictValue,
I18nConfig,
} from './configs';
export {OutputData} from './data-formats/output-data';
/**
@ -67,6 +84,7 @@ export interface API {
toolbar: Toolbar;
inlineToolbar: InlineToolbar;
tooltip: Tooltip;
i18n: I18n;
}
/**