Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MOBILE-4603 lang: Inherit custom strings from parent language #4299

Merged
merged 1 commit into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading