diff options
Diffstat (limited to 'ext/js')
-rw-r--r-- | ext/js/background/backend.js | 26 | ||||
-rw-r--r-- | ext/js/comm/api.js | 7 | ||||
-rw-r--r-- | ext/js/data/options-util.js | 28 | ||||
-rw-r--r-- | ext/js/language/en/language-english.js | 29 | ||||
-rw-r--r-- | ext/js/language/ja/language-japanese.js | 77 | ||||
-rwxr-xr-x | ext/js/language/languages.js | 61 | ||||
-rwxr-xr-x | ext/js/language/text-preprocessors.js | 35 | ||||
-rw-r--r-- | ext/js/language/translator.js | 134 | ||||
-rwxr-xr-x | ext/js/pages/settings/languages-controller.js | 49 | ||||
-rw-r--r-- | ext/js/pages/settings/settings-main.js | 4 |
10 files changed, 357 insertions, 93 deletions
diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index e246f0bb..31191612 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -34,6 +34,7 @@ import {DictionaryDatabase} from '../dictionary/dictionary-database.js'; import {Environment} from '../extension/environment.js'; import {ObjectPropertyAccessor} from '../general/object-property-accessor.js'; import {distributeFuriganaInflected, isCodePointJapanese, isStringPartiallyJapanese, convertKatakanaToHiragana as jpConvertKatakanaToHiragana} from '../language/ja/japanese.js'; +import {getLanguageSummaries} from '../language/languages.js'; import {Translator} from '../language/translator.js'; import {AudioDownloader} from '../media/audio-downloader.js'; import {getFileExtensionFromAudioMediaType, getFileExtensionFromImageMediaType} from '../media/media-util.js'; @@ -183,7 +184,8 @@ export class Backend { ['textHasJapaneseCharacters', this._onApiTextHasJapaneseCharacters.bind(this)], ['getTermFrequencies', this._onApiGetTermFrequencies.bind(this)], ['findAnkiNotes', this._onApiFindAnkiNotes.bind(this)], - ['openCrossFramePort', this._onApiOpenCrossFramePort.bind(this)] + ['openCrossFramePort', this._onApiOpenCrossFramePort.bind(this)], + ['getLanguageSummaries', this._onApiGetLanguageSummaries.bind(this)] ]); /* eslint-enable @stylistic/no-multi-spaces */ @@ -906,6 +908,11 @@ export class Backend { return {targetTabId, targetFrameId}; } + /** @type {import('api').ApiHandler<'getLanguageSummaries'>} */ + _onApiGetLanguageSummaries() { + return getLanguageSummaries(); + } + // Command handlers /** @@ -2361,15 +2368,9 @@ export class Backend { if (typeof deinflect !== 'boolean') { deinflect = true; } const enabledDictionaryMap = this._getTranslatorEnabledDictionaryMap(options); const { - general: {mainDictionary, sortFrequencyDictionary, sortFrequencyDictionaryOrder}, + general: {mainDictionary, sortFrequencyDictionary, sortFrequencyDictionaryOrder, language}, scanning: {alphanumeric}, translation: { - convertHalfWidthCharacters, - convertNumericCharacters, - convertAlphabeticCharacters, - convertHiraganaToKatakana, - convertKatakanaToHiragana, - collapseEmphaticSequences, textReplacements: textReplacementsOptions, searchResolution } @@ -2394,16 +2395,11 @@ export class Backend { sortFrequencyDictionary, sortFrequencyDictionaryOrder, removeNonJapaneseCharacters: !alphanumeric, - convertHalfWidthCharacters, - convertNumericCharacters, - convertAlphabeticCharacters, - convertHiraganaToKatakana, - convertKatakanaToHiragana, - collapseEmphaticSequences, searchResolution, textReplacements, enabledDictionaryMap, - excludeDictionaryDefinitions + excludeDictionaryDefinitions, + language }; } diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js index b4fdbeb5..40b8e252 100644 --- a/ext/js/comm/api.js +++ b/ext/js/comm/api.js @@ -361,6 +361,13 @@ export class API { return this._invoke('openCrossFramePort', {targetTabId, targetFrameId}); } + /** + * @returns {Promise<import('api').ApiReturn<'getLanguageSummaries'>>} + */ + getLanguageSummaries() { + return this._invoke('getLanguageSummaries', void 0); + } + // Utilities /** diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js index 1644df2f..7952eafc 100644 --- a/ext/js/data/options-util.js +++ b/ext/js/data/options-util.js @@ -522,7 +522,8 @@ export class OptionsUtil { this._updateVersion22, this._updateVersion23, this._updateVersion24, - this._updateVersion25 + this._updateVersion25, + this._updateVersion26 ]; /* eslint-enable @typescript-eslint/unbound-method */ if (typeof targetVersion === 'number' && targetVersion < result.length) { @@ -1155,6 +1156,31 @@ export class OptionsUtil { } /** + * - Added general.language. + * - Modularized text preprocessors. + * @type {import('options-util').UpdateFunction} + */ + _updateVersion26(options) { + const textPreprocessors = [ + 'convertHalfWidthCharacters', + 'convertNumericCharacters', + 'convertAlphabeticCharacters', + 'convertHiraganaToKatakana', + 'convertKatakanaToHiragana', + 'collapseEmphaticSequences' + ]; + + for (const {options: profileOptions} of options.profiles) { + profileOptions.general.language = 'ja'; + + for (const preprocessor of textPreprocessors) { + delete profileOptions.translation[preprocessor]; + } + } + } + + + /** * @param {string} url * @returns {Promise<chrome.tabs.Tab>} */ diff --git a/ext/js/language/en/language-english.js b/ext/js/language/en/language-english.js new file mode 100644 index 00000000..8268653f --- /dev/null +++ b/ext/js/language/en/language-english.js @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Yomitan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import {capitalizeFirstLetter, decapitalize} from '../text-preprocessors.js'; + +/** @type {import('language-english').EnglishLanguageDescriptor} */ +export const descriptor = { + name: 'English', + iso: 'en', + exampleText: 'read', + textPreprocessors: { + capitalizeFirstLetter, + decapitalize + } +}; diff --git a/ext/js/language/ja/language-japanese.js b/ext/js/language/ja/language-japanese.js new file mode 100644 index 00000000..ced34bcd --- /dev/null +++ b/ext/js/language/ja/language-japanese.js @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 Yomitan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import {basicTextPreprocessorOptions} from '../text-preprocessors.js'; +import {convertAlphabeticToKana} from './japanese-wanakana.js'; +import {collapseEmphaticSequences, convertHalfWidthKanaToFullWidth, convertHiraganaToKatakana, convertKatakanaToHiragana, convertNumericToFullWidth} from './japanese.js'; + +/** @type {import('language-japanese').JapaneseLanguageDescriptor} */ +export const descriptor = { + name: 'Japanese', + iso: 'ja', + exampleText: '読め', + textPreprocessors: { + convertHalfWidthCharacters: { + name: 'Convert half width characters to full width', + description: 'ヨミチャン → ヨミチャン', + options: basicTextPreprocessorOptions, + /** @type {import('language').TextPreprocessorFunction<boolean>} */ + process: (str, setting, sourceMap) => (setting ? convertHalfWidthKanaToFullWidth(str, sourceMap) : str) + }, + convertNumericCharacters: { + name: 'Convert numeric characters to full width', + description: '1234 → 1234', + options: basicTextPreprocessorOptions, + /** @type {import('language').TextPreprocessorFunction<boolean>} */ + process: (str, setting) => (setting ? convertNumericToFullWidth(str) : str) + }, + convertAlphabeticCharacters: { + name: 'Convert alphabetic characters to hiragana', + description: 'yomichan → よみちゃん', + options: basicTextPreprocessorOptions, + /** @type {import('language').TextPreprocessorFunction<boolean>} */ + process: (str, setting, sourceMap) => (setting ? convertAlphabeticToKana(str, sourceMap) : str) + }, + convertHiraganaToKatakana: { + name: 'Convert hiragana to katakana', + description: 'よみちゃん → ヨミチャン', + options: basicTextPreprocessorOptions, + /** @type {import('language').TextPreprocessorFunction<boolean>} */ + process: (str, setting) => (setting ? convertHiraganaToKatakana(str) : str) + }, + convertKatakanaToHiragana: { + name: 'Convert katakana to hiragana', + description: 'ヨミチャン → よみちゃん', + options: basicTextPreprocessorOptions, + /** @type {import('language').TextPreprocessorFunction<boolean>} */ + process: (str, setting) => (setting ? convertKatakanaToHiragana(str) : str) + }, + collapseEmphaticSequences: { + name: 'Collapse emphatic character sequences', + description: 'すっっごーーい → すっごーい / すごい', + options: [[false, false], [true, false], [true, true]], + /** @type {import('language').TextPreprocessorFunction<[collapseEmphatic: boolean, collapseEmphaticFull: boolean]>} */ + process: (str, setting, sourceMap) => { + const [collapseEmphatic, collapseEmphaticFull] = setting; + if (collapseEmphatic) { + str = collapseEmphaticSequences(str, collapseEmphaticFull, sourceMap); + } + return str; + } + } + } +}; diff --git a/ext/js/language/languages.js b/ext/js/language/languages.js new file mode 100755 index 00000000..f51ca163 --- /dev/null +++ b/ext/js/language/languages.js @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 Yomitan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import {descriptor as descriptorEnglish} from './en/language-english.js'; +import {descriptor as descriptorJapanese} from './ja/language-japanese.js'; + +const languageDescriptors = [ + descriptorEnglish, + descriptorJapanese +]; + +/** @type {Map<string, typeof languageDescriptors[0]>} */ +const languageDescriptorMap = new Map(); +for (const languageDescriptor of languageDescriptors) { + languageDescriptorMap.set(languageDescriptor.iso, languageDescriptor); +} + +/** + * @returns {import('language').LanguageSummary[]} + */ +export function getLanguageSummaries() { + const results = []; + for (const {name, iso, exampleText} of languageDescriptorMap.values()) { + results.push({name, iso, exampleText}); + } + return results; +} + +/** + * @returns {import('language').LanguageAndPreprocessors[]} + * @throws {Error} + */ +export function getAllLanguageTextPreprocessors() { + const results = []; + for (const {iso, textPreprocessors} of languageDescriptorMap.values()) { + /** @type {import('language').TextPreprocessorWithId<unknown>[]} */ + const textPreprocessorsArray = []; + for (const [id, textPreprocessor] of Object.entries(textPreprocessors)) { + textPreprocessorsArray.push({ + id, + textPreprocessor: /** @type {import('language').TextPreprocessor<unknown>} */ (textPreprocessor) + }); + } + results.push({iso, textPreprocessors: textPreprocessorsArray}); + } + return results; +} diff --git a/ext/js/language/text-preprocessors.js b/ext/js/language/text-preprocessors.js new file mode 100755 index 00000000..12b3d1b6 --- /dev/null +++ b/ext/js/language/text-preprocessors.js @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 Yomitan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +/** @type {import('language').TextPreprocessorOptions<boolean>} */ +export const basicTextPreprocessorOptions = [false, true]; + +/** @type {import('language').TextPreprocessor<boolean>} */ +export const decapitalize = { + name: 'Decapitalize text', + description: 'CAPITALIZED TEXT → capitalized text', + options: basicTextPreprocessorOptions, + process: (str, setting) => (setting ? str.toLowerCase() : str) +}; + +/** @type {import('language').TextPreprocessor<boolean>} */ +export const capitalizeFirstLetter = { + name: 'Capitalize first letter', + description: 'lowercase text → Lowercase text', + options: basicTextPreprocessorOptions, + process: (str, setting) => (setting ? str.charAt(0).toUpperCase() + str.slice(1) : str) +}; diff --git a/ext/js/language/translator.js b/ext/js/language/translator.js index b2342e8d..4f9304b5 100644 --- a/ext/js/language/translator.js +++ b/ext/js/language/translator.js @@ -18,9 +18,9 @@ import {applyTextReplacement} from '../general/regex-util.js'; import {TextSourceMap} from '../general/text-source-map.js'; -import {convertAlphabeticToKana} from './ja/japanese-wanakana.js'; -import {collapseEmphaticSequences, convertHalfWidthKanaToFullWidth, convertHiraganaToKatakana, convertKatakanaToHiragana, convertNumericToFullWidth, isCodePointJapanese} from './ja/japanese.js'; +import {isCodePointJapanese} from './ja/japanese.js'; import {LanguageTransformer} from './language-transformer.js'; +import {getAllLanguageTextPreprocessors} from './languages.js'; /** * Class which finds term and kanji dictionary entries for text. @@ -41,6 +41,8 @@ export class Translator { this._stringComparer = new Intl.Collator('en-US'); // Invariant locale /** @type {RegExp} */ this._numberRegex = /[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?/; + /** @type {Map<string, {textPreprocessors: import('language').TextPreprocessorWithId<unknown>[], optionSpace: import('translation-internal').PreprocessorOptionsSpace}>} */ + this._textPreprocessors = new Map(); } /** @@ -49,6 +51,14 @@ export class Translator { */ prepare(descriptor) { this._languageTransformer.addDescriptor(descriptor); + for (const {iso, textPreprocessors} of getAllLanguageTextPreprocessors()) { + /** @type {Map<string, import('language').TextPreprocessorOptions<unknown>>} */ + const optionSpace = new Map(); + for (const {id, textPreprocessor} of textPreprocessors) { + optionSpace.set(id, textPreprocessor.options); + } + this._textPreprocessors.set(iso, {textPreprocessors, optionSpace}); + } } /** @@ -415,51 +425,45 @@ export class Translator { } } - // Deinflections and text transformations + // Deinflections and text preprocessing /** * @param {string} text * @param {import('translation').FindTermsOptions} options * @returns {import('translation-internal').DatabaseDeinflection[]} + * @throws {Error} */ _getAlgorithmDeinflections(text, options) { - /** @type {import('translation-internal').TextDeinflectionOptionsArrays} */ - const textOptionVariantArray = [ - this._getTextReplacementsVariants(options), - this._getTextOptionEntryVariants(options.convertHalfWidthCharacters), - this._getTextOptionEntryVariants(options.convertNumericCharacters), - this._getTextOptionEntryVariants(options.convertAlphabeticCharacters), - this._getTextOptionEntryVariants(options.convertHiraganaToKatakana), - this._getTextOptionEntryVariants(options.convertKatakanaToHiragana), - this._getCollapseEmphaticOptions(options) - ]; + const {language} = options; + const info = this._textPreprocessors.get(language); + if (typeof info === 'undefined') { throw new Error(`Unsupported language: ${language}`); } + const {textPreprocessors, optionSpace: textPreprocessorOptionsSpace} = info; + + /** @type {Map<string, import('language').TextPreprocessorOptions<unknown>>} */ + const variantSpace = new Map(); + variantSpace.set('textReplacements', this._getTextReplacementsVariants(options)); + for (const [key, value] of textPreprocessorOptionsSpace) { + variantSpace.set(key, value); + } /** @type {import('translation-internal').DatabaseDeinflection[]} */ const deinflections = []; const used = new Set(); - for (const [textReplacements, halfWidth, numeric, alphabetic, katakana, hiragana, [collapseEmphatic, collapseEmphaticFull]] of /** @type {Generator<import('translation-internal').TextDeinflectionOptions, void, unknown>} */ (this._getArrayVariants(textOptionVariantArray))) { + + for (const arrayVariant of this._generateArrayVariants(variantSpace)) { + const textReplacements = /** @type {import('translation').FindTermsTextReplacement[] | null} */ (arrayVariant.get('textReplacements')); + let text2 = text; const sourceMap = new TextSourceMap(text2); + if (textReplacements !== null) { text2 = this._applyTextReplacements(text2, sourceMap, textReplacements); } - if (halfWidth) { - text2 = convertHalfWidthKanaToFullWidth(text2, sourceMap); - } - if (numeric) { - text2 = convertNumericToFullWidth(text2); - } - if (alphabetic) { - text2 = convertAlphabeticToKana(text2, sourceMap); - } - if (katakana) { - text2 = convertHiraganaToKatakana(text2); - } - if (hiragana) { - text2 = convertKatakanaToHiragana(text2); - } - if (collapseEmphatic) { - text2 = collapseEmphaticSequences(text2, collapseEmphaticFull, sourceMap); + + for (const preprocessor of textPreprocessors.values()) { + const {id, textPreprocessor} = preprocessor; + const setting = arrayVariant.get(id); + text2 = textPreprocessor.process(text2, setting, sourceMap); } for ( @@ -527,36 +531,6 @@ export class Translator { } /** - * @param {import('translation').FindTermsVariantMode} value - * @returns {boolean[]} - */ - _getTextOptionEntryVariants(value) { - switch (value) { - case 'true': return [true]; - case 'variant': return [false, true]; - default: return [false]; - } - } - - /** - * @param {import('translation').FindTermsOptions} options - * @returns {[collapseEmphatic: boolean, collapseEmphaticFull: boolean][]} - */ - _getCollapseEmphaticOptions(options) { - /** @type {[collapseEmphatic: boolean, collapseEmphaticFull: boolean][]} */ - const collapseEmphaticOptions = [[false, false]]; - switch (options.collapseEmphaticSequences) { - case 'true': - collapseEmphaticOptions.push([true, false]); - break; - case 'full': - collapseEmphaticOptions.push([true, false], [true, true]); - break; - } - return collapseEmphaticOptions; - } - - /** * @param {import('translation').FindTermsOptions} options * @returns {(import('translation').FindTermsTextReplacement[] | null)[]} */ @@ -1343,26 +1317,32 @@ export class Translator { } /** - * @param {[...args: unknown[][]]} arrayVariants - * @yields {[...args: unknown[]]} - * @returns {Generator<unknown[], void, unknown>} + * @param {Map<string, unknown[]>} arrayVariants + * @yields {Map<string, unknown>} + * @returns {Generator<Map<string, unknown>, void, void>} */ - *_getArrayVariants(arrayVariants) { - const ii = arrayVariants.length; - - let total = 1; - for (let i = 0; i < ii; ++i) { - total *= arrayVariants[i].length; + *_generateArrayVariants(arrayVariants) { + const variantKeys = [...arrayVariants.keys()]; + const entryVariantLengths = []; + for (const key of variantKeys) { + const entryVariants = /** @type {unknown[]} */ (arrayVariants.get(key)); + entryVariantLengths.push(entryVariants.length); } + const totalVariants = entryVariantLengths.reduce((acc, length) => acc * length, 1); + + for (let variantIndex = 0; variantIndex < totalVariants; ++variantIndex) { + /** @type {Map<string, unknown>} */ + const variant = new Map(); + let remainingIndex = variantIndex; - for (let a = 0; a < total; ++a) { - const variant = []; - let index = a; - for (let i = 0; i < ii; ++i) { - const entryVariants = arrayVariants[i]; - variant.push(entryVariants[index % entryVariants.length]); - index = Math.floor(index / entryVariants.length); + for (let keyIndex = 0; keyIndex < variantKeys.length; ++keyIndex) { + const key = variantKeys[keyIndex]; + const entryVariants = /** @type {unknown[]} */ (arrayVariants.get(key)); + const entryIndex = remainingIndex % entryVariants.length; + variant.set(key, entryVariants[entryIndex]); + remainingIndex = Math.floor(remainingIndex / entryVariants.length); } + yield variant; } } diff --git a/ext/js/pages/settings/languages-controller.js b/ext/js/pages/settings/languages-controller.js new file mode 100755 index 00000000..78f036df --- /dev/null +++ b/ext/js/pages/settings/languages-controller.js @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023-2024 Yomitan Authors + * Copyright (C) 2021-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import {querySelectorNotNull} from '../../dom/query-selector.js'; + +export class LanguagesController { + /** + * @param {import('./settings-controller.js').SettingsController} settingsController + */ + constructor(settingsController) { + /** @type {import('./settings-controller.js').SettingsController} */ + this._settingsController = settingsController; + } + + /** */ + async prepare() { + const languages = await this._settingsController.application.api.getLanguageSummaries(); + languages.sort((a, b) => a.iso.localeCompare(b.iso, 'en')); + this._fillSelect(languages); + } + + /** + * @param {import('language').LanguageSummary[]} languages + */ + _fillSelect(languages) { + const selectElement = querySelectorNotNull(document, '#language-select'); + for (const {iso, name} of languages) { + const option = document.createElement('option'); + option.value = iso; + option.text = `(${iso}) ${name}`; + selectElement.appendChild(option); + } + } +} diff --git a/ext/js/pages/settings/settings-main.js b/ext/js/pages/settings/settings-main.js index dc4b36c9..0b115246 100644 --- a/ext/js/pages/settings/settings-main.js +++ b/ext/js/pages/settings/settings-main.js @@ -30,6 +30,7 @@ import {DictionaryImportController} from './dictionary-import-controller.js'; import {ExtensionKeyboardShortcutController} from './extension-keyboard-shortcuts-controller.js'; import {GenericSettingController} from './generic-setting-controller.js'; import {KeyboardShortcutController} from './keyboard-shortcuts-controller.js'; +import {LanguagesController} from './languages-controller.js'; import {MecabController} from './mecab-controller.js'; import {ModalController} from './modal-controller.js'; import {NestedPopupsController} from './nested-popups-controller.js'; @@ -137,6 +138,9 @@ await Application.main(async (application) => { const secondarySearchDictionaryController = new SecondarySearchDictionaryController(settingsController); secondarySearchDictionaryController.prepare(); + const languagesController = new LanguagesController(settingsController); + languagesController.prepare(); + const translationTextReplacementsController = new TranslationTextReplacementsController(settingsController); translationTextReplacementsController.prepare(); |