Przeglądaj źródła

Merge branch 'master' into i/6121-table-paste-to-one-cell

Kuba Niegowski 5 lat temu
rodzic
commit
8e3543a451

+ 1 - 1
docs/builds/guides/support/reporting-issues.md

@@ -22,7 +22,7 @@ Most of the issues are reproducible on the demo. If you have a different case th
 
 ## Reporting issues
 
-All issues should be reported in the [CKEditor 5 repository](https://github.com/ckeditor/ckeditor5/issues) on GitHub, except ones that are related to one of the following projects (which have their own issue pages):
+All issues should be reported in the [CKEditor 5 repository](https://github.com/ckeditor/ckeditor5/issues) on GitHub, except ones that are related to one of the following projects (that have their own issue pages):
 
 * [Angular integration](https://github.com/ckeditor/ckeditor5-angular),
 * [React integration](https://github.com/ckeditor/ckeditor5-react),

+ 7 - 5
docs/features/ui-language.md

@@ -7,7 +7,7 @@ menu-title: UI language
 
 # Setting the UI language
 
-The UI of the editor can be localized. CKEditor 5 currently supports around 50 languages and the number is growing.
+The UI of the editor can be localized. CKEditor 5 currently supports around 90 languages and the number is growing.
 
 <info-box>
 	If you want to help translate CKEditor 5 into your native language, join the [CKEditor 5 project on Transifex](https://www.transifex.com/ckeditor/ckeditor5/). Your help will be much appreciated!
@@ -17,6 +17,10 @@ See the demo of the editor in Spanish:
 
 {@snippet features/ui-language}
 
+<info-box>
+	If you are interested in creating features that can be localized, check out the {@link framework/guides/deep-dive/localization localization guide}.
+</info-box>
+
 ## Right–to–left (RTL) languages support
 
 CKEditor 5 supports right–to–left languages out–of–the–box. When one of <abbr title="right–to–left">RTL</abbr> languages is used, the WYSIWYG editor adapts its UI for the best editing experience, for instance, mirroring various elements like toolbars, dropdowns, buttons, etc.
@@ -102,7 +106,7 @@ If you build CKEditor 5 from scratch or integrate it directly into your applicat
 
 1. Install the [`@ckeditor/ckeditor5-dev-webpack-plugin`](https://www.npmjs.com/package/@ckeditor/ckeditor5-dev-webpack-plugin) package:
 
-	```bash
+	```
 	npm install --save @ckeditor/ckeditor5-dev-webpack-plugin
 	```
 
@@ -142,7 +146,7 @@ If you build CKEditor 5 from scratch or integrate it directly into your applicat
 	// ...
 	```
 
-3. Run webpack. If the `additionalLanguages` option is not set, the CKEditor 5 plugin for webpack will replace the {@link module:utils/locale~Locale#t `t()`} function call parameters used in the source code with localized language strings. Otherwise the CKEditor 5 plugin for webpack will replace the {@link module:utils/locale~Locale#t `t()`} function call parameters with short IDs and emit the translation files that should land in the `'translations'` directory (or different, if the `outputDirectory` option is specified).
+3. Run webpack. The CKEditor 5 plugin for webpack will emit additional files for each language specified in the `additionalLanguages` option. They will contain translations for messages from the {@link module:utils/locale~Locale#t `t()` function} calls. The files will be created in the `translations` directory (or another one if the `outputDirectory` option is specified). Translations from the language specified in the `language` option will be automatically included in the build.
 
 4. If you want to change the language after the build ends, you will need to edit the `index.html` file, add the translation file, and set the UI language to the target one.
 
@@ -165,8 +169,6 @@ If you build CKEditor 5 from scratch or integrate it directly into your applicat
 
 <info-box>
 	We are aware that the current localization method is not sufficient for some needs. It does not support different bundlers (e.g. Rollup or Browserify). We will be extending the localization possibilities in the future.
-
-	You can read more about the used techniques in the [Implement translation services](https://github.com/ckeditor/ckeditor5/issues/387) and [Implement translation services v2](https://github.com/ckeditor/ckeditor5/issues/624) issues.
 </info-box>
 
 ## Setting the language of the content

+ 4 - 0
docs/framework/guides/architecture/ui-library.md

@@ -11,6 +11,10 @@ The standard UI library of CKEditor 5 is [`@ckeditor/ckeditor5-ui`](https://www.
 
 Views use [templates](#templates) to build the UI. They also provide observable interfaces that other features (e.g. {@link framework/guides/architecture/core-editor-architecture#plugins plugins}, {@link framework/guides/architecture/core-editor-architecture#commands commands}, etc.) can use to change the DOM without any actual interaction with the native API.
 
+<info-box>
+	All views can be localized using the `locale` instance with which they were created. Check the {@link framework/guides/deep-dive/localization localization guide} to see how to use the `t()` function available in the `locale` instance.
+</info-box>
+
 ### Definition
 
 A simple input view class can be defined as follows:

+ 2 - 2
docs/framework/guides/contributing/contributing.md

@@ -42,12 +42,12 @@ Read more about our {@link framework/guides/contributing/testing-environment tes
 </info-box>
 
 <info-box>
-	[Angular](https://github.com/ckeditor/ckeditor5-angular), [React](https://github.com/ckeditor/ckeditor5-react) and [Vue](https://github.com/ckeditor/ckeditor5-vue) integrations as well as [CKEditor 5 inspector](https://github.com/ckeditor/ckeditor5-inspector) and [dev packages](https://github.com/ckeditor/ckeditor5-dev) are kept in separate repositories. The below steps assume that you want to propose a change in the [main CKEditor 5 repository](https://github.com/ckeditor/ckeditor5).
+	The [Angular](https://github.com/ckeditor/ckeditor5-angular), [React](https://github.com/ckeditor/ckeditor5-react) and [Vue](https://github.com/ckeditor/ckeditor5-vue) integrations as well as [CKEditor 5 inspector](https://github.com/ckeditor/ckeditor5-inspector) and [development tools packages](https://github.com/ckeditor/ckeditor5-dev) are kept in separate repositories. The steps below assume that you want to propose a change in the [main CKEditor 5 repository](https://github.com/ckeditor/ckeditor5).
 </info-box>
 
 Assuming that you would like to propose some changes, these are the steps you should take to create a pull request:
 
-1. Make sure to open a ticket in https://github.com/ckeditor/ckeditor5 describing the issue/feature/problem that you want to solve in your pull request. This can be skipped in case of obvious and trivial changes (typos, documentation, etc.).
+1. Make sure to open a ticket in https://github.com/ckeditor/ckeditor5 describing the issue, feature or problem that you want to solve in your pull request. This can be skipped in case of obvious and trivial changes (typos, documentation, etc.).
 1. Make sure your {@link framework/guides/contributing/development-environment development environment} is ready.
 1. Go to GitHub and [fork the repository](https://help.github.com/articles/fork-a-repo). The forked repository will appear in your GitHub account as `https://github.com/YOUR-USERNAME/ckeditor5`.
 1. Open your terminal, then go to the package ("repository") folder in your development environment:

+ 22 - 21
docs/framework/guides/contributing/development-environment.md

@@ -15,10 +15,10 @@ The CKEditor 5 codebase is divided into multiple [npm](http://npmjs.com/) packag
 
 The main package's GitHub repository also [hosts all other CKEditor5 sub-packages](https://github.com/ckeditor/ckeditor5/tree/master/packages).
 
-You can find all the official packages listed in [CKEditor 5 development repository's README](https://github.com/ckeditor/ckeditor5#packages).
+You can find all the official packages listed in the [CKEditor 5 repository's README](https://github.com/ckeditor/ckeditor5#packages).
 
 <info-box info>
-Prior to version **19.0.0** CKEditor 5 was developed in a multi repository architecture. If you'd like to work with the older multi-repository release, please refer to [older Developer Environment guide](/docs/ckeditor5/19.0.0/framework/guides/contributing/development-environment.html) for multi-repository oriented instructions.
+	Prior to version **19.0.0** CKEditor 5 was developed in a multi-repository architecture. If you would like to work with an older multi-repository release, please refer to the [older Development environment guide](/docs/ckeditor5/19.0.0/framework/guides/contributing/development-environment.html) for multi-repository oriented instructions.
 </info-box>
 
 ## Requirements
@@ -34,7 +34,7 @@ First, you need to install  [Yarn](https://yarnpkg.com/) that will be used for d
 
 It is best to install it globally in your system for an easier use later on:
 
-```bash
+```
 npm install -g yarn
 ```
 
@@ -42,28 +42,28 @@ npm install -g yarn
 
 Then clone the [CKEditor 5 repository](https://github.com/ckeditor/ckeditor5):
 
-```bash
+```
 git clone https://github.com/ckeditor/ckeditor5.git
 cd ckeditor5
 ```
 
 And install all CKEditor 5 packages from the [npm registry](http://npmjs.com/).
 
-```bash
+```
 yarn install
 ```
 
 ## Running tests
 
-In order to run tests you need to use the `test` and `manual` tasks.
+In order to run tests, you need to use the `test` and `manual` tasks.
 
-```bash
+```
 yarn run test --watch --coverage --source-map --files=engine
 ```
 
 or, shorter:
 
-```bash
+```
 yarn run test -- -wcs --files=engine
 ```
 
@@ -73,19 +73,19 @@ This command will run the [`ckeditor5-engine`](https://github.com/ckeditor/ckedi
 
 To create a server for manual tests use the `manual` task:
 
-```bash
+```
 yarn run manual
 ```
 
-To help test localized editors, the task accepts two optional configurations: `--language="en"` and `--additionalLanguages="ar,pl,..."`. The former sets the main language used by test editors. By default it is `"en"` and it in most scenarios you do not need to change it. The later brings more languages to manual tests, e.g. which is helpful when working with {@link features/ui-language#righttoleft-rtl-languages-support right–to–left languages in the user interface}.
+To help test localized editors, the task accepts two optional configurations: `--language="en"` and `--additionalLanguages="ar,pl,..."`. The former sets the main language used by test editors. By default it is `"en"` and in most scenarios you do not need to change it. The latter brings more languages to manual tests, which is helpful e.g. when working with {@link features/ui-language#righttoleft-rtl-languages-support right–to–left languages in the user interface}.
 
 You can read more about the {@link framework/guides/contributing/testing-environment Testing environment}.
 
 ## Generating documentation
 
-To build the documentation you need to run the `docs` task:
+To build the documentation, you need to run the `docs` task:
 
-```bash
+```
 yarn run docs
 ```
 
@@ -93,13 +93,13 @@ The documentation will be available in `build/docs/`.
 
 This task accepts the following arguments:
 
-* `--skip-api` &mdash; Skips building the API documentation (which takes the majority of the total time).
-* `--skip-snippets` &mdash; Skips building live snippets.
-* `--snippets=snippet-name` &mdash; Whitelist snippets to build (accepts glob patterns).
-* `--skip-validation` &mdash; Skips the final link validation.
-* `--watch` &mdash; Runs the documentation generator in a watch mode. It covers guides (it does not cover API docs).
-* `--production` &mdash; Minifies the assets and performs other actions which are unnecessary during CKEditor 5 development.
-* `--verbose` &mdash; Prints out more information.
+* `--skip-api` &ndash; Skips building the API documentation (which takes the majority of the total time).
+* `--skip-snippets` &ndash; Skips building live snippets.
+* `--snippets=snippet-name` &ndash; Whitelists snippets to build (accepts glob patterns).
+* `--skip-validation` &ndash; Skips the final link validation.
+* `--watch` &ndash; Runs the documentation generator in a watch mode. It covers guides but it does not cover API docs.
+* `--production` &ndash; Minifies the assets and performs other actions which are unnecessary during CKEditor 5 development.
+* `--verbose` &ndash; Prints out more information.
 
 Note: These arguments must be passed after additional `--`:
 
@@ -117,9 +117,10 @@ yarn run docs:serve
 
 It is possible to generate a stylesheet containing content styles brought by all CKEditor 5 features. In order to do that, execute:
 
-```bash
+```
 yarn docs:content-styles
 ```
+
 The stylesheet will be saved in the `build/content-styles` folder.
 
 To learn more, refer to the {@link builds/guides/integration/content-styles Content styles} guide.
@@ -132,7 +133,7 @@ By default, CKEditor 5 supports SVG icons found in the `ckeditor5-*/theme/icons`
 
 To remove the excess data and prevent [certain issues](https://github.com/ckeditor/ckeditor5-ui/issues/245), **all new icons should be optimized before joining the code base**. To do that, you can use the `clean-up-svg-icons` script in the [root of the project](#setting-up-the-ckeditor-development-environment), a wrapper for the [SVGO](https://github.com/svg/svgo) tool:
 
-```bash
+```
 cd path/to/ckeditor5
 
 # Optimize all SVG files in the folder.

+ 10 - 10
docs/framework/guides/contributing/git-commit-message-convention.md

@@ -5,7 +5,7 @@ order: 40
 
 # Git commit message convention
 
-Every commit made *directly* to the `master` branch must follow the below convention. Based on commits in the `master` branch CKEditor 5 release tools will generate changelog entries for the current release.
+Every commit made *directly* to the `master` branch must follow the convention below. Based on commits in the `master` branch CKEditor 5 release tools will generate changelog entries for the current release.
 
 <info-box>
 	Commits in the ticket branches are not analyzed for the changelog and do not have to follow any specific convention (other than finishing sentences with periods). In case of ticket branches, **only merge commits are analyzed**.
@@ -20,7 +20,7 @@ Commit message template:
 ```
 Type (package-name): A short sentence about the commit. Closes #XXX.
 
-Type (other-package-name): If the change affects more than one package, it's possible to put multiple entries at once. Closes #YYY.
+Type (another-package-name): If the change affects more than one package, it's possible to put multiple entries at once. Closes #YYY.
 
 Optional description.
 
@@ -41,7 +41,7 @@ BREAKING CHANGE: Another breaking change if needed. Closes #ZZZ.
 | Revert | `patch` | Revert of some commit. | Hidden |
 | Release | `patch` | A special type of commit used by the release tools. | Hidden |
 
-Each commit can contain additional notes which will be inserted into the changelog:
+Each commit can contain additional notes that will be inserted into the changelog:
 
 * `MAJOR BREAKING CHANGE` (alias: `BREAKING CHANGE`),
 * `MINOR BREAKING CHANGE`.
@@ -54,9 +54,9 @@ For reference on how to identify minor or major breaking change see the {@link f
 
 Most commits are related to one or more packages. Each affected package should be listed in parenthesis following the commit type. A package that was the most impacted by the change should be listed first.
 
-It is, however, possible to skip this part if many packages are affected. This is a typically indication that this is a generic change and having all the packages listed would reduce changelog readability.
+It is, however, possible to skip this part if many packages are affected. This usually indicates a generic change. In this case having all the packages listed would reduce the changelog readability.
 
-The package name is based on npm package name, however it has the `@ckeditor/ckeditor(5)-` prefix stripped.
+The package name is based on the npm package name, however, it has the `@ckeditor/ckeditor(5)-` prefix stripped.
 
 If your change is related to the main package only, use `ckeditor5` as the package name.
 
@@ -76,13 +76,13 @@ A generic bug fix for an existing feature that affects many packages (closes two
 Fix: The editor will be great again. Closes #3. Closes #4.
 ```
 
-Commit with updated documentation:
+A commit with updated documentation:
 
 ```
 Docs (link): Updated the README.
 ```
 
-Commit that provides or changes the tests:
+A commit that provides or changes the tests:
 
 ```
 Tests (widget): Introduced missing tests. Closes #5.
@@ -128,9 +128,9 @@ Changelog
 
 ## Handling pull requests
 
-When making a pull request its author may (it is recommended in the pull request template) propose a merge commit message.
+When creating a pull request, its author may (it is recommended in the pull request template) propose a merge commit message.
 
-The reviewer's duty is to validate the proposed message and apply necessary changes (the PR description can be edited).
+The reviewer's duty is to validate the proposed message and apply necessary changes. The PR description can be edited.
 
 Things like:
 
@@ -144,7 +144,7 @@ should be checked and added if missing.
 
 As a reviewer, remember that the message will end up in the changelog and must be understandable in a broad context of the entire editor. It is not for you &mdash; it is for other developers.
 
-When closing a PR remember to copy the source of the message to the textarea with the merge commit message:
+When closing a PR, remember to copy the source of the message to the textarea with the merge commit message:
 
 {@img assets/img/closing-a-pr.gif Screencast how to copy a source version of the suggested commit message when closing a PR.}
 

+ 3 - 3
docs/framework/guides/contributing/testing-environment.md

@@ -32,19 +32,19 @@ It accepts the following arguments that must be passed after the `--` option:
 
 Run all tests with the code coverage check of the [`ckeditor5-core`](https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-core/tests) package:
 
-```bash
+```
 yarn run test -c --files=core
 ```
 
 Run and watch the [engine's `view` namespace tests](https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-engine/tests/view) and all the tests in [`ckeditor5-typing`](https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-typing/tests):
 
-```bash
+```
 yarn run test -cw --files=engine/view,typing
 ```
 
 Run the `bold*.js` tests in the [`ckeditor5-basic-styles`](https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-basic-styles/tests) package:
 
-```bash
+```
 yarn run test -cw --files=basic-styles/bold*.js
 ```
 

+ 5 - 1
docs/framework/guides/creating-simple-plugin.md

@@ -19,6 +19,10 @@ The plugin that you will write will use a part of the {@link features/image imag
 	We recommend using the official {@link framework/guides/development-tools#ckeditor-5-inspector CKEditor 5 inspector} for development and debugging. It will give you tons of useful information about the state of the editor such as internal data structures, selection, commands, and many more.
 </info-box>
 
+<info-box hint>
+	For simplicity reasons this guide does not describe how to localize the created plugin. To see how to localize plugins, refer to the {@link framework/guides/deep-dive/localization localization guide}.
+</info-box>
+
 ## Step 1. Installing dependencies
 
 Start from installing the necessary dependencies:
@@ -27,7 +31,7 @@ Start from installing the necessary dependencies:
 * The [`@ckeditor/ckeditor5-core`](https://www.npmjs.com/package/@ckeditor/ckeditor5-core) package which contains the {@link module:core/plugin~Plugin} and {@link module:core/command~Command} classes.
 * The [`@ckeditor/ckeditor5-ui`](https://www.npmjs.com/package/@ckeditor/ckeditor5-ui) package which contains the UI library and framework.
 
-```bash
+```
 npm install --save @ckeditor/ckeditor5-image \
 	@ckeditor/ckeditor5-core \
 	@ckeditor/ckeditor5-ui

+ 242 - 0
docs/framework/guides/deep-dive/localization.md

@@ -0,0 +1,242 @@
+---
+category: framework-deep-dive-ui
+order: 70
+---
+
+# Localization
+
+## Introduction
+
+All CKEditor 5 WYSIWYG editor features support message localization. It means that the user interface of any feature can be translated into various languages and regions depending on the user's preferences.
+
+CKEditor 5 translation system is open to third-party plugins. Any custom features that you introduce can be localized. The system also provides a way to add missing or overwrite existing translations and supports translating plural forms.
+
+<info-box warning>
+	Make sure to use up-to-date CKEditor 5 development tool packages. Versions of the tools older than v19.0.1 do not provide support for features described in this guide.
+</info-box>
+
+### Open translation API
+
+The CKEditor 5 localization system focuses on the following points:
+
+- Supporting the localization of third-party plugins.
+- Making it possible to pass your own translations to fix missing or invalid localizations.
+- Generating deterministic builds.
+- Exposing easy-to-use APIs for providing translations and writing localizable content.
+- Supporting plural forms in each step of the localization system for better translations.
+
+### Glossary of terms
+
+Before we start, let us explain the meaning of terms that are crucial for the translation process:
+
+- *A message* &ndash; A string or an object that should be translated.
+	The string version works as a shortcut for the `{ id: message, string: message }` object form.
+- *A message ID* &ndash; A property used to distinguish messages.
+	It is useful for short messages where a collision might occur, like `%0 images`.
+- *A message string* &ndash; The default (English) form of the message.
+	When the message supports plural versions, this is the default singular version.
+- *A message plural* &ndash; An optional plural (English) version of the message.
+	The presence of this property indicates that the message should support both singular and plural forms.
+- *A PO file (`.po`)* &ndash; A file containing translations for the language in the [PO format](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html).
+	All localizable packages of CKEditor 5 contain such files in the `lang/translations/` directory.
+- *A POT file (`.pot`)* &ndash; A file containing source messages (English sentences) that will be translated.
+- *A translation asset* &ndash; A JavaScript file or a part of the file with generated translations for one language.
+
+## Writing a localizable UI
+
+All *messages* that need localization should be passed to the special CKEditor 5's {@link module:utils/locale~Locale#t `t()` function}. This function can be retrieved either from the editor's {@link module:utils/locale~Locale} instance: `const { t } = editor.locale;` or from any view method: `const t = this.t;`.
+
+As the first argument, the `t()` function accepts either a string literal or an object literal containing the `id`, `string` and `plural` (optional) properties. The string literal will serve as both the *message ID* and the *message string*.
+
+As the second argument, the translation function accepts a value or an array of values. These values will be used to fill the placeholders in more advanced translation scenarios. If the `plural` property is specified, the first value will be used as the quantity determining the plural form.
+
+<info-box warning>
+    Due to the fact that a static code analyzer is used in the translation process, you must use a function named exactly `t()`. It should not be called on a `Locale` instance and it cannot have a different name. The code analyzer looks for *localizable messages* only in `t()` function calls.
+
+    For the same reason, the first argument can only be a string literal or an object literal. Variables cannot be passed.
+</info-box>
+
+When using the `t()` function, you can create your own *localizable messages* or reuse *messages* created in CKEditor 5 packages that your project depends on. In case of reusing *messages*, you will not need to worry about translating them as all work will be done by the CKEditor 5 team and [Transifex translators](https://www.transifex.com/ckeditor/ckeditor5/). Obviously, {@link framework/guides/contributing/contributing#translating your help in translating} will still be appreciated!
+
+For simple *localizable messages*, use the string form for simplicity:
+
+```js
+const emojiName = 'cat';
+
+// Assuming that the English language was picked:
+t( 'insert emoji' ); // "insert emoji"
+t( 'insert %0 emoji', emojiName ); // "insert cat emoji"
+t( 'insert %0 emoji', [ emojiName ] ); // "insert cat emoji"
+```
+
+For more advanced scenarios, use plain object forms:
+
+```js
+const quantity = 3;
+
+// Assuming that the English language was picked:
+t( { string: '%0 emoji', id: 'ACTION_EMOJI' }, 'insert' ); // "insert emoji"
+t( { string: '%0 emoji', plural: '%0 emojis', id: 'N_EMOJIS' }, quantity ); // "3 emojis"
+t( { string: '%1 %0 emoji', plural: '%1 %0 emojis', id: 'ACTION_N_EMOJIS' }, [ quantity, 'Insert' ] ); // "Insert 3 emojis"
+```
+
+### Example: Localizing the plugin UI
+
+This example shows how to create a localizable user interface of a plugin. Here is how you can create a button that will insert a smiling face emoji. The button will have a localizable tooltip.
+
+```js
+// ...
+editor.ui.componentFactory.add( 'smilingFaceEmoji', locale => {
+	const buttonView = new ButtonView( locale );
+
+	// The translation function.
+	const { t } = editor.locale;
+
+	// The localized label.
+	const label = t( 'Insert smiling face emoji' );
+
+	buttonView.set( {
+		label,
+		icon: emojiIcon,
+		tooltip: true
+	} );
+
+	buttonView.on( 'execute', () => {
+		editor.execute( 'insertSmilingFaceEmoji' );
+		editor.editing.view.focus();
+	} );
+} );
+// ...
+```
+
+<info-box warning>
+	See {@link framework/guides/creating-simple-plugin how to create a complete plugin} to have a better understanding of creating CKEditor 5 plugins.
+</info-box>
+
+### Example: Localizing pending actions
+
+{@link module:core/pendingactions~PendingActions Pending actions} are used to inform the user that an action is in progress and they will lose data if they exit the editor at the given moment. Here is how you can localize them:
+
+```js
+class FileRepository {
+	// ...
+	updatePendingAction() {
+		// ...
+		const pendingActions = this.editor.plugins.get( PendingActions );
+
+		// ...
+		const t = this.editor.t;
+		const getMessage = value => t( 'Upload in progress (%0%).', value ); // Upload in progress (12%).
+
+		this._pendingAction = pendingActions.add( getMessage( this.uploadedPercent ) );
+		this._pendingAction.bind( 'message' ).to( this, 'uploadedPercent', getMessage );
+	}
+}
+```
+
+## Adding translations and localizing the editor UI
+
+First of all, if you found a missing or incorrect translation in any of CKEditor 5 features, {@link framework/guides/contributing/contributing#translating see how you can contribute to the project}. CKEditor 5 is an Open Source project used by people from all around the world, so your help will be appreciated by others.
+
+Adding translations to the editor can be done in three ways to satisfy various needs.
+
+- By [adding translations via the translation-service's `add()` function](#using-the-add-function).
+	This can be done before initiating the CKEditor 5 editor instance but it requires importing the CKEditor 5 utility function.
+- By [extending the global `window.CKEDITOR_TRANSLATIONS` object](#using-the-windowckeditor_translations-object).
+	This can be done before initiating the CKEditor 5 editor instance.
+- By [creating `.po` files with translations](#creating-po-files) in the `lang/translations/` directory of the published package like other CKEditor 5 packages do.
+	This option will be useful for third-party plugin creators as it allows bundling translations only for needed languages during the webpack compilation step.
+
+### Using the `add()` function
+
+The first option for adding translations is via {@link module:utils/translation-service.add the translation-service's `add()` helper}. This utility adds translations to the global `window.CKEDITOR_TRANSLATIONS` object by extending it. Since it needs to be imported, it works only before building the editor.
+
+Starting with the CKEditor 5 v19.0.0 release, the `add()` method now accepts an optional `getPluralForm()` function as the third argument. This function is only needed for defining the plural form if no language file was loaded for a particular language. It also accepts an array of translations for a *message* if the *message* should support singular and plural forms.
+
+```js
+add( 'pl', {
+	'Add space': [ 'Dodaj spację', 'Dodaj %0 spacje', 'Dodaj %0 spacji' ]
+} );
+
+// Assuming that the Polish language was picked:
+t( { string: 'Add space', plural: 'Add %0 spaces' }, 1 ) // "Dodaj spację"
+t( { string: 'Add space', plural: 'Add %0 spaces' }, 2 ) // "Dodaj 2 spacje"
+t( { string: 'Add space', plural: 'Add %0 spaces' }, 5 ) // "Dodaj 5 spacji"
+```
+
+### Using the `window.CKEDITOR_TRANSLATIONS` object
+
+The second option is adding translations via the global `window.CKEDITOR_TRANSLATIONS` object.
+
+For each language that should be supported, the `dictionary` property of this object should be extended and the `getPluralForm()` function should be provided if missing.
+
+The `dictionary` property is a `message ID ⇒ translations` map, where the `translations` can be either a string or an array of translations with plural forms for the given language if the message should support plural forms as well.
+
+The `getPluralForm()` property should be a function that returns the plural form index for a given quantity. Note that when using CKEditor 5 translations, this property will be defined by *CKEditor 5 translation assets*.
+
+Check an example below that demonstrates a part of the `window.CKEDITOR_TRANSLATIONS` object with Polish translations for the `Cancel` and `Add space` *message IDs*:
+
+```js
+{
+	// Each key should be a valid language code.
+	pl: {
+		// A map of translations for the 'pl' language.
+		dictionary: {
+			'Cancel': 'Anuluj',
+			'Add space': [ 'Dodaj spację', 'Dodaj %0 spacje', 'Dodaj %0 spacji' ]
+		},
+
+		// A function that returns the plural form index for the given language.
+		// Note that you only need to pass this function when you add translations for a new language.
+		getPluralForm: n => n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && ( n % 100 < 10 || n % 100 >= 20 ) ? 1 : 2
+	}
+	// Other languages...
+}
+```
+
+It is important to extend the existing properties in the `window.CKEDITOR_TRANSLATIONS` object to not lose other translations. This can be achieved easily using `Object.assign()` and the `||` operator.
+
+```js
+// Make sure that the global object is defined. If not, define it.
+window.CKEDITOR_TRANSLATIONS = window.CKEDITOR_TRANSLATIONS || {};
+
+// Make sure that the dictionary for Polish translations exist.
+window.CKEDITOR_TRANSLATIONS[ 'pl' ] = window.CKEDITOR_TRANSLATIONS[ 'pl' ] || {};
+window.CKEDITOR_TRANSLATIONS[ 'pl' ].dictionary =  window.CKEDITOR_TRANSLATIONS[ 'pl' ].dictionary || {};
+
+// Extend the dictionary for Polish translations with your translations:
+Object.assign( window.CKEDITOR_TRANSLATIONS[ 'pl' ].dictionary, {
+	'Save': 'Zapisz'
+} );
+```
+
+If you add a new language, remember to set the `getPluralForm()` function which should return a number (or a Boolean for languages with simple plural rules like English) that indicates which form should be used for the given value.
+
+### Creating `.po` files
+
+The third option of adding plugins should fit mostly owners of plugins that contain many localizable messages. Using this option you need to create a `.po` file per each language code in the `lang/translations/` directory containing translations that follow the [PO file format](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html).
+
+```
+# lang/translations/es.po
+
+msgid ""
+msgstr ""
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgid "Align left"
+msgstr "Alinear a la izquierda"
+```
+
+<info-box warning>
+	Note that by default, the [CKEditor 5 webpack plugin](https://github.com/ckeditor/ckeditor5-dev/tree/master/packages/ckeditor5-dev-webpack-plugin) is configured to parse only the CKEditor 5 source code when looking for *localizable messages* and generating *translation assets*.
+
+	If you develop your own plugin outside the CKEditor 5 ecosystem and localize it by creating *PO files*, you should override both the `sourceFilesPattern` and the `packageNamePattern` options to allow the CKEditor 5 webpack plugin to analyze the code and find *messages* with corresponding translations. You should also mention these webpack plugin changes in your package README to make other users build the localized CKEditor 5 editor with your plugin correctly. This obstacle may be simplified in the future when the localization feature gets more popular.
+</info-box>
+
+To build and configure a localized editor, follow the steps from the {@link features/ui-language Setting the UI language guide}.
+
+## Known limitations
+
+- Currently it is impossible to change the chosen editor's language at runtime without destroying the editor.
+- Currently it is impossible to add more than one language to the bundle using the the CKEditor 5 webpack plugin. In case where multiple *translation assets* should be added to the application, they should be added using the `<script>` tags or imports to the generated *translation assets*.

+ 2 - 1
package.json

@@ -130,7 +130,7 @@
     "test": "node --max_old_space_size=4096 node_modules/@ckeditor/ckeditor5-dev-tests/bin/test.js",
     "manual": "node --max_old_space_size=4096 node_modules/@ckeditor/ckeditor5-dev-tests/bin/test-manual.js",
     "bootstrap": "yarn install",
-    "clean": "rm -rf node_modules",
+    "clean": "rm -rf ./node_modules && rm -rf ./external/*/node_modules",
     "reset": "rm -rf ./node_modules && yarn run bootstrap",
     "reinstall": "yarn run clean && yarn run bootstrap",
     "docs": "node --max-old-space-size=4096 ./scripts/docs/build-docs.js",
@@ -167,6 +167,7 @@
   "workspaces": {
     "packages": [
       "packages/*",
+      "external/*",
       "external/*/packages/*",
       "."
     ],

+ 1 - 1
packages/ckeditor5-font/tests/ui/colorui.js

@@ -304,7 +304,7 @@ describe( 'ColorUI', () => {
 						label: '#000'
 					},
 					{
-						color: 'rgb(255,255,255)',
+						color: 'rgb(255, 255, 255)',
 						label: 'Biały'
 					},
 					{

+ 19 - 1
packages/ckeditor5-table/src/ui/colorinputview.js

@@ -294,8 +294,9 @@ export default class ColorInputView extends View {
 	 */
 	_setInputValue( inputValue ) {
 		if ( !this._stillTyping ) {
+			const normalizedInputValue = normalizeColor( inputValue );
 			// Check if the value matches one of our defined colors.
-			const mappedColor = this.options.colorDefinitions.find( def => inputValue === def.color );
+			const mappedColor = this.options.colorDefinitions.find( def => normalizedInputValue === normalizeColor( def.color ) );
 
 			if ( mappedColor ) {
 				this._inputView.value = mappedColor.label;
@@ -305,3 +306,20 @@ export default class ColorInputView extends View {
 		}
 	}
 }
+
+// Normalizes color value, by stripping extensive whitespace.
+// For example., transforms:
+// * `   rgb(  25 50    0 )` to `rgb(25 50 0)`,
+// * "\t  rgb(  25 ,  50,0 )		" to `rgb(25 50 0)`.
+//
+// @param {String} colorString The value to be normalized.
+// @returns {String}
+function normalizeColor( colorString ) {
+	return colorString
+		// Remove any whitespace right after `(` or `,`.
+		.replace( /([(,])\s+/g, '$1' )
+		// Remove any whitespace at the beginning or right before the end, `)`, `,`, or another whitespace.
+		.replace( /^\s+|\s+(?=[),\s]|$)/g, '' )
+		// Then, replace `,` or whitespace with a single space.
+		.replace( /,|\s/g, ' ' );
+}

+ 6 - 1
packages/ckeditor5-table/src/ui/utils.js

@@ -16,6 +16,7 @@ import { isColor, isLength, isPercentage } from '@ckeditor/ckeditor5-engine/src/
 import { getTableWidgetAncestor } from '../utils';
 import { findAncestor } from '../commands/utils';
 import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
+import { centeredBalloonPositionForLongWidgets } from '@ckeditor/ckeditor5-widget/src/utils';
 
 const DEFAULT_BALLOON_POSITIONS = BalloonPanelView.defaultPositions;
 const BALLOON_POSITIONS = [
@@ -26,6 +27,10 @@ const BALLOON_POSITIONS = [
 	DEFAULT_BALLOON_POSITIONS.southArrowNorthWest,
 	DEFAULT_BALLOON_POSITIONS.southArrowNorthEast
 ];
+const TABLE_PROPERTRIES_BALLOON_POSITIONS = [
+	...BALLOON_POSITIONS,
+	centeredBalloonPositionForLongWidgets
+];
 
 const isEmpty = val => val === '';
 
@@ -69,7 +74,7 @@ export function getBalloonTablePositionData( editor ) {
 
 	return {
 		target: editor.editing.view.domConverter.viewToDom( viewTable ),
-		positions: BALLOON_POSITIONS
+		positions: TABLE_PROPERTRIES_BALLOON_POSITIONS
 	};
 }
 

+ 20 - 0
packages/ckeditor5-table/tests/ui/colorinputview.js

@@ -218,6 +218,26 @@ describe( 'ColorInputView', () => {
 				expect( inputView.value ).to.equal( 'bar' );
 			} );
 
+			it(
+				`when the color input value is set to one of defined colors, but with few additional white spaces,
+				should use its label as the text input value`,
+				() => {
+					view.value = 'rgb(0,    255, 0)';
+					expect( inputView.value ).to.equal( 'Green' );
+
+					view.value = '   rgb( 255 0  0)    ';
+					expect( inputView.value ).to.equal( 'Red' );
+
+					view.value = ' 		  rgb(0,  0,  255 )';
+					expect( inputView.value ).to.equal( 'Blue' );
+
+					// Blindly stripping spaces may not work.
+					// rgb(25 50 0) != rgb(255 0 0)
+					view.value = ' 		  rgb(25 50  0)';
+					expect( inputView.value ).to.equal( ' 		  rgb(25 50  0)' );
+				}
+			);
+
 			it( `when the color input value is set to one of defined colors,
 			should use its label as the text input value`, () => {
 				view.value = 'rgb(0,255,0)';

+ 3 - 1
packages/ckeditor5-table/tests/ui/utils.js

@@ -29,6 +29,7 @@ import {
 } from '../../src/ui/utils';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { centeredBalloonPositionForLongWidgets } from '@ckeditor/ckeditor5-widget/src/utils';
 import { modelTable } from '../_utils/utils';
 
 describe( 'UI Utils', () => {
@@ -139,7 +140,8 @@ describe( 'UI Utils', () => {
 						defaultPositions.northArrowSouthEast,
 						defaultPositions.southArrowNorth,
 						defaultPositions.southArrowNorthWest,
-						defaultPositions.southArrowNorthEast
+						defaultPositions.southArrowNorthEast,
+						centeredBalloonPositionForLongWidgets
 					]
 				} );
 			} );

+ 5 - 2
packages/ckeditor5-ui/src/colorgrid/utils.js

@@ -64,13 +64,16 @@ export function normalizeColorOptions( options ) {
 }
 
 // Creates a normalized color definition from the user-defined configuration.
+// The "normalization" means it will create full
+// {@link module:ui/colorgrid/colorgrid~ColorDefinition `ColorDefinition-like`}
+// object for string values, and add a `view` property, for each definition.
 //
 // @param {String|module:ui/colorgrid/colorgrid~ColorDefinition}
 // @returns {module:ui/colorgrid/colorgrid~ColorDefinition}
 export function normalizeSingleColorDefinition( color ) {
 	if ( typeof color === 'string' ) {
 		return {
-			model: color.replace( / /g, '' ),
+			model: color,
 			label: color,
 			hasBorder: false,
 			view: {
@@ -82,7 +85,7 @@ export function normalizeSingleColorDefinition( color ) {
 		};
 	} else {
 		return {
-			model: color.color.replace( / /g, '' ),
+			model: color.color,
 			label: color.label || color.color,
 			hasBorder: color.hasBorder === undefined ? false : color.hasBorder,
 			view: {

+ 55 - 0
packages/ckeditor5-widget/src/utils.js

@@ -9,6 +9,9 @@
 
 import HighlightStack from './highlightstack';
 import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview';
+import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
+import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 
 import dragHandleIcon from '../theme/icons/drag-handle.svg';
 
@@ -339,6 +342,58 @@ export function viewToModelPositionOutsideModelElement( model, viewElementMatche
 	};
 }
 
+/**
+ * A positioning function passed to the {@link module:utils/dom/position~getOptimalPosition} helper as a last resort
+ * when attaching {@link  module:ui/panel/balloon/balloonpanelview~BalloonPanelView balloon UI} to widgets.
+ * It comes in handy when a widget is longer than the visual viewport of the web browser and/or upper/lower boundaries
+ * of a widget are off screen because of the web page scroll.
+ *
+ *	                                       ┌─┄┄┄┄┄┄┄┄┄Widget┄┄┄┄┄┄┄┄┄┐
+ *	                                       ┊                         ┊
+ *	┌────────────Viewport───────────┐   ┌──╁─────────Viewport────────╁──┐
+ *	│  ┏━━━━━━━━━━Widget━━━━━━━━━┓  │   │  ┃            ^            ┃  │
+ *	│  ┃            ^            ┃  │   │  ┃   ╭───────/ \───────╮   ┃  │
+ *	│  ┃   ╭───────/ \───────╮   ┃  │   │  ┃   │     Balloon     │   ┃  │
+ *	│  ┃   │     Balloon     │   ┃  │   │  ┃   ╰─────────────────╯   ┃  │
+ *	│  ┃   ╰─────────────────╯   ┃  │   │  ┃                         ┃  │
+ *	│  ┃                         ┃  │   │  ┃                         ┃  │
+ *	│  ┃                         ┃  │   │  ┃                         ┃  │
+ *	│  ┃                         ┃  │   │  ┃                         ┃  │
+ *	│  ┃                         ┃  │   │  ┃                         ┃  │
+ *	│  ┃                         ┃  │   │  ┃                         ┃  │
+ *	│  ┃                         ┃  │   │  ┃                         ┃  │
+ *	└──╀─────────────────────────╀──┘   └──╀─────────────────────────╀──┘
+ *	   ┊                         ┊         ┊                         ┊
+ *	   ┊                         ┊         └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
+ *	   ┊                         ┊
+ *	   └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
+ *
+ * **Note**: Works best if used together with
+ * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions default `BalloonPanelView` positions}
+ * like `northArrowSouth` and `southArrowNorth`; the transition between these two and this position is smooth.
+ *
+ * @param {module:utils/dom/rect~Rect} widgetRect A rect of the widget.
+ * @param {module:utils/dom/rect~Rect} balloonRect A rect of the balloon.
+ * @returns {module:utils/dom/position~Position}
+ */
+export function centeredBalloonPositionForLongWidgets( widgetRect, balloonRect ) {
+	const viewportRect = new Rect( global.window );
+	const viewportWidgetInsersectionRect = viewportRect.getIntersection( widgetRect );
+
+	// Because this is a last resort positioning, to keep things simple we're not playing with positions of the arrow
+	// like, for instance, "south west" or whatever. Just try to keep the balloon in the middle of the visible area of
+	// the widget for as long as it is possible. If the widgets becomes invisible (because cropped by the viewport),
+	// just... place the balloon in the middle of it (because why not?).
+	const targetRect = viewportWidgetInsersectionRect || widgetRect;
+	const left = targetRect.left + targetRect.width / 2 - balloonRect.width / 2;
+
+	return {
+		top: Math.max( widgetRect.top, 0 ) + BalloonPanelView.arrowVerticalOffset,
+		left,
+		name: 'arrow_n'
+	};
+}
+
 // Default filler offset function applied to all widget elements.
 //
 // @returns {null}

+ 6 - 2
packages/ckeditor5-widget/src/widgettoolbarrepository.js

@@ -11,7 +11,10 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
 import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
 import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
-import { isWidget } from './utils';
+import {
+	isWidget,
+	centeredBalloonPositionForLongWidgets
+} from './utils';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
@@ -272,7 +275,8 @@ function getBalloonPositionData( editor, relatedElement ) {
 			defaultPositions.northArrowSouthEast,
 			defaultPositions.southArrowNorth,
 			defaultPositions.southArrowNorthWest,
-			defaultPositions.southArrowNorthEast
+			defaultPositions.southArrowNorthEast,
+			centeredBalloonPositionForLongWidgets
 		]
 	};
 }

+ 104 - 1
packages/ckeditor5-widget/tests/utils.js

@@ -20,7 +20,8 @@ import {
 	setHighlightHandling,
 	findOptimalInsertionPosition,
 	viewToModelPositionOutsideModelElement,
-	WIDGET_CLASS_NAME
+	WIDGET_CLASS_NAME,
+	centeredBalloonPositionForLongWidgets
 } from '../src/utils';
 import UIElement from '@ckeditor/ckeditor5-engine/src/view/uielement';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
@@ -29,6 +30,9 @@ import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import Mapper from '@ckeditor/ckeditor5-engine/src/conversion/mapper';
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
 import ModelText from '@ckeditor/ckeditor5-engine/src/model/text';
+import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
 
 describe( 'widget utils', () => {
 	let element, writer, viewDocument;
@@ -489,4 +493,103 @@ describe( 'widget utils', () => {
 			expect( modelPosition.path ).to.deep.equal( [ 3, 1 ] );
 		} );
 	} );
+
+	describe( 'centeredBalloonPositionForLongWidgets()', () => {
+		const arrowVerticalOffset = BalloonPanelView.arrowVerticalOffset;
+
+		// Balloon is a 10x10 rect.
+		const balloonRect = new Rect( {
+			top: 0,
+			left: 0,
+			right: 10,
+			bottom: 10,
+			width: 10,
+			height: 10
+		} );
+
+		beforeEach( () => {
+			testUtils.sinon.stub( global.window, 'innerWidth' ).value( 100 );
+			testUtils.sinon.stub( global.window, 'innerHeight' ).value( 100 );
+		} );
+
+		it( 'should position the balloon inside a widget – at the top + in the middle', () => {
+			// Widget is a 50x150 rect, translated (25,25) from viewport's beginning (0,0).
+			const widgetRect = new Rect( {
+				top: 25,
+				left: 25,
+				right: 75,
+				bottom: 175,
+				width: 50,
+				height: 150
+			} );
+
+			const position = centeredBalloonPositionForLongWidgets( widgetRect, balloonRect );
+
+			expect( position ).to.deep.equal( {
+				top: 25 + arrowVerticalOffset,
+				left: 45,
+				name: 'arrow_n'
+			} );
+		} );
+
+		it( 'should stick the balloon to the top of the viewport when the top of a widget is off-screen', () => {
+			// Widget is a 50x150 rect, translated (25,-25) from viewport's beginning (0,0).
+			const widgetRect = new Rect( {
+				top: -25,
+				left: 25,
+				right: 75,
+				bottom: 150,
+				width: 50,
+				height: 150
+			} );
+
+			const position = centeredBalloonPositionForLongWidgets( widgetRect, balloonRect );
+
+			expect( position ).to.deep.equal( {
+				top: arrowVerticalOffset,
+				left: 45,
+				name: 'arrow_n'
+			} );
+		} );
+
+		it( 'should horizontally center the balloon in the visible area when the widget is cropped by the viewport', () => {
+			// Widget is a 50x150 rect, translated (25,-25) from viewport's beginning (0,0).
+			const widgetRect = new Rect( {
+				top: 25,
+				left: -25,
+				right: 25,
+				bottom: 175,
+				width: 50,
+				height: 150
+			} );
+
+			const position = centeredBalloonPositionForLongWidgets( widgetRect, balloonRect );
+
+			expect( position ).to.deep.equal( {
+				top: 25 + arrowVerticalOffset,
+				left: 7.5,
+				name: 'arrow_n'
+			} );
+		} );
+
+		it( 'should horizontally center the balloon in the widget when the widget is completely off the viewport', () => {
+			// Widget is a 50x150 rect, translated (0,-100) from viewport's beginning (0,0).
+			const widgetRect = new Rect( {
+				top: 0,
+				left: -100,
+				right: -50,
+				bottom: 150,
+				width: 50,
+				height: 150
+			} );
+
+			const position = centeredBalloonPositionForLongWidgets( widgetRect, balloonRect );
+
+			expect( position ).to.deep.equal( {
+				top: 0 + arrowVerticalOffset,
+				left: -80,
+				name: 'arrow_n'
+			} );
+		} );
+	} );
 } );

+ 40 - 1
packages/ckeditor5-widget/tests/widgettoolbarrepository.js

@@ -7,13 +7,18 @@
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor';
+import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote';
 import Widget from '../src/widget';
 import WidgetToolbarRepository from '../src/widgettoolbarrepository';
-import { isWidget, toWidget } from '../src/utils';
+import {
+	isWidget,
+	toWidget,
+	centeredBalloonPositionForLongWidgets
+} from '../src/utils';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import View from '@ckeditor/ckeditor5-ui/src/view';
 
@@ -470,6 +475,40 @@ describe( 'WidgetToolbarRepository', () => {
 
 			expect( balloon.view.pin.lastCall.args[ 0 ].target ).to.equal( newFakeDomElement );
 		} );
+
+		it( 'toolbar should use one of pre-defined positions when attaching to a widget', () => {
+			const editingView = editor.editing.view;
+			const balloonAddSpy = sinon.spy( balloon, 'add' );
+			const defaultPositions = BalloonPanelView.defaultPositions;
+
+			widgetToolbarRepository.register( 'fake', {
+				items: editor.config.get( 'fake.toolbar' ),
+				getRelatedElement: getSelectedFakeWidget
+			} );
+
+			setData( model, '<paragraph>foo</paragraph>[<fake-widget></fake-widget>]' );
+
+			const fakeWidgetToolbarView = widgetToolbarRepository._toolbarDefinitions.get( 'fake' ).view;
+			const widgetViewElement = editingView.document.getRoot().getChild( 1 );
+
+			sinon.assert.calledOnce( balloonAddSpy );
+			sinon.assert.calledWithExactly( balloonAddSpy, {
+				view: fakeWidgetToolbarView,
+				position: {
+					target: editingView.domConverter.mapViewToDom( widgetViewElement ),
+					positions: [
+						defaultPositions.northArrowSouth,
+						defaultPositions.northArrowSouthWest,
+						defaultPositions.northArrowSouthEast,
+						defaultPositions.southArrowNorth,
+						defaultPositions.southArrowNorthWest,
+						defaultPositions.southArrowNorthEast,
+						centeredBalloonPositionForLongWidgets
+					]
+				},
+				balloonClassName: 'ck-toolbar-container'
+			} );
+		} );
 	} );
 } );