Merge pull request #3643 from NoelDeMartin/MOBILE-4288
MOBILE-4288 multilang: Use parent languages
This commit is contained in:
		
						commit
						70d473397b
					
				| @ -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<string> { | ||||
|         let language = await CoreLang.getCurrentLanguage(); | ||||
|         // Get available languages.
 | ||||
|         const regex = /<(?:lang|span)[^>]+lang="([a-zA-Z0-9_-]+)"[^>]*>.*?<\/(?:lang|span)>/img; | ||||
|         const languages: Set<string> = 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 + '"[^>]*>(.*?)</(?:lang|span)>', '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 + '"[^>]*>(.*?)</(?:lang|span)>', '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}"[^>]*>(.*?)</(?:lang|span)>`, 'img'); | ||||
| 
 | ||||
|         text = text.replace(currentLangRegEx, '$1'); | ||||
|         // Delete the rest of languages
 | ||||
|         text = text.replace(anyLangRegEx, ''); | ||||
| 
 | ||||
|         return text; | ||||
|  | ||||
| @ -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( | ||||
|  | ||||
| @ -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<void> { | ||||
|         const promises: Promise<unknown>[] = []; | ||||
| 
 | ||||
|         // 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<void> { | ||||
|         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); | ||||
|  | ||||
							
								
								
									
										134
									
								
								src/core/services/tests/lang.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/core/services/tests/lang.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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(` | ||||
|             <span class="multilang" lang="es">Spanish</span> | ||||
|             <span class="multilang" lang="en">English</span> | ||||
|             <span class="multilang" lang="ja">Japanese</span> | ||||
|             <span class="multilang" lang="ES">(ES)</span> | ||||
|             <span class="multilang" lang="EN">(EN)</span> | ||||
|             <span class="multilang" lang="JA">(JA)</span> | ||||
|             <span lang="es" class="multilang">[Spain]</span> | ||||
|             <span lang="en" class="multilang">[United States]</span> | ||||
|             <span lang="ja" class="multilang">[Japan]</span> | ||||
|             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(` | ||||
|             <span class="multilang" lang="en">English</span> | ||||
|             <span class="multilang" lang="en-US">English</span> | ||||
|             <span class="multilang" lang="en_US">(US)</span> | ||||
|             <span class="multilang" lang="en-AU">English</span> | ||||
|             <span class="multilang" lang="en_AU">(AU)</span> | ||||
|             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 = ` | ||||
|             <span class="multilang" lang="es">Spanish</span> | ||||
|             <span class="multilang" lang="en">English</span> | ||||
|             <span class="multilang" lang="ja">Japanese</span> | ||||
|             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(` | ||||
|             <span class="multilang" lang="es">Spanish</span> | ||||
|             <span class="multilang" lang="en">English</span> | ||||
|             <span class="multilang" lang="ja">Japanese</span> | ||||
|             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<void> { | ||||
|         const actual = await lang.filterMultilang(multilangString(text)); | ||||
| 
 | ||||
|         expect(actual.replace(/\s+/g, ' ').trim()).toEqual(expected); | ||||
|     } | ||||
| 
 | ||||
| }); | ||||
| @ -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) { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user