diff --git a/src/addons/filter/multilang/services/handlers/multilang.ts b/src/addons/filter/multilang/services/handlers/multilang.ts index b5b68ab1f..30685c130 100644 --- a/src/addons/filter/multilang/services/handlers/multilang.ts +++ b/src/addons/filter/multilang/services/handlers/multilang.ts @@ -44,27 +44,34 @@ export class AddonFilterMultilangHandlerService extends CoreFilterDefaultHandler options?: CoreFilterFormatTextOptions, // eslint-disable-line @typescript-eslint/no-unused-vars siteId?: string, // eslint-disable-line @typescript-eslint/no-unused-vars ): Promise { - let language = await CoreLang.getCurrentLanguage(); + // Get available languages. + const regex = /<(?:lang|span)[^>]+lang="([a-zA-Z0-9_-]+)"[^>]*>.*?<\/(?:lang|span)>/img; + const languages: Set = new Set(); + let match: RegExpExecArray | null; - // Match the current language. - const anyLangRegEx = /<(?:lang|span)[^>]+lang="[a-zA-Z0-9_-]+"[^>]*>(.*?)<\/(?:lang|span)>/g; - let currentLangRegEx = new RegExp('<(?:lang|span)[^>]+lang="' + language + '"[^>]*>(.*?)', 'g'); + while ((match = regex.exec(text))) { + const language = match[1].toLowerCase().replace(/_/g, '-'); - if (!text.match(currentLangRegEx)) { - // Current lang not found. Try to find the first language. - const matches = text.match(anyLangRegEx); - if (matches?.[0]) { - language = matches[0].match(/lang="([a-zA-Z0-9_-]+)"/)?.[1] || language; - currentLangRegEx = new RegExp('<(?:lang|span)[^>]+lang="' + language + '"[^>]*>(.*?)', 'g'); - } else { - // No multi-lang tag found, stop. - return text; - } + languages.add(language); } - // Extract contents of current language. + // Find language to use. + let language: string | undefined = await CoreLang.getCurrentLanguage(); + + if (!languages.has(language)) { + language = CoreLang.getParentLanguage(); + } + + if (!language) { + return text; + } + + // Apply filter. + const anyLangRegEx = /<(lang|span)[^>]+lang="[a-zA-Z0-9_-]+"[^>]*>.*?<\/(lang|span)>/img; + const languageRegEx = language.replace(/-/g, '(?:-|_)'); + const currentLangRegEx = new RegExp(`<(?:lang|span)[^>]+lang="${languageRegEx}"[^>]*>(.*?)`, 'img'); + text = text.replace(currentLangRegEx, '$1'); - // Delete the rest of languages text = text.replace(anyLangRegEx, ''); return text; diff --git a/src/addons/filter/multilang2/services/handlers/multilang2.ts b/src/addons/filter/multilang2/services/handlers/multilang2.ts index 003b2786a..91976ed1f 100644 --- a/src/addons/filter/multilang2/services/handlers/multilang2.ts +++ b/src/addons/filter/multilang2/services/handlers/multilang2.ts @@ -45,7 +45,7 @@ export class AddonFilterMultilang2HandlerService extends CoreFilterDefaultHandle const currentLang = await CoreLang.getCurrentLanguage(); this.replacementDone = false; - const parentLanguage = CoreLang.getParentLanguage(currentLang); + const parentLanguage = CoreLang.getParentLanguage(); const search = /{\s*mlang\s+((?:[a-z0-9_-]+)(?:\s*,\s*[a-z0-9_-]+\s*)*)\s*}(.*?){\s*mlang\s*}/gim; const result = text.replace( diff --git a/src/core/services/lang.ts b/src/core/services/lang.ts index 73497ee4d..7af4b5e9b 100644 --- a/src/core/services/lang.ts +++ b/src/core/services/lang.ts @@ -152,12 +152,11 @@ export class CoreLangProvider { /** * Get the parent language defined on the language strings. * - * @param currentLanguage Current language. * @returns If a parent language is set, return the index name. */ - getParentLanguage(currentLanguage: string): string | undefined { + getParentLanguage(): string | undefined { const parentLang = Translate.instant('core.parentlanguage'); - if (parentLang != '' && parentLang != 'core.parentlanguage' && parentLang != currentLanguage) { + if (parentLang !== '' && parentLang !== 'core.parentlanguage' && parentLang !== this.currentLanguage) { return parentLang; } } @@ -169,41 +168,16 @@ export class CoreLangProvider { * @returns Promise resolved when the change is finished. */ async changeCurrentLanguage(language: string): Promise { - const promises: Promise[] = []; - - // Change the language, resolving the promise when we receive the first value. - promises.push(new Promise((resolve, reject) => { - CoreSubscriptions.once(Translate.use(language), async data => { - // Check if it has a parent language. - const fallbackLang = this.getParentLanguage(language); - - if (fallbackLang) { - try { - // Merge parent translations with the child ones. - const parentTranslations = Translate.translations[fallbackLang] ?? await this.readLangFile(fallbackLang); - - const mergedData = Object.assign(parentTranslations, data); - - Object.assign(data, mergedData); - } catch { - // Ignore errors. - } - } - - resolve(data); - }, reject); - })); - - // Change the config. - promises.push(CoreConfig.set('current_language', language)); - // Use british english when parent english is loaded. moment.locale(language == 'en' ? 'en-gb' : language); this.currentLanguage = language; try { - await Promise.all(promises); + await Promise.all([ + this.reloadLanguageStrings(), + CoreConfig.set('current_language', language), + ]); } finally { // Load the custom and site plugins strings for the language. if (this.loadLangStrings(this.customStrings, language) || this.loadLangStrings(this.sitePluginsStrings, language)) { @@ -558,6 +532,39 @@ export class CoreLangProvider { } } + /** + * Reload language strings for the current language. + */ + protected async reloadLanguageStrings(): Promise { + const currentLanguage = this.currentLanguage; + + if (!currentLanguage) { + return; + } + + await new Promise((resolve, reject) => { + CoreSubscriptions.once(Translate.use(currentLanguage), async data => { + // Check if it has a parent language. + const fallbackLang = this.getParentLanguage(); + + if (fallbackLang) { + try { + // Merge parent translations with the child ones. + const parentTranslations = Translate.translations[fallbackLang] ?? await this.readLangFile(fallbackLang); + + const mergedData = Object.assign(parentTranslations, data); + + Object.assign(data, mergedData); + } catch { + // Ignore errors. + } + } + + resolve(data); + }, reject); + }); + } + } export const CoreLang = makeSingleton(CoreLangProvider); diff --git a/src/core/services/tests/lang.test.ts b/src/core/services/tests/lang.test.ts new file mode 100644 index 000000000..9df163e5f --- /dev/null +++ b/src/core/services/tests/lang.test.ts @@ -0,0 +1,134 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { mockSingleton } from '@/testing/utils'; +import { CoreLang, CoreLangProvider, multilangString } from '@services/lang'; + +describe('Lang', () => { + + let lang: CoreLangProvider; + let currentLanguage: string; + let parentLanguage: string | undefined; + + beforeEach(() => { + lang = new CoreLangProvider(); + currentLanguage = 'en'; + + mockSingleton(CoreLang, { + getCurrentLanguage: () => Promise.resolve(currentLanguage), + getParentLanguage: () => parentLanguage, + }); + }); + + it('filters multilang text', async () => { + await expectMultilangFilter('foo', 'foo'); + + await expectMultilangFilter(` + Spanish + English + Japanese + (ES) + (EN) + (JA) + [Spain] + [United States] + [Japan] + text + `, 'English (EN) [United States] text'); + + await expectMultilangFilter(` + {mlang es}Spanish{mlang} + {mlang en}English{mlang} + {mlang ja}Japanese{mlang} + {mlang ES}(ES){mlang} + {mlang EN}(EN){mlang} + {mlang JA}(JA){mlang} + text + `, 'English (EN) text'); + }); + + it('filters multilang text using regions', async () => { + currentLanguage = 'en-au'; + + await expectMultilangFilter(` + English + English + (US) + English + (AU) + text + `, 'English (AU) text'); + + await expectMultilangFilter(` + {mlang en}English{mlang} + {mlang en-US}English{mlang} + {mlang en_US}(US){mlang} + {mlang en-AU}English{mlang} + {mlang en_AU}(AU){mlang} + text + `, 'English (AU) text'); + }); + + it('filters multilang text using the current language', async () => { + const multilangText = ` + Spanish + English + Japanese + text + `; + const multilang2Text = ` + {mlang es}Spanish{mlang} + {mlang en}English{mlang} + {mlang ja}Japanese{mlang} + text + `; + + currentLanguage = 'en'; + await expectMultilangFilter(multilangText, 'English text'); + await expectMultilangFilter(multilang2Text, 'English text'); + + currentLanguage = 'es'; + await expectMultilangFilter(multilangText, 'Spanish text'); + await expectMultilangFilter(multilang2Text, 'Spanish text'); + }); + + it('filters multilang text using the parent language', async () => { + currentLanguage = 'ca'; + parentLanguage = 'ja'; + + await expectMultilangFilter(` + Spanish + English + Japanese + text + `, 'Japanese text'); + + await expectMultilangFilter(` + {mlang es}Spanish{mlang} + {mlang en}English{mlang} + {mlang ja}Japanese{mlang} + text + `, 'Japanese text'); + }); + + /** + * Test multilang filter (normalizing whitespace). + */ + async function expectMultilangFilter(text: string, expected: string): Promise { + const actual = await lang.filterMultilang(multilangString(text)); + + expect(actual.replace(/\s+/g, ' ').trim()).toEqual(expected); + } + +}); diff --git a/src/core/services/utils/url.ts b/src/core/services/utils/url.ts index 32c2e568d..2f21fe6fd 100644 --- a/src/core/services/utils/url.ts +++ b/src/core/services/utils/url.ts @@ -261,7 +261,7 @@ export class CoreUrlUtilsProvider { try { let lang = await CoreLang.getCurrentLanguage(); - lang = CoreLang.getParentLanguage(lang) || lang; + lang = CoreLang.getParentLanguage() || lang; return docsUrl.replace('/en/', '/' + lang + '/'); } catch (error) {