Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Text part language support #9074

Merged
merged 36 commits into from
Mar 16, 2021
Merged

Text part language support #9074

merged 36 commits into from
Mar 16, 2021

Conversation

jacekbogdanski
Copy link
Member

Suggested merge commit message (convention)

Feature (language): Added language dropdown button to support text part language. Closes #8989.

Other (utils): Added language.getLanguageDirection helper function allowing to determine text direction based on language code.

Internal (locale): Language text direction detection moved to the language utils module.

{
"name": "@ckeditor/ckeditor5-language",
"version": "25.0.0",
"description": "Block quote feature for CKEditor 5.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ping ;)

@@ -0,0 +1,16 @@
CKEditor 5 language feature
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be consistent here – "Language feature".

Also, cc @AnnaTomanek, do you think it makes sense to call it like this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Language" looks fine (as it's consistent with CKE4). Another option would be "text language" as it then would be clear that it's not about the UI language. In WCAG the success criterion is named "Language of Parts", the lang attribute itself defines the language of text/element. To make it short, we can go with "language" as the feature name and then use other keywords (language of text, language of content, language of text parts) in the docs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm for "text language" and renaming this plugin. I'd prefer to avoid confusion as much as we can.

Copy link
Member Author

@jacekbogdanski jacekbogdanski Feb 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text language would also improve options config. For now, I had to go with languageList to avoid duplication with language. Not sure if CKE5 follow plugin name == config name semantic in the whole codebase, but in most cases, I saw that they are the same.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know we actually used config.language already. So thats a must to rename this feature. The name of the property should match the name of the plugin.

}

/**
* The configuration of the language feature. Introduced by the {@link module:language/languageediting~LanguageEditing} feature.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to be super clear here that it's not about the editor interface language. Ideally, let's link to https://ckeditor.com/docs/ckeditor5/latest/features/ui-language.html in a couple most critical places.

* @module language/utils
*/

import { getLanguageDirection } from '@ckeditor/ckeditor5-utils/src/language';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import is incorrect and I wonder why it wasn't caught by the linter. It should go through ckeditor5/....

* @module utils/language
*/

const RTL_LANGUAGE_CODES = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the 3-letter versions in the standard? In the UI we support the 2-letter ones only, AFAIK.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3-letter version is supported by CKE4.

import { getLanguageDirection } from '../src/language';

describe( 'language', () => {
describe( 'getLanguageDirection', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
describe( 'getLanguageDirection', () => {
describe( 'getLanguageDirection()', () => {


describe( 'language', () => {
describe( 'getLanguageDirection', () => {
it( 'determines the language direction', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid one huge tests. Better to have multiple smaller ones so you know which fails.

describe( 'language', () => {
describe( 'getLanguageDirection', () => {
it( 'determines the language direction', () => {
expect( getLanguageDirection( 'en' ) ).to.eql( 'ltr' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expect( getLanguageDirection( 'en' ) ).to.eql( 'ltr' );
expect( getLanguageDirection( 'en' ) ).to.equal( 'ltr' );

No idea what's the difference, but we use the longer form everywhere else.

*
* const languageConfig = {
* options: [
* { title: 'Arabic', class: 'ck-language_ar', languageCode: 'ar' },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is that class used? I'm not sure the ck- prefix makes sense, but it depends on the context.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should've read further:

 * The `title` and `class` properties will be used by the `language` dropdown to render available options.

I don't think anyone will be styling these options in the dropdown so I don't think we need to have specific classes for them. If anything, we can have these classes added automatically.

In the heading feature, the classes are configurable as you're supposed to style the headings in the dropdown to look like headings in the content. And for that you need these classes.

*/

/**
* @module block-quote/languageediting
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this block-quote correct here?

*
* The `title` and `class` properties will be used by the `language` dropdown to render available options.
*
* The `languageCode` property is used for the lang attribute in ISO 639 format. Language codes can be found
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned in the other place – so far I think we've been supporting two-letter versions. But if CKE4 allowed using 3-letter ones, then it makes sense to have them covered this way here.

import LanguageUI from './languageui';

/**
* The language plugin.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also a good place to clarify that this is not about UI language but an ability to set the language of parts of the content.

/**
* The available language options.
*
* The default value is:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the default from CKE4? It's quite useless, but I suppose it's on purpose – it ensures that the feature works by default, but requires to be reconfigured when integrated for real.

Thus, we don't need to have translations for these language titles. Note: in many other features such strings are covered by language files and we have automatic translations for them. But here, it makes no sense.

This is something that may not be obvious to others (that we don't translate them on purpose), so it'd be good to have a comment about that in the code (next to the default value?). It may be hard to spot if you don't know that you're looking for it, but it's better than nothing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the default from CKE4? It's quite useless, but I suppose it's on purpose – it ensures that the feature works by default, but requires to be reconfigured when integrated for real.

Yes, and yes :) that's the purpose of this default configuration in CKE4.

@niegowski niegowski self-requested a review February 23, 2021 09:13
* @param {module:core/editor/editor~Editor} editor
* @param {String} attributeKey Attribute that will be set by the command.
*/
constructor( editor, attributeKey ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we need this param for the attributeKey. Let's make it inline as in the Link command.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My comment depends on the decision from this comment: #9074 (comment)

const doc = model.document;
const selection = doc.selection;

const value = languageCode ? parseLanguageToString( languageCode, textDirection ) : false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more like stringifyLanguageAttribute, not parseLanguageToString.

* @param {String|Boolean} [options.languageCode] Language code to be applied to the model.
* @param {String} [options.textDirection] Language text direction.
*/
execute( { languageCode, textDirection } = {} ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class looks like a copy-paste from basic-styles AttributeCommand, maybe a better way would be to expose AttributeCommand class from some utils to be reused in basic-styles and in here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT @Reinmar

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just expose _getValueFromFirstAllowedNode() and a part of execute logic as utils helpers, used in both language and AttributeCommand?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe we could provide those helpers as the model methods similar to insertContent and deleteContent?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we have findAttributeRange exported from typing package and it's used in some places, maybe typing should provide more such helpers?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH, IDK what findAttributeRange() does in the typing package as it's not very related... I'm afraid whatever's needed in the LanguageCommand doesn't make much sense there too.

Do you have an idea for a better place?

Also, a little copy-paste here is not a big deal. So, if we can reduce the current ~100% duplication by creating 1-2 reasonable utils and putting them in the core or engine packages, then I'd go for that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that for now we could go with the copy-paste, but someday we will refactor it to extract some common code fragments to the model and selection methods: #9107

return;
}

const { languageCode, textDirection } = parseLanguageFromString( attributeValue );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a better name would be parseLanguageAttribute?

const t = editor.t;

return editor.config.get( 'languageList.options' ).map( option => {
option.title = t( option.title );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this would work as expected.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will not. t() requires a string in order to replace it with a correct translation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I explained it in another comment that perhaps we don't need this translated: https://github.com/ckeditor/ckeditor5/pull/9074#discussion_r580850365.

Copy link
Member Author

@jacekbogdanski jacekbogdanski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't cover API docs at packages/language/docs yet to avoid some redundant changes if we decide to update code which could be reflected in the documentation.

Comment on lines +256 to +269
/**
* Allows to use different language for the editor UI.
*
* The language codes are defined in the [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) standard.
*
* @member {String} module:core/editor/editorconfig~LanguageConfig#ui
*/

/**
* Allows to use different language of the editor content.
*
* The language codes are defined in the [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) standard.
*
* @member {String} module:core/editor/editorconfig~LanguageConfig#content
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

textFragmentLanguage is also supporting ISO 639-2 (3 letters) codes. I'm wondering if we should also extend language.content and language.ui with ISO 639-2 or rather remove this information from textFragmentLanguage and treat ISO 639-2 only as deprecated, compatibility support? I have a feeling that it would be nicer to limit supported codes only to ISO 639-1 as they are more common.

Copy link
Member Author

@jacekbogdanski jacekbogdanski Mar 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've simplified it to ISO 639-1. It's still possible to use other ISO formats since it's just a string that won't conflict with the current implementation, but using a single format makes our life a bit easier.

}

/**
* The available {@link module:language/textfragmentlanguage} options allowing setting language of parts of the content.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with language.textFragmentLanguage option name to match the plugin name, but it would be probably nicer to simply name it language.textFragment to avoid duplication. WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* The available {@link module:language/textfragmentlanguage} options allowing setting language of parts of the content.
* The available {@link module:language/textfragmentlanguage~TextFragmentLanguage
} options allowing setting language of parts of the content.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a second thought, it probably makes a bit more sense to keep a longer format, so the plugin name. Otherwise, it will be hard to tell what feature is actually introducing this option which may affect discoverability.

@@ -0,0 +1,3 @@
{
"Language": "Toolbar button tooltip for the Text Fragment Language feature."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should describe contexts for all translatable strings.

packages/ckeditor5-language/src/index.js Outdated Show resolved Hide resolved
* Refer to [WCAG 3.1.2 Language of Parts](https://www.w3.org/TR/UNDERSTANDING-WCAG20/meaning-other-lang-id.html) specification
* to learn more.
*
* To change UI editor language, refer to [Setting the UI language](https://ckeditor.com/docs/ckeditor5/latest/features/ui-language.html)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should use glink here

}

/**
* The available {@link module:language/textfragmentlanguage} options allowing setting language of parts of the content.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* The available {@link module:language/textfragmentlanguage} options allowing setting language of parts of the content.
* The available {@link module:language/textfragmentlanguage~TextFragmentLanguage
} options allowing setting language of parts of the content.

* Refer to [WCAG 3.1.2 Language of Parts](https://www.w3.org/TR/UNDERSTANDING-WCAG20/meaning-other-lang-id.html) specification
* to learn more.
*
* To change UI editor languge, refer to [Setting the UI language](https://ckeditor.com/docs/ckeditor5/latest/features/ui-language.html)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use glink here.

/**
* The text fragment language UI plugin.
*
* It introduces the `'language'` button.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's introducing a dropdown, not a button.

packages/ckeditor5-language/src/utils.js Outdated Show resolved Hide resolved
packages/ckeditor5-language/src/textfragmentlanguage.js Outdated Show resolved Hide resolved
packages/ckeditor5-language/src/utils.js Outdated Show resolved Hide resolved
packages/ckeditor5-utils/src/language.js Outdated Show resolved Hide resolved
Comment on lines 6 to 9
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import TextFragmentLanguageEditing from '../src/textfragmentlanguageediting';
import TextFragmentLanguageCommand from '../src/textfragmentlanguagecommand';
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't mix relative imports with package imports.

Comment on lines 115 to 124
function createCustomConfigEditor( languageConfig ) {
return VirtualTestEditor
.create( {
plugins: [ TextFragmentLanguageEditing, Paragraph ],
language: languageConfig
} )
.then( newEditor => {
editor = newEditor;
model = editor.model;
} );
}
} );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would override the editor created before and both the editors would not be destroyed properly.

Comment on lines 8 to 12
import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import TextFragmentLanguageEditing from '../src/textfragmentlanguageediting';
import TextFragmentLanguageUI from '../src/textfragmentlanguageui';
import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't mix imports.

@Reinmar
Copy link
Member

Reinmar commented Mar 9, 2021

CI fails.

Copy link
Contributor

@niegowski niegowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, only some very minor issues as commented.

Comment on lines 15 to 31
/**
* @extends module:core/plugin~Plugin
*/
export default class TextFragmentLanguage extends Plugin {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing description.

Comment on lines 59 to 76
* @member {Array.<module:language/textfragmentlanguage~TextFragmentLanguageOption>}
* module:core/editor/editorconfig~LanguageConfig#textFragmentLanguage
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should note here that this option is not available by default and the TextFragmentLanguage plugin must be added.

let spy;

beforeEach( () => {
spy = sinon.spy();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should init sinon sandbox before using it (testUtils.createSinonSandbox();)

Comment on lines 142 to 145
function getListViewItems( listView ) {
// Let's drop separator.
return listView.items.filter( item => item.children );
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should define this helper inside a describe block.

@Mgsy
Copy link
Member

Mgsy commented Mar 15, 2021

The PR is ready for testing, @FilipTokarski @LukaszGudel, could you please take a look at it?

@jacekbogdanski
Copy link
Member Author

jacekbogdanski commented Mar 15, 2021

I've renamed the feature to "text part language". It shouldn't break anything (especially that tests passed), but worth pulling the latest changes @FilipTokarski @LukaszGudel

@FilipTokarski
Copy link
Member

FilipTokarski commented Mar 15, 2021

There's a little mismatch between content from CKE4 and CKE5. If you create a "nested" text, for example like Hebrew-French-Hebrew, CKE4 creates this markup:

<p><span dir="rtl" lang="he">hebrew <span dir="ltr" lang="fr">french</span> hebrew again</span></p>

CKE5 creates three separate spans:

<p><span lang="he" dir="rtl">hebrew </span><span lang="fr" dir="ltr">french</span><span lang="he" dir="rtl"> hebrew again</span></p>

If you now want to get content from 4 and put it inside 5, the inner span will be removed and the whole text remains in Hebrew:

<p><span lang="he" dir="rtl">hebrew french hebrew again</span></p>

@LukaszGudel
Copy link
Contributor

After testing this PR it's looking good to me.

Other than the issue reported by Filip I could not find anything buggy related to those changes. BTW, could it be also related to #8921?

There are few issues related to RTL languages or mixing LTR with RTL in one paragraph but same issues can be reproduced in the browser without CKE5, so I guess those are not related to the editor itself.

@jacekbogdanski
Copy link
Member Author

@FilipTokarski as @LukaszGudel already wrote the issue with this HTML is caused by #8921.

I've already checked it (using some dirty fix) and it should be fixed with #8921. Basically, element nodes in

if ( !data.modelRange ) {
// Convert children and set conversion result as a current data.
data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) );
}
// Set attribute on current `output`. `Schema` is checked inside this helper function.
const attributeWasSet = setAttributeOn( data.modelRange, { key: modelKey, value: modelValue }, shallow, conversionApi );
if ( attributeWasSet ) {
conversionApi.consumable.consume( data.viewItem, match.match );
}
are converted recursively, so every next child for the same range will replace lang attribute as there is no check if the attribute has already been set.

@FilipTokarski
Copy link
Member

I didn't find any other issues, looks ok 👍

Copy link
Contributor

@niegowski niegowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@niegowski niegowski merged commit 1e2f373 into master Mar 16, 2021
@niegowski niegowski deleted the i/8989 branch March 16, 2021 09:59
@AnnaTomanek
Copy link
Contributor

Just for future reference, the main changelog entry should not be about adding the dropdown for the feature but about adding the feature itself. Now it sounds like the feature has already existed and we've just added a dropdown that was missing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for setting text part language
9 participants