diff --git a/src/addons/calendar/pages/event/event.ts b/src/addons/calendar/pages/event/event.ts index 8104f7621..153c00c0b 100644 --- a/src/addons/calendar/pages/event/event.ts +++ b/src/addons/calendar/pages/event/event.ts @@ -42,6 +42,7 @@ import { AddonCalendarEventsSource } from '@addons/calendar/classes/events-sourc import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreReminders, CoreRemindersService } from '@features/reminders/services/reminders'; import { CoreRemindersSetReminderMenuComponent } from '@features/reminders/components/set-reminder-menu/set-reminder-menu'; +import { CoreLang } from '@services/lang'; /** * Page that displays a single calendar event. @@ -370,7 +371,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { const groups = await CoreGroups.getUserGroupsInCourse(courseId); const group = groups.find((group) => group.id == event.groupid); - this.groupName = group ? group.name : ''; + this.groupName = group ? await CoreLang.filterMultilang(group.name) : ''; } catch { // Error getting groups, just don't show the group name. diff --git a/src/addons/filter/multilang/services/handlers/multilang.ts b/src/addons/filter/multilang/services/handlers/multilang.ts index de308f819..b5b68ab1f 100644 --- a/src/addons/filter/multilang/services/handlers/multilang.ts +++ b/src/addons/filter/multilang/services/handlers/multilang.ts @@ -21,7 +21,7 @@ import { CoreSite } from '@classes/site'; import { makeSingleton } from '@singletons'; /** - * Handler to support the Multilang filter. + * Handler to support the Multilang filter in core. */ @Injectable({ providedIn: 'root' }) export class AddonFilterMultilangHandlerService extends CoreFilterDefaultHandler { diff --git a/src/addons/filter/multilang2/services/handlers/multilang2.ts b/src/addons/filter/multilang2/services/handlers/multilang2.ts index 1060b9f6d..003b2786a 100644 --- a/src/addons/filter/multilang2/services/handlers/multilang2.ts +++ b/src/addons/filter/multilang2/services/handlers/multilang2.ts @@ -19,7 +19,9 @@ import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/def import { makeSingleton } from '@singletons'; /** - * Handler to support the Multilang filter. + * Handler to support the multilang2 community filter. + * + * @see https://moodle.org/plugins/filter_multilang2 */ @Injectable({ providedIn: 'root' }) export class AddonFilterMultilang2HandlerService extends CoreFilterDefaultHandler { diff --git a/src/addons/messages/services/messages.ts b/src/addons/messages/services/messages.ts index e6804e12b..5c4d88403 100644 --- a/src/addons/messages/services/messages.ts +++ b/src/addons/messages/services/messages.ts @@ -33,6 +33,7 @@ import { CoreError } from '@classes/errors/error'; import { AddonMessagesSyncEvents, AddonMessagesSyncProvider } from './messages-sync'; import { CoreWSError } from '@classes/errors/wserror'; import { AddonNotificationsPreferencesNotificationProcessorState } from '@addons/notifications/services/notifications'; +import { MultilangString } from '@services/lang'; const ROOT_CACHE_KEY = 'mmaMessages:'; @@ -2970,7 +2971,7 @@ export type AddonMessagesConversationMember = { conversations?: { // Conversations between users. id: number; // Conversations id. type: number; // Conversation type: private or public. - name: string; // Multilang compatible conversation name2. + name: MultilangString; // Multilang compatible conversation name2. timecreated: number; // The timecreated timestamp for the conversation. }[]; }; @@ -3495,7 +3496,7 @@ export type AddonMessagesGetUserContactsWSResponse = { conversations?: { // Conversations between users. id: number; // Conversations id. type: number; // Conversation type: private or public. - name: string; // Multilang compatible conversation name2. + name: MultilangString; // Multilang compatible conversation name2. timecreated: number; // The timecreated timestamp for the conversation. }[]; }[]; @@ -3535,7 +3536,7 @@ export type AddonMessagesGetContactRequestsWSResponse = { conversations?: { // Conversations between users. id: number; // Conversations id. type: number; // Conversation type: private or public. - name: string; // Multilang compatible conversation name2. + name: MultilangString; // Multilang compatible conversation name2. timecreated: number; // The timecreated timestamp for the conversation. }[]; }[]; diff --git a/src/addons/mod/assign/components/submission/submission.ts b/src/addons/mod/assign/components/submission/submission.ts index c045ff47a..9484f7e45 100644 --- a/src/addons/mod/assign/components/submission/submission.ts +++ b/src/addons/mod/assign/components/submission/submission.ts @@ -1147,10 +1147,10 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can if (this.assign.teamsubmission) { if (lastAttempt.submissiongroup) { // Get the name of the group. - promises.push(CoreGroups.getActivityAllowedGroups(this.assign.cmid).then((result) => { + promises.push(CoreGroups.getActivityAllowedGroups(this.assign.cmid).then(async (result) => { const group = result.groups.find((group) => group.id === lastAttempt.submissiongroup); if (group) { - lastAttempt.submissiongroupname = group.name; + lastAttempt.submissiongroupname = await CoreLang.filterMultilang(group.name); } return; diff --git a/src/addons/mod/assign/services/handlers/prefetch.ts b/src/addons/mod/assign/services/handlers/prefetch.ts index 0cb4fc2f4..e4c389836 100644 --- a/src/addons/mod/assign/services/handlers/prefetch.ts +++ b/src/addons/mod/assign/services/handlers/prefetch.ts @@ -35,6 +35,7 @@ import { AddonModAssignSync, AddonModAssignSyncResult } from '../assign-sync'; import { CoreUser } from '@features/user/services/user'; import { CoreGradesHelper } from '@features/grades/services/grades-helper'; import { CoreCourses } from '@features/courses/services/courses'; +import { multilangString } from '@services/lang'; /** * Handler to prefetch assigns. @@ -355,7 +356,7 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref // Teacher, prefetch all submissions. if (!groupInfo.groups || groupInfo.groups.length == 0) { - groupInfo.groups = [{ id: 0, name: '' }]; + groupInfo.groups = [{ id: 0, name: multilangString() }]; } const promises = groupInfo.groups.map((group) => diff --git a/src/addons/mod/data/services/handlers/prefetch.ts b/src/addons/mod/data/services/handlers/prefetch.ts index 49a2027a8..88a68fa91 100644 --- a/src/addons/mod/data/services/handlers/prefetch.ts +++ b/src/addons/mod/data/services/handlers/prefetch.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { multilangString } from '@services/lang'; import { Injectable } from '@angular/core'; import { CoreComments } from '@features/comments/services/comments'; import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; @@ -98,7 +99,7 @@ export class AddonModDataPrefetchHandlerService extends CoreCourseActivityPrefet const groupInfo = await CoreGroups.getActivityGroupInfo(module.id, false, undefined, options.siteId); if (!groupInfo.groups || groupInfo.groups.length == 0) { - groupInfo.groups = [{ id: 0, name: '' }]; + groupInfo.groups = [{ id: 0, name: multilangString() }]; } groups = groupInfo.groups || []; diff --git a/src/addons/mod/feedback/services/handlers/prefetch.ts b/src/addons/mod/feedback/services/handlers/prefetch.ts index 058465519..1ed289302 100644 --- a/src/addons/mod/feedback/services/handlers/prefetch.ts +++ b/src/addons/mod/feedback/services/handlers/prefetch.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { multilangString } from '@services/lang'; import { Injectable } from '@angular/core'; import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; import { CoreCourseAnyModuleData, CoreCourseCommonModWSOptions } from '@features/course/services/course'; @@ -187,7 +188,7 @@ export class AddonModFeedbackPrefetchHandlerService extends CoreCourseActivityPr const promises: Promise[] = []; if (!groupInfo.groups || groupInfo.groups.length == 0) { - groupInfo.groups = [{ id: 0, name: '' }]; + groupInfo.groups = [{ id: 0, name: multilangString() }]; } groupInfo.groups.forEach((group) => { diff --git a/src/addons/mod/forum/pages/new-discussion/new-discussion.ts b/src/addons/mod/forum/pages/new-discussion/new-discussion.ts index c6ccedb75..86be911f7 100644 --- a/src/addons/mod/forum/pages/new-discussion/new-discussion.ts +++ b/src/addons/mod/forum/pages/new-discussion/new-discussion.ts @@ -44,6 +44,7 @@ import { AddonModForumDiscussionsSwipeManager } from '../../classes/forum-discus import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreLang, multilangString } from '@services/lang'; type NewDiscussionData = { subject: string; @@ -190,14 +191,14 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea } // eslint-disable-next-line promise/no-nesting - return promise.then((forumGroups) => { + return promise.then(async (forumGroups) => { if (forumGroups.length > 0) { this.groups = forumGroups; this.groupIds = forumGroups.map((group) => group.id).filter((id) => id > 0); // Do not override group id. this.newDiscussion.groupId = this.newDiscussion.groupId || this.getInitialGroupId(); this.showGroups = true; - this.calculateGroupName(); + await this.calculateGroupName(); if (this.groupIds.length <= 1) { this.newDiscussion.postToAllGroups = false; } @@ -271,7 +272,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea this.newDiscussion.subscribe = !!discussion.options.discussionsubscribe; this.newDiscussion.pin = !!discussion.options.discussionpinned; this.messageControl.setValue(discussion.message); - this.calculateGroupName(); + await this.calculateGroupName(); // Treat offline attachments if any. if (typeof discussion.options.attachmentsid === 'object' && discussion.options.attachmentsid.offline) { @@ -426,7 +427,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea groups.unshift({ courseid: this.courseId, id: AddonModForumProvider.ALL_PARTICIPANTS, - name: Translate.instant('core.allparticipants'), + name: multilangString(Translate.instant('core.allparticipants')), }); } @@ -611,11 +612,13 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea /** * Calculate current group's name. */ - calculateGroupName(): void { + async calculateGroupName(): Promise { if (this.newDiscussion.groupId <= 0) { this.groupName = undefined; } else { - this.groupName = this.groups.find(group => group.id === this.newDiscussion.groupId)?.name; + const groupName = this.groups.find(group => group.id === this.newDiscussion.groupId)?.name; + + this.groupName = groupName && await CoreLang.filterMultilang(groupName); } } diff --git a/src/addons/mod/lesson/components/index/index.ts b/src/addons/mod/lesson/components/index/index.ts index bd66d2b49..7d5eb0c88 100644 --- a/src/addons/mod/lesson/components/index/index.ts +++ b/src/addons/mod/lesson/components/index/index.ts @@ -48,6 +48,7 @@ import { import { AddonModLessonModuleHandlerService } from '../../services/handlers/module'; import { CoreTime } from '@singletons/time'; import { CoreError } from '@classes/errors/error'; +import { CoreLang } from '@services/lang'; /** * Component that displays a lesson entry page. @@ -491,7 +492,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo // Search the name of the group if it isn't all participants. if (groupId && this.groupInfo && this.groupInfo.groups) { const group = this.groupInfo.groups.find(group => groupId == group.id); - this.selectedGroupName = group?.name || ''; + this.selectedGroupName = group ? await CoreLang.filterMultilang(group.name) : ''; } // Get the overview of retakes for the group. diff --git a/src/addons/mod/wiki/components/index/index.ts b/src/addons/mod/wiki/components/index/index.ts index 7ed9d1ca3..10d24a76e 100644 --- a/src/addons/mod/wiki/components/index/index.ts +++ b/src/addons/mod/wiki/components/index/index.ts @@ -55,6 +55,7 @@ import { } from '../../services/wiki-sync'; import { AddonModWikiMapModalComponent, AddonModWikiMapModalReturn } from '../map/map'; import { AddonModWikiSubwikiPickerComponent } from '../subwiki-picker/subwiki-picker'; +import { CoreLang } from '@services/lang'; /** * Component that displays a wiki entry page. @@ -901,7 +902,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp if (subwiki.groupid != 0 && userGroups.length > 0) { // Get groupLabel if it has groupId. const group = userGroups.find(group => group.id == subwiki.groupid); - groupLabel = group?.name || ''; + groupLabel = group ? await CoreLang.filterMultilang(group.name) : ''; } else { groupLabel = Translate.instant('addon.mod_wiki.notingroup'); } diff --git a/src/addons/mod/workshop/services/handlers/prefetch.ts b/src/addons/mod/workshop/services/handlers/prefetch.ts index 2da580e75..fd2e48d9b 100644 --- a/src/addons/mod/workshop/services/handlers/prefetch.ts +++ b/src/addons/mod/workshop/services/handlers/prefetch.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { multilangString } from '@services/lang'; import { AddonModDataSyncResult } from '@addons/mod/data/services/data-sync'; import { Injectable } from '@angular/core'; import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; @@ -105,7 +106,7 @@ export class AddonModWorkshopPrefetchHandlerService extends CoreCourseActivityPr if (access.canviewallsubmissions) { const groupInfo = await CoreGroups.getActivityGroupInfo(module.id, false, undefined, options.siteId); if (!groupInfo.groups || groupInfo.groups.length == 0) { - groupInfo.groups = [{ id: 0, name: '' }]; + groupInfo.groups = [{ id: 0, name: multilangString() }]; } groups = groupInfo.groups; } diff --git a/src/core/components/group-selector/group-selector.html b/src/core/components/group-selector/group-selector.html index 3337ff5c3..d7308072e 100644 --- a/src/core/components/group-selector/group-selector.html +++ b/src/core/components/group-selector/group-selector.html @@ -13,8 +13,8 @@ - - {{groupOpt.name}} + + {{ option.text }} diff --git a/src/core/components/group-selector/group-selector.ts b/src/core/components/group-selector/group-selector.ts index 2365e3ffe..74254c084 100644 --- a/src/core/components/group-selector/group-selector.ts +++ b/src/core/components/group-selector/group-selector.ts @@ -12,8 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; +import { + Component, + Input, + Output, + EventEmitter, + ChangeDetectionStrategy, + OnChanges, + SimpleChanges, + ChangeDetectorRef, +} from '@angular/core'; import { CoreGroupInfo } from '@services/groups'; +import { CoreLang } from '@services/lang'; /** * Component to display a group selector. @@ -23,11 +33,51 @@ import { CoreGroupInfo } from '@services/groups'; templateUrl: 'group-selector.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CoreGroupSelectorComponent { +export class CoreGroupSelectorComponent implements OnChanges { @Input() groupInfo?: CoreGroupInfo; @Input() multipleGroupsMessage?: string; @Input() selected!: number; @Output() selectedChange = new EventEmitter(); + options: GroupOption[] = []; + + constructor(protected changeDetectorRef: ChangeDetectorRef) {} + + /** + * @inheritdoc + */ + async ngOnChanges(changes: SimpleChanges): Promise { + if ('groupInfo' in changes) { + this.options = await this.getOptions(); + + this.changeDetectorRef.markForCheck(); + } + } + + /** + * Get options array. + * + * @returns Options. + */ + protected async getOptions(): Promise { + const options = await Promise.all( + (this.groupInfo?.groups ?? []).map(async group => { + const text = await CoreLang.filterMultilang(group.name); + + return { value: group.id, text }; + }), + ); + + return options; + } + +} + +/** + * Group display info. + */ +interface GroupOption { + value: number; + text: string; } diff --git a/src/core/features/user/components/user-profile-field/user-profile-field.ts b/src/core/features/user/components/user-profile-field/user-profile-field.ts index 441b36d6c..5f42f50d7 100644 --- a/src/core/features/user/components/user-profile-field/user-profile-field.ts +++ b/src/core/features/user/components/user-profile-field/user-profile-field.ts @@ -12,11 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { AddonFilterMultilangHandler } from '@addons/filter/multilang/services/handlers/multilang'; -import { AddonFilterMultilang2Handler } from '@addons/filter/multilang2/services/handlers/multilang2'; import { Component, Input, OnInit, Type } from '@angular/core'; import { FormGroup } from '@angular/forms'; - +import { CoreLang, multilangString } from '@services/lang'; import { AuthEmailSignupProfileField } from '@features/login/services/login-helper'; import { CoreUserProfileField } from '@features/user/services/user'; import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profile-field-delegate'; @@ -54,8 +52,7 @@ export class CoreUserProfileFieldComponent implements OnInit { this.componentClass = await CoreUserProfileFieldDelegate.getComponent(this.field, this.signup); if ('param1' in this.field && this.field.param1) { - this.field.param1 = await AddonFilterMultilangHandler.filter( this.field.param1); - this.field.param1 = await AddonFilterMultilang2Handler.filter( this.field.param1); + this.field.param1 = await CoreLang.filterMultilang(multilangString(this.field.param1)); } this.data.field = this.field; diff --git a/src/core/services/groups.ts b/src/core/services/groups.ts index e537c5012..825aa0732 100644 --- a/src/core/services/groups.ts +++ b/src/core/services/groups.ts @@ -20,6 +20,7 @@ import { CoreError } from '@classes/errors/error'; import { makeSingleton, Translate } from '@singletons'; import { CoreWSExternalWarning } from '@services/ws'; import { CoreCourses } from '@features/courses/services/courses'; +import { multilangString, MultilangString } from '@services/lang'; const ROOT_CACHE_KEY = 'mmGroups:'; @@ -174,7 +175,7 @@ export class CoreGroupsProvider { groupInfo.defaultGroupId = 0; } else { if (result.canaccessallgroups || groupInfo.visibleGroups) { - groupInfo.groups.push({ id: 0, name: Translate.instant('core.allparticipants') }); + groupInfo.groups.push({ id: 0, name: multilangString(Translate.instant('core.allparticipants')) }); groupInfo.defaultGroupId = 0; } else { groupInfo.defaultGroupId = result.groups[0].id; @@ -442,7 +443,7 @@ export const CoreGroups = makeSingleton(CoreGroupsProvider); */ export type CoreGroup = { id: number; // Group ID. - name: string; // Multilang compatible name, course unique'. + name: MultilangString; // Multilang compatible name, course unique'. description?: string; // Group description text. descriptionformat?: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). idnumber?: string; // Id number. @@ -526,7 +527,7 @@ type CoreGroupGetCourseUserGroupsWSParams = { export type CoreGroupGetCourseUserGroupsWSResponse = { groups: { id: number; // Group record id. - name: string; // Multilang compatible name, course unique. + name: MultilangString; // Multilang compatible name, course unique. description: string; // Group description text. descriptionformat: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). idnumber: string; // Id number. diff --git a/src/core/services/lang.ts b/src/core/services/lang.ts index c6763637c..73497ee4d 100644 --- a/src/core/services/lang.ts +++ b/src/core/services/lang.ts @@ -24,6 +24,9 @@ import { makeSingleton, Translate, Http } from '@singletons'; import moment from 'moment-timezone'; import { CoreSite } from '../classes/site'; import { CorePlatform } from '@services/platform'; +import { AddonFilterMultilangHandler } from '@addons/filter/multilang/services/handlers/multilang'; +import { AddonFilterMultilang2Handler } from '@addons/filter/multilang2/services/handlers/multilang2'; +import { Brand } from '@/core/utils/types'; /* * Service to handle language features, like changing the current language. @@ -516,6 +519,18 @@ export class CoreLangProvider { return > await observable.toPromise(); } + /** + * Filter a multilang string. + * + * @param text Multilang string. + * @returns Filtered string. + */ + async filterMultilang(text: MultilangString): Promise { + return Promise.resolve(text as unknown as string) + .then(text => AddonFilterMultilangHandler.filter(text)) + .then(text => AddonFilterMultilang2Handler.filter(text)); + } + /** * Unload custom or site plugin strings, removing them from the translations table. * @@ -547,11 +562,27 @@ export class CoreLangProvider { export const CoreLang = makeSingleton(CoreLangProvider); +/** + * Make a multilang string. + * + * @param text String. + * @returns Multilang string. + */ +export function multilangString(text: string = ''): MultilangString { + return text as unknown as MultilangString; +} + /** * Language code. E.g. 'au', 'es', etc. */ export type CoreLangLanguage = string; +/** + * Branded type to mark multilang strings, this is useful to avoid rendering + * multilang strings without filtering. + */ +export type MultilangString = Brand; + /** * Language object has two leves, first per language and second per string key. */ diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 30602240e..baa152a7a 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -46,7 +46,6 @@ import { CoreViewerImageComponent } from '@features/viewer/components/image/imag import { CoreFormFields, CoreForms } from '../../singletons/form'; import { CoreModalLateralTransitionEnter, CoreModalLateralTransitionLeave } from '@classes/modal-lateral-transition'; import { CoreZoomLevel } from '@features/settings/services/settings-helper'; -import { AddonFilterMultilangHandler } from '@addons/filter/multilang/services/handlers/multilang'; import { CoreSites } from '@services/sites'; import { NavigationStart } from '@angular/router'; import { filter } from 'rxjs/operators'; @@ -58,8 +57,8 @@ import { CoreSiteError } from '@classes/errors/siteerror'; import { CoreUserSupport } from '@features/user/services/support'; import { CoreErrorInfoComponent } from '@components/error-info/error-info'; import { CorePlatform } from '@services/platform'; -import { AddonFilterMultilang2Handler } from '@addons/filter/multilang2/services/handlers/multilang2'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; +import { CoreLang, multilangString } from '@services/lang'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -1171,8 +1170,7 @@ export class CoreDomUtilsProvider { if (hasHTMLTags && !CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.7')) { // Treat multilang. - options.message = await AddonFilterMultilangHandler.filter( options.message); - options.message = await AddonFilterMultilang2Handler.filter( options.message); + options.message = await CoreLang.filterMultilang(multilangString( options.message)); } const alertId = Md5.hashAsciiStr((options.header || '') + '#' + (options.message || ''));