slab / quill

Quill is a modern WYSIWYG editor built for compatibility and extensibility
https://quilljs.com
BSD 3-Clause "New" or "Revised" License
43.5k stars 3.38k forks source link

Quill 2.0 Locales #4331

Open simongiesen opened 2 months ago

simongiesen commented 2 months ago

Recently, it came out that it is extremely unsatisfying using locals with Quill.

I am using the new Quill 2.x WYSIWYG editor. I am trying to implement german localization. Here is my approach so far:

const de = { 'ui': { 'font': 'Schriftart', 'size': 'Größe', 'bold': 'Fett', 'italic': 'Kursiv', 'underline': 'Unterstrichen', 'strike': 'Durchgestrichen', 'color': 'Farbe', 'background': 'Hintergrund', 'script': 'Skript', 'header': 'Überschrift', 'blockquote': 'Zitat', 'code-block': 'Codeblock', 'indent': 'Einrücken', 'list': 'Liste', 'direction': 'Textrichtung', 'link': 'Link', 'image': 'Bild', 'video': 'Video', 'formula': 'Formel', }, 'tooltips': { 'bold': 'Fett', 'italic': 'Kursiv', 'underline': 'Unterstrichen', 'strike': 'Durchgestrichen', 'color': 'Textfarbe', 'background': 'Hintergrundfarbe', 'header': 'Überschrift', 'font': 'Schriftart', 'size': 'Schriftgröße', 'link': 'Link einfügen', 'image': 'Bild einfügen', 'blockquote': 'Zitat', 'code-block': 'Codeblock', 'indent': { '+1': 'Einrücken', '-1': 'Ausrücken' }, 'align': { 'center': 'Zentriert ausrichten', 'right': 'Rechts ausrichten', 'justify': 'Blocksatz' }, 'list': { 'ordered': 'Nummerierte Liste', 'bullet': 'Aufzählungsliste' }, 'direction': 'Textrichtung', 'link': 'Link einfügen', 'formula': 'Formel einfügen' }, 'placeholder': 'Geben Sie hier Ihren Text ein...', 'missing': { 'image': 'Bildbeschreibung', 'link': 'Wohin soll dieser Link führen?' }, 'action': { 'apply': 'Anwenden', 'cancel': 'Abbrechen' } };

var quill = new Quill('#editorRisikobewertung', { modules: { toolbar: [ [{ header: [1, 2, false] }], ['bold', 'italic', 'underline'], ['link', 'blockquote', 'code-block'], [{ 'list': 'ordered'}, { 'list': 'bullet' }] ], },
language: 'de', i18n: { de }, theme: 'snow'
}); Somehow I am missing the point, can you help me out? Nothing happens, the locales are not applied. What is the best approach to implement localization for Quill? I find nothing detailed in the Knowledge Base.

BTW:

it does not make any difference it is just not working.

simongiesen commented 2 months ago

it turns out that localization can be applied via CSS:

<style>
    .ql-snow .ql-tooltip::before {
  content: "URL besuchen:" !important;
}

.ql-snow .ql-tooltip input[type=text]::placeholder {
  content: "https://www.beispiel.de";
}

.ql-snow .ql-tooltip a.ql-action::after {
  content: 'Bearbeiten' !important;
}

.ql-snow .ql-tooltip a.ql-remove::before {
  content: 'Entfernen' !important;
}

.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
  content: 'Speichern' !important;
}

.ql-snow .ql-tooltip[data-mode=link]::before {
  content: "Link eingeben:" !important;
}

.ql-snow .ql-tooltip[data-mode=formula]::before {
  content: "Formel eingeben:" !important;
}

.ql-snow .ql-tooltip[data-mode=video]::before {
  content: "Video-URL eingeben:" !important;
}

/* Zusätzliche Stile für bessere Anpassung */
.ql-snow .ql-tooltip input[type=text] {
  width: 200px; /* Breite anpassen, falls nötig */
}

.ql-snow .ql-tooltip a.ql-preview {
  max-width: 250px; /* Maximale Breite für Vorschau-Links anpassen */
}
  </style>
Memoraike commented 2 months ago

Yeah, it's kind of crazy. But don't rely on !important. Create a separate file with the css class and then connect it during initialization, so your solution will be more flexible. Although with things like embed url placeholder it won't work... This example reproduces a case with a translation into Russian and modification of the Font size representation.

UI ![image](https://github.com/user-attachments/assets/026458d7-0758-497f-88a9-e9a609af65a6)
QuillEditor.vue ```vue ```
_i18n_ru.scss ```scss .ql__i18n_ru { &.ql-bubble { & :deep(.ql-picker) { &.ql-size { $fontSvg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='white' viewBox='0 0 20 20'%3E%3Cpath d='M9.816 11.5 7.038 4.785 4.261 11.5h5.555zm.62 1.5H3.641l-1.666 4.028H.312l5.789-14h1.875l5.789 14h-1.663L10.436 13zm7.55 2.279.779-.779.707.707-2.265 2.265-2.193-2.265.707-.707.765.765V4.825c0-.042 0-.083.002-.123l-.77.77-.707-.707L17.207 2.5l2.265 2.265-.707.707-.782-.782c.002.043.003.089.003.135v10.454z'%3E%3C/path%3E%3C/svg%3E"); $selectedColor: #2e3238; width: 48px; & .ql-picker-options { padding: 4px 0; & > span { padding: 5px 8px; } } & .ql-picker-label { opacity: 0.75; &:hover { opacity: 1; } } &:has(.ql-picker-label:not(:is([data-value='small'], [data-value='large'], [data-value='huge']))) { .ql-picker-item:not(:is([data-value='small'], [data-value='large'], [data-value='huge'])) { background-color: $selectedColor; } } &:has(.ql-picker-label[data-value='small']) { .ql-picker-item[data-value='small'] { background-color: $selectedColor; } } &:has(.ql-picker-label[data-value='large']) { .ql-picker-item[data-value='large'] { background-color: $selectedColor; } } &:has(.ql-picker-label[data-value='huge']) { .ql-picker-item[data-value='huge'] { background-color: $selectedColor; } } & :is(.ql-picker-item)::before { content: 'По умолчанию'; } & :is(.ql-picker-item)[data-value='small']::before { content: 'Маленький'; } & :is(.ql-picker-item)[data-value='large']::before { content: 'Крупный'; } & :is(.ql-picker-item)[data-value='huge']::before { content: 'Очень крупный'; } & .ql-picker-label::before { content: $fontSvg; width: 18px; height: 18px; position: absolute; top: 50%; left: 0; transform: translate(50%, -50%); } } &.ql-header { width: 130px; & :is(.ql-picker-label, .ql-picker-item)::before { content: 'По умолчанию'; } & :is(.ql-picker-label, .ql-picker-item)[data-value='1']::before { content: 'Заголовок 1'; } & :is(.ql-picker-label, .ql-picker-item)[data-value='2']::before { content: 'Заголовок 2'; } & :is(.ql-picker-label, .ql-picker-item)[data-value='3']::before { content: 'Заголовок 3'; } & :is(.ql-picker-label, .ql-picker-item)[data-value='4']::before { content: 'Заголовок 4'; } & :is(.ql-picker-label, .ql-picker-item)[data-value='5']::before { content: 'Заголовок 5'; } & :is(.ql-picker-label, .ql-picker-item)[data-value='6']::before { content: 'Заголовок 6'; } } } } } ```
enzedonline commented 2 months ago

Quill doesn't seem to have any awareness for multi-lingual use or for screen-reader suitability.

I use these for tooltips and labels, each get passed an array of tuples where the first tuple element is a css selector and the second a string display value:

    // set titles on toolbar buttons - multilang support 
    addTooltips(tooltips) {
        if (tooltips) {
            tooltips.map(([selector, tooltip]) => {
                try {
                    this.toolbarContainer.querySelectorAll(selector).forEach(element => {
                        element.setAttribute('title', tooltip);
                    });
                } catch (error) {
                    console.warn(`Quill Tooltips - Invalid selector: ${selector}. Error: ${error.message}`);
                }
            });
        }
    }

    // set labels on toolbar items - multilang support for dropdown items such as font/heading size
    replaceLabels(labels) {
        if (labels) {
            try {
                const style = document.createElement('style');
                style.innerHTML = labels.map(([selector, label]) => {
                    return `${selector} { content: '${label}' !important; }`;
                }).join(' ');
                document.head.appendChild(style);
            } catch (error) {
                console.warn(`Quill Toolbar Labels - Error: ${error.message}`);
            }
        }
    }

And also for button icons:

    replaceButtonIcons(buttonIcons) {
        if (buttonIcons) {
            buttonIcons.map(([selector, svg]) => {
                try {
                    this.toolbarContainer.querySelectorAll(selector).forEach(element => {
                        element.innerHTML = svg;
                    });
                } catch (error) {
                    console.warn(`Quill ButtonIcons - Invalid selector: ${selector}. Error: ${error.message}`);
                }
            });
        }
    }

Where buttonIcons is an array of tuples of selector + svg