From 1504f1d4bbda6d329152dea6a757fe17fe38acbb Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms <tleilax+studip@gmail.com> Date: Thu, 20 Jun 2024 12:27:18 +0000 Subject: [PATCH] don't html escape title attributes in vue, fixes #3050 Closes #3050 Merge request studip/studip!3135 --- resources/vue/components/ActiveFilter.vue | 2 +- resources/vue/components/AdminCourses.vue | 2 +- resources/vue/components/ContentModulesControl.vue | 2 +- resources/vue/components/ContentModulesEditTiles.vue | 9 ++++++--- resources/vue/components/ContentmodulesEditTable.vue | 8 +++++--- resources/vue/components/EditableList.vue | 2 +- resources/vue/components/FilesTable.vue | 12 ++++++------ .../courseware/CoursewareContentPermissions.vue | 6 +++--- .../tasks/CoursewareTasksDialogDistribute.vue | 4 ++-- .../courseware/tasks/TaskGroupsAddSolversDialog.vue | 4 ++-- .../courseware/unit/CoursewareUnitItem.vue | 2 +- .../courseware/unit/CoursewareUnitProgress.vue | 2 +- .../form_inputs/CalendarPermissionsTable.vue | 6 ++++-- .../form_inputs/MyCoursesColouredTable.vue | 2 +- .../vue/components/questionnaires/InputArray.vue | 6 +++--- resources/vue/components/tree/StudipTreeList.vue | 11 +++++++---- resources/vue/components/tree/StudipTreeTable.vue | 11 +++++++---- resources/vue/components/tree/TreeBreadcrumb.vue | 2 +- resources/vue/components/tree/TreeCourseDetails.vue | 2 +- resources/vue/components/tree/TreeNodeTile.vue | 2 +- resources/vue/components/tree/TreeSearchResult.vue | 2 +- 21 files changed, 56 insertions(+), 43 deletions(-) diff --git a/resources/vue/components/ActiveFilter.vue b/resources/vue/components/ActiveFilter.vue index d4654faa9f4..5394bce50a0 100644 --- a/resources/vue/components/ActiveFilter.vue +++ b/resources/vue/components/ActiveFilter.vue @@ -4,7 +4,7 @@ <button @click="onRemoveActiveFilter" type="button" - :title="$gettextInterpolate($gettext('Filter \'%{name}\' entfernen'), { name })" + :title="$gettextInterpolate($gettext('Filter \'%{name}\' entfernen'), { name }, true)" > <StudipIcon class="text-bottom" shape="decline" role="presentation" alt="" /> </button> diff --git a/resources/vue/components/AdminCourses.vue b/resources/vue/components/AdminCourses.vue index 23f93755365..d24a9004281 100644 --- a/resources/vue/components/AdminCourses.vue +++ b/resources/vue/components/AdminCourses.vue @@ -23,7 +23,7 @@ <th v-for="activeField in sortedActivatedFields" :key="`field-${activeField}`" :class="sort.by === activeField ? 'sort' + sort.direction.toLowerCase() : ''"> <a href="#" @click.prevent="changeSort(activeField)" - :title="sort.by === activeField && sort.direction === 'ASC' ? $gettextInterpolate('Sortiert aufsteigend nach %{field}', {field: fields[activeField]}) : (sort.by === activeField && sort.direction === 'DESC' ? $gettextInterpolate('Sortiert absteigend nach %{ field } ', { field: fields[activeField]}) : $gettextInterpolate('Sortieren nach %{ field }', { field: fields[activeField]}))" + :title="sort.by === activeField && sort.direction === 'ASC' ? $gettextInterpolate('Sortiert aufsteigend nach %{field}', {field: fields[activeField]}, true) : (sort.by === activeField && sort.direction === 'DESC' ? $gettextInterpolate('Sortiert absteigend nach %{ field } ', { field: fields[activeField]}, true) : $gettextInterpolate('Sortieren nach %{ field }', { field: fields[activeField]}, true))" v-if="!unsortableFields.includes(activeField)" > {{ fields[activeField] }} diff --git a/resources/vue/components/ContentModulesControl.vue b/resources/vue/components/ContentModulesControl.vue index f403bddb8c1..34f4125624c 100644 --- a/resources/vue/components/ContentModulesControl.vue +++ b/resources/vue/components/ContentModulesControl.vue @@ -15,7 +15,7 @@ @click.prevent="toggleModuleVisibility(module)"> <studip-icon :shape="module.visibility !== 'tutor' ? 'visibility-visible' : 'visibility-invisible'" class="text-bottom" - :title="$gettextInterpolate($gettext('Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'), { name: module.displayname})"></studip-icon> + :title="$gettextInterpolate($gettext('Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'), { name: module.displayname}, true)"></studip-icon> </a> </div> </div> diff --git a/resources/vue/components/ContentModulesEditTiles.vue b/resources/vue/components/ContentModulesEditTiles.vue index 383c6ab4cb6..7407632c40a 100644 --- a/resources/vue/components/ContentModulesEditTiles.vue +++ b/resources/vue/components/ContentModulesEditTiles.vue @@ -35,7 +35,8 @@ $gettext( 'Sortierelement für Werkzeug %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.' ), - { module: module.displayname } + { module: module.displayname }, + true ) " @keydown="keyboardHandler($event, module)" @@ -76,7 +77,8 @@ $gettext( 'Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten' ), - { name: module.displayname } + { name: module.displayname }, + true ) " ></studip-icon> @@ -90,7 +92,8 @@ $gettext( 'Umbenennen des Inhaltsmoduls %{ name }' ), - { name: module.displayname } + { name: module.displayname }, + true ) " ></studip-icon> diff --git a/resources/vue/components/ContentmodulesEditTable.vue b/resources/vue/components/ContentmodulesEditTable.vue index 8724a24ed92..0a8a0aa6f38 100644 --- a/resources/vue/components/ContentmodulesEditTable.vue +++ b/resources/vue/components/ContentmodulesEditTable.vue @@ -25,7 +25,8 @@ $gettext( 'Sortierelement für Module %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.' ), - { module: module.displayname } + { module: module.displayname }, + true ) " @keydown="keyboardHandler($event, module)" @@ -71,7 +72,8 @@ $gettext( 'Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten' ), - { name: module.displayname } + { name: module.displayname }, + true ) " ></studip-icon> @@ -83,7 +85,7 @@ :title=" $gettextInterpolate($gettext('Umbenennen des Inhaltsmoduls %{ name }'), { name: module.displayname, - }) + }, true) " ></studip-icon> </a> diff --git a/resources/vue/components/EditableList.vue b/resources/vue/components/EditableList.vue index cf1716b6357..39c32ace231 100644 --- a/resources/vue/components/EditableList.vue +++ b/resources/vue/components/EditableList.vue @@ -12,7 +12,7 @@ <studip-icon v-if="item.icon" :shape="item.icon" role="info" :size="20" class="text-bottom" alt=""></studip-icon> <input v-if="name" type="hidden" :name="name + '[]'" :value="item.value"> <span>{{item.name}}</span> - <button v-if="item.deletable" @click.prevent="deleteItem(item)" :title="$gettextInterpolate($gettext('%{ name } löschen'), {name: item.name})" class="undecorated"> + <button v-if="item.deletable" @click.prevent="deleteItem(item)" :title="$gettextInterpolate($gettext('%{ name } löschen'), {name: item.name}, true)" class="undecorated"> <studip-icon shape="trash" role="clickable" :size="20" class="text-bottom"></studip-icon> </button> </li> diff --git a/resources/vue/components/FilesTable.vue b/resources/vue/components/FilesTable.vue index 0c9dbdaeeeb..1e19cc9f948 100644 --- a/resources/vue/components/FilesTable.vue +++ b/resources/vue/components/FilesTable.vue @@ -132,7 +132,7 @@ > <a href="#" @click.prevent - :title="$gettextInterpolate($gettext('Nach %{ colName } sortieren'), {colName: name})"> + :title="$gettextInterpolate($gettext('Nach %{ colName } sortieren'), {colName: name}, true)"> {{name}} </a> </th> @@ -170,14 +170,14 @@ <a :href="folder.url" :id="`folder-${folder.id}`" :title="$gettextInterpolate($gettext('Ordner %{foldername} öffnen'), - { foldername: folder.name})"> + { foldername: folder.name}, true)"> <studip-icon :shape="folder.icon" :size="26" class="text-bottom" alt=""></studip-icon> </a> </td> <td :class="{'filter-match': valueMatchesFilter(folder.name)}"> <a :href="folder.url" :title="$gettextInterpolate($gettext('Ordner %{foldername} öffnen'), - { foldername: folder.name})"> + { foldername: folder.name}, true)"> <span v-html="highlightString(folder.name)"></span> </a> </td> @@ -228,7 +228,7 @@ :href="file.download_url" target="_blank" rel="noopener noreferrer" :title="$gettextInterpolate($gettext('Datei %{filename} herunterladen'), - { filename: file.name })"> + { filename: file.name }, true)"> <studip-icon :shape="file.icon" :size="24" class="text-bottom"></studip-icon> </a> <studip-icon v-else :shape="file.icon" :size="24"></studip-icon> @@ -238,14 +238,14 @@ class="lightbox-image" data-lightbox="gallery" :title="$gettextInterpolate($gettext('Datei %{filename} anzeigen'), - { filename: file.name })"></a> + { filename: file.name }, true)"></a> </td> <td :class="{'filter-match': valueMatchesFilter(file.name)}"> <a :href="file.details_url" data-dialog :id="`file-${file.id}`" :title="$gettextInterpolate($gettext('Details zur Datei %{filename} anzeigen'), - { filename: file.name })"> + { filename: file.name }, true)"> <span v-html="highlightString(file.name)"></span> <studip-icon v-if="file.isAccessible" shape="accessibility" diff --git a/resources/vue/components/courseware/CoursewareContentPermissions.vue b/resources/vue/components/courseware/CoursewareContentPermissions.vue index d2412ef26c4..60f09644471 100644 --- a/resources/vue/components/courseware/CoursewareContentPermissions.vue +++ b/resources/vue/components/courseware/CoursewareContentPermissions.vue @@ -42,7 +42,7 @@ <td class="perm"> <input class="right" - :title="$gettextInterpolate($gettext('Leserechte für %{ userName }'), { userName: user_perm.username })" + :title="$gettextInterpolate($gettext('Leserechte für %{ userName }'), { userName: user_perm.username }, true)" type="radio" :name="`${user_perm.id}_right`" value="read" @@ -53,7 +53,7 @@ <td class="perm"> <input class="right" - :title="$gettextInterpolate($gettext('Lese- und Schreibrechte für %{ userName }'), { userName: user_perm.username })" + :title="$gettextInterpolate($gettext('Lese- und Schreibrechte für %{ userName }'), { userName: user_perm.username }, true)" type="radio" :name="`${user_perm.id}_right`" value="write" @@ -75,7 +75,7 @@ <td class="actions"> <button class="cw-permission-delete" - :title="$gettextInterpolate($gettext('Entfernen der Rechte von %{ userName }'), { userName: user_perm.username })" + :title="$gettextInterpolate($gettext('Entfernen der Rechte von %{ userName }'), { userName: user_perm.username }, true)" @click.prevent="confirmDeleteUserPerm(index)" > </button> diff --git a/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue b/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue index 1e076c752fa..7680167cd2e 100644 --- a/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue +++ b/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue @@ -179,7 +179,7 @@ :aria-label=" $gettextInterpolate($gettext('%{userName} auswählen'), { userName: user.formattedname, - }) + }, true) " /> </td> @@ -217,7 +217,7 @@ :aria-label=" $gettextInterpolate($gettext('%{groupName} auswählen'), { groupName: group.name, - }) + }, true) " /> </td> diff --git a/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue b/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue index a84334169a2..d7db6e19963 100644 --- a/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue +++ b/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue @@ -44,7 +44,7 @@ :aria-label=" $gettextInterpolate($gettext('%{userName} auswählen'), { userName: user.formattedname, - }) + }, true) " /> </td> @@ -77,7 +77,7 @@ :aria-label=" $gettextInterpolate($gettext('%{groupName} auswählen'), { groupName: group.name, - }) + }, true) " /> </td> diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue index f09601e94d7..dc68225f873 100644 --- a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue +++ b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue @@ -63,7 +63,7 @@ :question=" $gettextInterpolate($gettext('Möchten Sie das Lernmaterial %{ unitTitle } wirklich löschen?'), { unitTitle: title, - }) + }, true) " height="200" @confirm="executeDelete" diff --git a/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue b/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue index 7c9de011ea1..31c6c01d3a7 100644 --- a/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue +++ b/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue @@ -17,7 +17,7 @@ <h1> <a :href="chapterUrl" - :title="$gettextInterpolate('%{ pageTitle } öffnen', { pageTitle: selected.name })" + :title="$gettextInterpolate('%{ pageTitle } öffnen', { pageTitle: selected.name }, true)" > {{ selected.name }} </a> diff --git a/resources/vue/components/form_inputs/CalendarPermissionsTable.vue b/resources/vue/components/form_inputs/CalendarPermissionsTable.vue index a0a76a2897a..e507adce1e3 100644 --- a/resources/vue/components/form_inputs/CalendarPermissionsTable.vue +++ b/resources/vue/components/form_inputs/CalendarPermissionsTable.vue @@ -30,14 +30,16 @@ v-model="user.write_permissions" :aria-label="$gettextInterpolate( $gettext('Schreibzugriff für %{name}'), - {name: user.name} + {name: user.name}, + true )"> </td> <td class="actions"> <studip-icon shape="trash" aria-role="button" @click="removeContact(user.id)" :title="$gettextInterpolate( $gettext('Kalender nicht mehr mit %{name} teilen'), - {name: user.name} + {name: user.name}, + true )"></studip-icon> </td> </tr> diff --git a/resources/vue/components/form_inputs/MyCoursesColouredTable.vue b/resources/vue/components/form_inputs/MyCoursesColouredTable.vue index d2cc2f1aa3f..fe67db55661 100644 --- a/resources/vue/components/form_inputs/MyCoursesColouredTable.vue +++ b/resources/vue/components/form_inputs/MyCoursesColouredTable.vue @@ -42,7 +42,7 @@ <input type="hidden" :name="`${name}_course_ids[${course.id}]`" value="0"> <input type="checkbox" :name="`${name}_course_ids[${course.id}]`" value="1" :checked="selected_course_id_list.includes(course.id)" - :title="$gettextInterpolate($gettext('%{course} auswählen'), {course: course.name})"> + :title="$gettextInterpolate($gettext('%{course} auswählen'), {course: course.name}, true)"> </td> </tr> <tr v-if="loadedSemesters.includes(semester_id) && courses.length === 0"> diff --git a/resources/vue/components/questionnaires/InputArray.vue b/resources/vue/components/questionnaires/InputArray.vue index f57ca58d33a..896418f425d 100644 --- a/resources/vue/components/questionnaires/InputArray.vue +++ b/resources/vue/components/questionnaires/InputArray.vue @@ -22,7 +22,7 @@ <td class="dragcolumn"> <a class="dragarea" tabindex="0" - :title="$gettextInterpolate($gettext(`Sortierelement für %{label} %{option}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.`), {option, label})" + :title="$gettextInterpolate($gettext(`Sortierelement für %{label} %{option}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.`), {option, label}, true)" @keydown="keyHandler($event, index)" ref="draghandle"> <span class="drag-handle"></span> @@ -41,7 +41,7 @@ shape="trash" :size="20" @click.prevent="deleteOption(index)" - :title="$gettextInterpolate($gettext('%{label} löschen'), {label})" + :title="$gettextInterpolate($gettext('%{label} löschen'), {label}, true)" /> </td> </tr> @@ -50,7 +50,7 @@ <tr> <td :colspan="3 + additionalColspan"> <button class="as-link" - :title="$gettextInterpolate($gettext('%{label} hinzufügen'), {label})" + :title="$gettextInterpolate($gettext('%{label} hinzufügen'), {label}, true)" @click.prevent="addOption()"> <StudipIcon shape="add" :size="20" alt="" /> </button> diff --git a/resources/vue/components/tree/StudipTreeList.vue b/resources/vue/components/tree/StudipTreeList.vue index 2ab489c62a4..6214234c3e9 100644 --- a/resources/vue/components/tree/StudipTreeList.vue +++ b/resources/vue/components/tree/StudipTreeList.vue @@ -15,7 +15,7 @@ <a v-if="editable && currentNode.attributes.id !== 'root'" :href="editUrl + '/' + currentNode.attributes.id" @click.prevent="editNode(editUrl, currentNode.id)" data-dialog="size=medium" - :title="$gettextInterpolate($gettext('%{name} bearbeiten'), {name: currentNode.attributes.name})"> + :title="$gettextInterpolate($gettext('%{name} bearbeiten'), {name: currentNode.attributes.name}, true)"> <studip-icon shape="edit" :size="20"></studip-icon> </a> @@ -36,7 +36,7 @@ <li v-for="(child, index) in children" :key="index" class="studip-tree-child"> <a v-if="editable && children.length > 1" class="drag-link" tabindex="0" - :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name})" + :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name}, true)" @keydown="keyHandler($event, index)" :ref="'draghandle-' + index"> <span class="drag-handle"></span> @@ -92,8 +92,11 @@ <tr v-for="(course) in courses" :key="course.id" class="studip-tree-child studip-tree-course"> <td> <a :href="courseUrl(course.id)" tabindex="0" - :title="$gettextInterpolate($gettext('Zur Veranstaltung %{ title }'), - { title: course.attributes.title })"> + :title="$gettextInterpolate( + $gettext('Zur Veranstaltung %{ title }'), + { title: course.attributes.title }, + true + )"> <studip-icon shape="seminar" :size="26"></studip-icon> <template v-if="course.attributes['course-number']"> {{ course.attributes['course-number'] }} diff --git a/resources/vue/components/tree/StudipTreeTable.vue b/resources/vue/components/tree/StudipTreeTable.vue index 4cfa7d59fd8..093dd9c7a4c 100644 --- a/resources/vue/components/tree/StudipTreeTable.vue +++ b/resources/vue/components/tree/StudipTreeTable.vue @@ -79,7 +79,7 @@ <td> <a v-if="editable && children.length > 1" class="drag-link" role="option" tabindex="0" - :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name})" + :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name}, true)" @keydown="keyHandler($event, index)" :ref="'draghandle-' + index"> <span class="drag-handle"></span> @@ -93,7 +93,7 @@ <a :href="nodeUrl(child.id, semester !== 'all' ? semester : null)" tabindex="0" @click.prevent="openNode(child)" :title="$gettextInterpolate($gettext('Unterebene %{ node } öffnen'), - { node: node.attributes.name })"> + { node: node.attributes.name }, true)"> {{ child.attributes.name }} </a> </td> @@ -112,8 +112,11 @@ </td> <td> <a :href="courseUrl(course.id)" tabindex="0" - :title="$gettextInterpolate($gettext('Zur Veranstaltung %{ title }'), - { title: course.attributes.title })"> + :title="$gettextInterpolate( + $gettext('Zur Veranstaltung %{ title }'), + { title: course.attributes.title }, + true + )"> <template v-if="course.attributes['course-number']"> {{ course.attributes['course-number'] }} </template> diff --git a/resources/vue/components/tree/TreeBreadcrumb.vue b/resources/vue/components/tree/TreeBreadcrumb.vue index 33b04b3c0a2..a8c3dd5e36a 100644 --- a/resources/vue/components/tree/TreeBreadcrumb.vue +++ b/resources/vue/components/tree/TreeBreadcrumb.vue @@ -10,7 +10,7 @@ <a :href="nodeUrl(ancestor.classname + '_' + ancestor.id)" :ref="ancestor.id" @click.prevent="openNode(ancestor.id, ancestor.classname)" tabindex="0" :id="'tree-breadcrumb-' + ancestor.id" - :title="$gettextInterpolate($gettext('%{ node } öffnen'), { node: ancestor.name})"> + :title="$gettextInterpolate($gettext('%{ node } öffnen'), { node: ancestor.name}, true)"> {{ ancestor.name }} </a> <template v-if="index !== node.attributes.ancestors.length - 1"> diff --git a/resources/vue/components/tree/TreeCourseDetails.vue b/resources/vue/components/tree/TreeCourseDetails.vue index 6f3e056cdb1..020cc330c0d 100644 --- a/resources/vue/components/tree/TreeCourseDetails.vue +++ b/resources/vue/components/tree/TreeCourseDetails.vue @@ -12,7 +12,7 @@ <span v-for="(lecturer, index) in details.lecturers" :key="index"> <a :href="profileUrl(lecturer.username)" :title="$gettextInterpolate($gettext('Zum Profil von %{ user }'), - { user: lecturer.name })" + { user: lecturer.name }, true)" tabindex="0"> {{ lecturer.name }} </a><template v-if="details.lecturers.length > 1 && index < details.lecturers.length - 1">, </template> diff --git a/resources/vue/components/tree/TreeNodeTile.vue b/resources/vue/components/tree/TreeNodeTile.vue index dab2bdf9ff6..698cc25c024 100644 --- a/resources/vue/components/tree/TreeNodeTile.vue +++ b/resources/vue/components/tree/TreeNodeTile.vue @@ -1,6 +1,6 @@ <template> <a :href="url" @click.prevent="openNode" :title="$gettextInterpolate($gettext('Unterebene %{ node } öffnen'), - { node: node.attributes.name })"> + { node: node.attributes.name }, true)"> <p class="studip-tree-child-title"> {{ node.attributes.name }} </p> diff --git a/resources/vue/components/tree/TreeSearchResult.vue b/resources/vue/components/tree/TreeSearchResult.vue index 1943dd355fe..9799dae1f4c 100644 --- a/resources/vue/components/tree/TreeSearchResult.vue +++ b/resources/vue/components/tree/TreeSearchResult.vue @@ -31,7 +31,7 @@ </td> <td> <a :href="courseUrl(course.id)" - :title="$gettextInterpolate($gettext('Zur Veranstaltung %{title}'), {title: course.attributes.title})" + :title="$gettextInterpolate($gettext('Zur Veranstaltung %{title}'), {title: course.attributes.title}, true)" tabindex="0"> <template v-if="course.attributes['course-number']"> {{ course.attributes['course-number'] }} -- GitLab