Skip to content

Commit

Permalink
Merge pull request #4299 from dpalou/MOBILE-4603
Browse files Browse the repository at this point in the history
MOBILE-4603 lang: Inherit custom strings from parent language
  • Loading branch information
crazyserver authored Jan 28, 2025
2 parents fa60592 + 1350def commit 6dc0af9
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 30 deletions.
148 changes: 118 additions & 30 deletions src/core/services/lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@ import { CoreConstants } from '@/core/constants';
import { LangChangeEvent } from '@ngx-translate/core';
import { CoreConfig } from '@services/config';
import { CoreSubscriptions } from '@singletons/subscriptions';
import { makeSingleton, Translate, Http } from '@singletons';
import { makeSingleton, Translate } from '@singletons';

import moment from 'moment-timezone';
import { CoreSite } from '../classes/sites/site';
import { CorePlatform } from '@services/platform';
import { firstValueFrom } from 'rxjs';
import { CoreLogger } from '@singletons/logger';
import { CoreSites } from './sites';

Expand Down Expand Up @@ -86,14 +85,9 @@ export class CoreLangProvider {
* @param strings Object with the strings to add.
* @param prefix A prefix to add to all keys.
*/
addSitePluginsStrings(lang: string, strings: string[], prefix?: string): void {
async addSitePluginsStrings(lang: string, strings: string[], prefix?: string): Promise<void> {
lang = lang.replace(/_/g, '-'); // Use the app format instead of Moodle format.

// Initialize structure if it doesn't exist.
if (!this.sitePluginsStrings[lang]) {
this.sitePluginsStrings[lang] = {};
}

for (const key in strings) {
const prefixedKey = prefix + key;
let value = strings[key];
Expand All @@ -111,7 +105,7 @@ export class CoreLangProvider {
value = value.replace(/{{{([^ ]+)}}}/gm, '{{$1}}');

// Load the string.
this.loadString(this.sitePluginsStrings, lang, prefixedKey, value);
await this.loadString(this.sitePluginsStrings, lang, prefixedKey, value);
}
}

Expand Down Expand Up @@ -146,6 +140,12 @@ export class CoreLangProvider {
* @returns Messages.
*/
getMessages(lang: string): Promise<Record<string, string>> {
// Try to use the loaded language first because Translate.getTranslation always reads from the file.
if (Translate.translations[lang]) {
return Promise.resolve(Translate.translations[lang]);
}

// Use Translate.getTranslation to read the translations from the file and store them in the translations variable.
return new Promise(resolve => CoreSubscriptions.once(
Translate.getTranslation(lang),
messages => resolve(messages),
Expand All @@ -154,9 +154,9 @@ export class CoreLangProvider {
}

/**
* Get the parent language defined on the language strings.
* Get the parent language for the current language defined on the language strings.
*
* @returns If a parent language is set, return the index name.
* @returns If a parent language is set, return the parent language.
*/
getParentLanguage(): string | undefined {
const parentLang = Translate.instant('core.parentlanguage');
Expand All @@ -165,6 +165,20 @@ export class CoreLangProvider {
}
}

/**
* Get the parent language for a certain language.
*
* @returns If a parent language is set, return the parent language.
*/
protected async getParentLanguageForLang(lang: string): Promise<string | undefined> {
const translations = await this.getMessages(lang);

const parentLang: string | undefined = translations['core.parentlanguage'];
if (parentLang && parentLang !== 'core.parentlanguage' && parentLang !== lang) {
return parentLang;
}
}

/**
* Change current language.
*
Expand Down Expand Up @@ -192,7 +206,12 @@ export class CoreLangProvider {
throw error;
} finally {
// Load the custom and site plugins strings for the language.
if (this.loadLangStrings(this.customStrings, language) || this.loadLangStrings(this.sitePluginsStrings, language)) {
const [customStringsChangedLang, pluginsStringsChangedLang] = await Promise.all([
this.loadLangStrings(this.customStrings, language),
this.loadLangStrings(this.sitePluginsStrings, language),
]);

if (customStringsChangedLang || pluginsStringsChangedLang) {
// Some lang strings have changed, emit an event to update the pipes.
Translate.onLangChange.emit({ lang: language, translations: Translate.translations[language] });
}
Expand Down Expand Up @@ -388,12 +407,45 @@ export class CoreLangProvider {
});
}

/**
* Check if a certain string is inherited from the parent language.
*
* @param lang Language being checked.
* @param key Key of the string to check.
* @param parentLang Parent language. If not set it will be calculated.
* @returns True if the string is inherited (same as parent), false otherwise.
*/
protected async isInheritedString(lang: string, key: string, parentLang?: string): Promise<boolean> {
parentLang = parentLang ?? await this.getParentLanguageForLang(lang);
if (!parentLang) {
return false;
}

const parentTranslations = await this.getMessages(parentLang);
const childTranslations = await this.getMessages(lang);

return parentTranslations[key] === childTranslations[key];
}

/**
* Check if a language is parent of another language.
*
* @param possibleParentLang Possible parent language.
* @param possibleChildLang Possible children language.
* @returns True if lang is child of the possible parent language.
*/
protected async isParentLang(possibleParentLang: string, possibleChildLang: string): Promise<boolean> {
const parentLang = await this.getParentLanguageForLang(possibleChildLang);

return !!parentLang && parentLang === possibleParentLang;
}

/**
* Loads custom strings obtained from site.
*
* @param currentSite Current site object. If not defined, use current site.
*/
loadCustomStringsFromSite(currentSite?: CoreSite): void {
async loadCustomStringsFromSite(currentSite?: CoreSite): Promise<void> {
currentSite = currentSite ?? CoreSites.getCurrentSite();

if (!currentSite) {
Expand All @@ -406,15 +458,15 @@ export class CoreLangProvider {
return;
}

this.loadCustomStrings(customStrings);
await this.loadCustomStrings(customStrings);
}

/**
* Load certain custom strings.
*
* @param strings Custom strings to load (tool_mobile_customlangstrings).
*/
loadCustomStrings(strings: string): void {
async loadCustomStrings(strings: string): Promise<void> {
if (strings === this.customStringsRaw) {
// Strings haven't changed, stop.
return;
Expand All @@ -430,7 +482,7 @@ export class CoreLangProvider {
let currentLangChanged = false;

const list: string[] = strings.split(/(?:\r\n|\r|\n)/);
list.forEach((entry: string) => {
await Promise.all(list.map(async (entry: string) => {
const values: string[] = entry.split('|').map(value => value.trim());

if (values.length < 3) {
Expand All @@ -444,12 +496,8 @@ export class CoreLangProvider {
currentLangChanged = true;
}

if (!this.customStrings[lang]) {
this.customStrings[lang] = {};
}

this.loadString(this.customStrings, lang, values[0], values[1]);
});
await this.loadString(this.customStrings, lang, values[0], values[1]);
}));

this.customStringsRaw = strings;

Expand All @@ -469,9 +517,35 @@ export class CoreLangProvider {
* @param lang Language to load.
* @returns Whether the translation table was modified.
*/
loadLangStrings(langObject: CoreLanguageObject, lang: string): boolean {
async loadLangStrings(langObject: CoreLanguageObject, lang: string): Promise<boolean> {
let langApplied = false;

// First load the strings of the parent language if they're inherited.
const parentLanguage = await this.getParentLanguageForLang(lang);
if (parentLanguage && langObject[parentLanguage]) {
for (const key in langObject[parentLanguage]) {
if (langObject[lang] && langObject[lang][key]) {
// There is a custom string for the child language, ignore the parent one.
continue;
}

const isInheritedString = await this.isInheritedString(lang, key, parentLanguage);
if (isInheritedString) {
// Store the modification in langObject so it can be undone later.
langObject[lang] = langObject[lang] || {};
langObject[lang][key] = {
original: Translate.translations[lang][key],
value: langObject[parentLanguage][key].value,
applied: true,
};

// Store the string in the translations table.
Translate.translations[lang][key] = langObject[parentLanguage][key].value;
langApplied = true;
}
}
}

if (langObject[lang]) {
for (const key in langObject[lang]) {
const entry = langObject[lang][key];
Expand Down Expand Up @@ -500,9 +574,26 @@ export class CoreLangProvider {
* @param key String key.
* @param value String value.
*/
loadString(langObject: CoreLanguageObject, lang: string, key: string, value: string): void {
async loadString(langObject: CoreLanguageObject, lang: string, key: string, value: string): Promise<void> {
lang = lang.replace(/_/g, '-'); // Use the app format instead of Moodle format.

// If the language to modify is the parent language of a loaded language and the value is inherited,
// update the child language too.
for (const loadedLang in Translate.translations) {
if (loadedLang === lang) {
continue;
}

const isInheritedString = await this.isParentLang(lang, loadedLang) &&
await this.isInheritedString(loadedLang, key, lang);
if (isInheritedString) {
// Modify the child language too.
await this.loadString(langObject, loadedLang, key, value);
}
}

langObject[lang] = langObject[lang] || {};

if (Translate.translations[lang]) {
// The language is loaded.
// Store the original value of the string.
Expand All @@ -529,13 +620,10 @@ export class CoreLangProvider {
*
* @param lang Language code.
* @returns Promise resolved with the file contents.
* @deprecated since 5.0. Use getMessages instead.
*/
async readLangFile(lang: CoreLangLanguage): Promise<Record<string, string>> {
const observable = Http.get(`assets/lang/${lang}.json`, {
responseType: 'json',
});

return <Record<string, string>> await firstValueFrom(observable);
return this.getMessages(lang);
}

/**
Expand Down Expand Up @@ -598,7 +686,7 @@ export class CoreLangProvider {
if (fallbackLang) {
try {
// Merge parent translations with the child ones.
const parentTranslations = Translate.translations[fallbackLang] ?? await this.readLangFile(fallbackLang);
const parentTranslations = await this.getMessages(fallbackLang);

const mergedData = {
...parentTranslations,
Expand Down
1 change: 1 addition & 0 deletions upgrade.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ For more information about upgrading, read the official documentation: https://m
- The parameters of treatDownloadedFile of plugin file handlers have changed. Now the third parameter is an object with all the optional parameters.
- Some CoreColors functions have been refactored to handle alpha and to validate colors.
- The parameters of CoreUrl.addParamsToUrl have changed. Now the third parameter is an object with all the optional parameters.
- The following CoreLang functions were converted to async to properly handle child languages: addSitePluginsStrings, loadCustomStringsFromSite, loadCustomStrings, loadLangStrings, loadString.

=== 4.5.0 ===

Expand Down

0 comments on commit 6dc0af9

Please sign in to comment.