From b6e1bb06f3339da13c5d9a349b2cf1f204826a85 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Wed, 6 May 2026 10:54:18 +0200 Subject: [PATCH 01/19] implemented tabulator localization --- .../controllers/api/frontend/v1/Phrasen.php | 27 ++ public/js/api/factory/phrasen.js | 12 +- .../Cis/Profil/MitarbeiterProfil.js | 33 +- .../Cis/Profil/MitarbeiterViewProfil.js | 27 +- public/js/components/Cis/Profil/Profil.js | 2 +- .../js/components/Cis/Profil/StudentProfil.js | 20 +- public/js/components/filter/Filter.js | 163 +++++++- system/phrasesupdate.php | 382 ++++++++++++++++++ 8 files changed, 603 insertions(+), 63 deletions(-) diff --git a/application/controllers/api/frontend/v1/Phrasen.php b/application/controllers/api/frontend/v1/Phrasen.php index 7cc652c71..9bf68a989 100644 --- a/application/controllers/api/frontend/v1/Phrasen.php +++ b/application/controllers/api/frontend/v1/Phrasen.php @@ -32,6 +32,7 @@ class Phrasen extends FHCAPI_Controller 'setLanguage' => self::PERM_ANONYMOUS, 'getLanguage' => self::PERM_ANONYMOUS, 'getAllLanguages' => self::PERM_ANONYMOUS, + 'getPhrases' => self::PERM_ANONYMOUS ]); $this->load->helper('hlp_language'); @@ -92,4 +93,30 @@ class Phrasen extends FHCAPI_Controller $this->terminateWithSuccess($langs); } + public function getPhrases() + { + $languages = json_decode($this->input->get('languages')); + if (!$languages || !count($languages)) { + $this->load->model('system/Sprache_model', 'sprachenModel'); + $activeLanguages = $this->sprachenModel->loadWhere(array('content' => true)); + $activeLanguagesData = $this->getDataOrTerminateWithError($activeLanguages); + $languages = array_map( + function ($languageData) { + return $languageData->sprache; + }, + $activeLanguagesData + ); + } + + $this->load->model('system/Phrase_model', 'phraseModel'); + $phrasesGroupedByCategory = json_decode($this->input->get('phrasesGroupedByCategory')); + $result = []; + foreach ($languages as $language) { + $phrases = $this->phraseModel->getPhrasesByCategoryAndPhrasesAndLanguage($phrasesGroupedByCategory, $language); + $result[$language] = $this->getDataOrTerminateWithError($phrases); + } + + $this->terminateWithSuccess($result); + } + } \ No newline at end of file diff --git a/public/js/api/factory/phrasen.js b/public/js/api/factory/phrasen.js index a87e4f4be..127e64826 100644 --- a/public/js/api/factory/phrasen.js +++ b/public/js/api/factory/phrasen.js @@ -41,5 +41,15 @@ export default { method: 'get', url: '/api/frontend/v1/phrasen/getAllLanguages' }; - } + }, + getPhrases(phrasesGroupedByCategory, languages = []) { + return { + method: 'get', + url: '/api/frontend/v1/phrasen/getPhrases', + params: { + phrasesGroupedByCategory: JSON.stringify(phrasesGroupedByCategory), + languages: JSON.stringify(languages), + }, + }; + }, }; \ No newline at end of file diff --git a/public/js/components/Cis/Profil/MitarbeiterProfil.js b/public/js/components/Cis/Profil/MitarbeiterProfil.js index 559006a1b..cd40d0741 100644 --- a/public/js/components/Cis/Profil/MitarbeiterProfil.js +++ b/public/js/components/Cis/Profil/MitarbeiterProfil.js @@ -34,7 +34,6 @@ export default { return { showModal: false, editDataFilter: null, - preloadedPhrasen:{}, // tabulator options funktionen_table_options: { persistenceID: "filterTableMaProfilFunktionen", @@ -46,6 +45,7 @@ export default { responsiveLayout: "collapse", responsiveLayoutCollapseUseFormatters: false, responsiveLayoutCollapseFormatter: Vue.$collapseFormatter, + locale: true, columns: [ { title: @@ -59,21 +59,21 @@ export default { visible: true }, { - title: Vue.computed(() => this.preloadedPhrasen.bezeichnungPhrase), + title: "ui/bezeichnung", field: "Bezeichnung", headerFilter: true, minWidth: 200, visible: true }, { - title: Vue.computed(() => this.preloadedPhrasen.organisationseinheitPhrase), + title: "lehre/organisationseinheit", field: "Organisationseinheit", headerFilter: true, minWidth: 200, visible: true }, { - title: Vue.computed(() => this.preloadedPhrasen.gueltigVonPhrase), + title: "global/gueltigVon", field: "Gültig_von", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -84,7 +84,7 @@ export default { formatterParams: this.datetimeFormatterParams() }, { - title: Vue.computed(() => this.preloadedPhrasen.gueltigBisPhrase), + title: "global/gueltigBis", field: "Gültig_bis", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -95,7 +95,7 @@ export default { formatterParams: this.datetimeFormatterParams() }, { - title: Vue.computed(() => this.preloadedPhrasen.wochenstundenPhrase), + title: "profil/wochenstunden", field: "Wochenstunden", headerFilter: true, minWidth: 200, @@ -115,6 +115,7 @@ export default { responsiveLayoutCollapseUseFormatters: false, responsiveLayoutCollapseFormatter: Vue.$collapseFormatter, data: [{betriebsmittel: "", Nummer: "", Ausgegeben_am: ""}], + locale: true, columns: [ { title: @@ -128,14 +129,14 @@ export default { visible: true }, { - title: Vue.computed(() => this.preloadedPhrasen.entlehnteBetriebsmittelPhrase), + title: "profil/entlehnteBetriebsmittel", field: "betriebsmittel", headerFilter: true, minWidth: 200, visible: true }, { - title: Vue.computed(() => this.preloadedPhrasen.inventarnummerPhrase), + title: "profil/inventarnummer", field: "Nummer", headerFilter: true, resizable: true, @@ -143,7 +144,7 @@ export default { visible: true }, { - title: Vue.computed(() => this.preloadedPhrasen.ausgabedatumPhrase), + title: "profil/ausgabedatum", field: "Ausgegeben_am", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -299,18 +300,6 @@ export default { }, created() { - // preload phrasen - this.$p.loadCategory(["ui","lehre","global","profil"]).then(() => { - this.preloadedPhrasen.bezeichnungPhrase = this.$p.t('ui/bezeichnung'); - this.preloadedPhrasen.organisationseinheitPhrase = this.$p.t('lehre/organisationseinheit'); - this.preloadedPhrasen.gueltigVonPhrase = this.$p.t('global/gueltigVon'); - this.preloadedPhrasen.gueltigBisPhrase = this.$p.t('global/gueltigBis'); - this.preloadedPhrasen.wochenstundenPhrase = this.$p.t('profil/wochenstunden'); - this.preloadedPhrasen.entlehnteBetriebsmittelPhrase = this.$p.t('profil/entlehnteBetriebsmittel'); - this.preloadedPhrasen.inventarnummerPhrase = this.$p.t('profil/inventarnummer'); - this.preloadedPhrasen.ausgabedatumPhrase = this.$p.t('profil/ausgabedatum'); - this.preloadedPhrasen.loaded=true; - }); //? sorts the profil Updates: pending -> accepted -> rejected this.data.profilUpdates?.sort(this.sortProfilUpdates); @@ -440,7 +429,6 @@ export default {
this.$p.t('ui/bezeichnung')), + title: "ui/bezeichnung", field: "Bezeichnung", headerFilter: true, minWidth: 200, visible: true }, { - title: Vue.computed(() => this.$p.t('lehre/organisationseinheit')), + title: "lehre/organisationseinheit", field: "Organisationseinheit", headerFilter: true, minWidth: 200, visible: true }, { - title: Vue.computed(() => this.$p.t('global/gueltigVon')), + title: "global/gueltigVon", field: "Gültig_von", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -71,7 +71,7 @@ export default { formatterParams: this.datetimeFormatterParams() }, { - title: Vue.computed(() => this.$p.t('global/gueltigBis')), + title: "global/gueltigBis", field: "Gültig_bis", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -82,7 +82,7 @@ export default { formatterParams: this.datetimeFormatterParams() }, { - title: Vue.computed(() => this.$p.t('profil/wochenstunden')), + title: "profil/wochenstunden", field: "Wochenstunden", headerFilter: true, minWidth: 200, @@ -113,9 +113,6 @@ export default { 'data.funktionen'(newVal) { if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setData(newVal); }, - 'language.value'(newVal) { // reevaluates computed phrasen - if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setColumns(this.funktionen_table_options.columns) - } }, computed: { getTelefonValue() { @@ -180,16 +177,6 @@ export default { }; }, }, - created(){ - this.$p.loadCategory(["ui", "lehre", "global", "profil"]).then(() => { - this.preloadedPhrasen.bezeichnungPhrase = this.$p.t('ui/bezeichnung'); - this.preloadedPhrasen.organisationseinheitPhrase = this.$p.t('lehre/organisationseinheit'); - this.preloadedPhrasen.gueltigVonPhrase = this.$p.t('global/gueltigVon'); - this.preloadedPhrasen.gueltigBisPhrase = this.$p.t('global/gueltigBis'); - this.preloadedPhrasen.wochenstundenPhrase = this.$p.t('profil/wochenstunden'); - this.preloadedPhrasen.loaded = true; - }); - }, template: /*html*/ ` @@ -247,7 +234,7 @@ export default {
- +
diff --git a/public/js/components/Cis/Profil/Profil.js b/public/js/components/Cis/Profil/Profil.js index da205816d..37f3a8edb 100644 --- a/public/js/components/Cis/Profil/Profil.js +++ b/public/js/components/Cis/Profil/Profil.js @@ -24,7 +24,7 @@ Vue.$collapseFormatter = function (data) { let item2 = document.createElement("div"); item2.classList.add("col-6"); - item.innerHTML = "" + col.title + ""; + item.innerHTML = "placeholder"; item2.innerHTML = col.value ? col.value : "-"; list.appendChild(item); diff --git a/public/js/components/Cis/Profil/StudentProfil.js b/public/js/components/Cis/Profil/StudentProfil.js index 3f8e8380d..b4debb3c3 100644 --- a/public/js/components/Cis/Profil/StudentProfil.js +++ b/public/js/components/Cis/Profil/StudentProfil.js @@ -33,7 +33,6 @@ export default { showModal: false, collapseIconBetriebsmittel: true, editDataFilter: null, - preloadedPhrasen:{}, // tabulator options zutrittsgruppen_table_options: { persistenceID: "filterTableStudentProfilZutrittsgruppen", @@ -43,7 +42,7 @@ export default { minHeight: 200, layout: "fitColumns", columns: [{ - title: Vue.computed(() => this.preloadedPhrasen.zutrittsGruppenPhrase), + title: "profil/zutrittsGruppen", field: "bezeichnung" }], }, @@ -57,6 +56,7 @@ export default { responsiveLayout: "collapse", responsiveLayoutCollapseUseFormatters: false, responsiveLayoutCollapseFormatter: Vue.$collapseFormatter, + locale: true, columns: [ { title: @@ -69,14 +69,14 @@ export default { headerClick: this.collapseFunction, }, { - title: Vue.computed(()=>this.preloadedPhrasen.entlehnteBetriebsmittelPhrase), + title: "profil/entlehnteBetriebsmittel", field: "betriebsmittel", headerFilter: true, minWidth: 200, visible: true }, { - title: Vue.computed(() =>this.preloadedPhrasen.inventarnummerPhrase) , + title: "profil/inventarnummer", field: "Nummer", headerFilter: true, resizable: true, @@ -84,7 +84,7 @@ export default { visible: true }, { - title: Vue.computed(() =>this.preloadedPhrasen.ausgabedatum) , + title: "profil/ausgabedatum", field: "Ausgegeben_am", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -242,14 +242,6 @@ export default { }, }, created() { - // preload phrasen - this.$p.loadCategory('profil').then(() => { - this.preloadedPhrasen.zutrittsGruppenPhrase = this.$p.t('profil/zutrittsGruppen'); - this.preloadedPhrasen.entlehnteBetriebsmittelPhrase = this.$p.t('profil/entlehnteBetriebsmittel'); - this.preloadedPhrasen.inventarnummerPhrase = this.$p.t('profil/inventarnummer'); - this.preloadedPhrasen.ausgabedatum = this.$p.t('profil/ausgabedatum'); - this.preloadedPhrasen.loaded = true; - }); //? sorts the profil Updates: pending -> accepted -> rejected this.data.profilUpdates?.sort(this.sortProfilUpdates); }, @@ -379,7 +371,6 @@ export default {
({...el, ...{title: filterTitles[el.name]}})); } }, + watch: { + "language.value": { + handler(newSelectedLanguage) { + if (!this.$props.tabulatorOptions.locale) return; + + this.tabulator.setLocale(newSelectedLanguage); + + if (this.$props.tabulatorOptions.responsiveLayoutCollapseFormatter) { + this.localizeCollapsedColumnHeadings(newSelectedLanguage); + } + }, + }, + }, methods: { reloadTable() { if (this.tableOnly) @@ -263,6 +278,11 @@ export const CoreFilterCmpt = { persistence: this.persistence, }, ...(this.tabulatorOptions || {})}; + // set up localizations if required + if (tabulatorOptions.locale) { + tabulatorOptions = await this.configureTabulatorLocalizations(tabulatorOptions); + } + // set default height if no height property is set if (tabulatorOptions.height === undefined && tabulatorOptions.minHeight === undefined && @@ -302,7 +322,13 @@ export const CoreFilterCmpt = { for (let evt of this.tabulatorEvents) this.tabulator.on(evt.event, evt.handler); } - this.tabulator.on('tableBuilt', () => {this.tableBuilt = true; this.$emit('tableBuilt');}); + this.tabulator.on('tableBuilt', () => { + this.tableBuilt = true; + this.$emit('tableBuilt'); + if (tabulatorOptions.locale && tabulatorOptions.responsiveLayoutCollapseFormatter) { + this.localizeCollapsedColumnHeadings(); + } + }); this.tabulator.on("rowSelectionChanged", data => { this.selectedData = data; }); @@ -340,6 +366,9 @@ export const CoreFilterCmpt = { this.selectedFields = cols.filter(col => col.isVisible()).map(col => col.getField()); if (this.tabulator.options.persistence.headerFilter) this._setHeaderFilter(); + if (tabulatorOptions.locale && tabulatorOptions.responsiveLayoutCollapseFormatter) { + this.localizeCollapsedColumnHeadings(); + } }); } @@ -349,6 +378,134 @@ export const CoreFilterCmpt = { this.$emit("headerFilterOn", this.filterActive); }); }, + async configureTabulatorLocalizations(tabulatorOptions) { + tabulatorOptions.locale = this.$p.user_language.value; + tabulatorOptions.langs = {}; + + let phrasesGroupedByCategoryRequestParam = { + tabulator: [ + "loading", + "error", + "item", + "items", + "page_size", + "page_title", + "first", + "first_title", + "last", + "last_title", + "prev", + "prev_title", + "next", + "next_title", + "all", + "showing", + "of", + "rows", + "pages", + ], + }; + tabulatorOptions.columns.forEach((column) => { + if (column.field === "collapse") return; + + let [category, phrase] = column.title.split("/"); + if (phrasesGroupedByCategoryRequestParam[category]) { + phrasesGroupedByCategoryRequestParam[category].push(phrase); + } else { + phrasesGroupedByCategoryRequestParam[category] = [phrase]; + } + }); + + const phrasesResponse = await this.$api.call( + ApiPhrases.getPhrases(phrasesGroupedByCategoryRequestParam), + ); + const phrasesData = phrasesResponse.data; + + Object.keys(phrasesData).forEach((language) => { + let genericTabulatorPhrases = phrasesData[language].filter( + (phrase) => phrase.category === "tabulator", + ); + + let tabulatorPhraseToTranslationMapper = {}; + genericTabulatorPhrases.forEach((phrase) => { + tabulatorPhraseToTranslationMapper[phrase.phrase] = + phrase.text; + }); + + tabulatorOptions.langs[language] = { + data: { + loading: tabulatorPhraseToTranslationMapper["loading"], + error: tabulatorPhraseToTranslationMapper["error"], + }, + groups: { + item: tabulatorPhraseToTranslationMapper["item"], + items: tabulatorPhraseToTranslationMapper["items"], + }, + pagination: { + page_size: + tabulatorPhraseToTranslationMapper["page_size"], + page_title: + tabulatorPhraseToTranslationMapper["page_title"], + first: tabulatorPhraseToTranslationMapper["first"], + first_title: + tabulatorPhraseToTranslationMapper["first_title"], + last: tabulatorPhraseToTranslationMapper["last"], + last_title: + tabulatorPhraseToTranslationMapper["last_title"], + prev: tabulatorPhraseToTranslationMapper["prev"], + prev_title: + tabulatorPhraseToTranslationMapper["prev_title"], + next: tabulatorPhraseToTranslationMapper["next"], + next_title: + tabulatorPhraseToTranslationMapper["next_title"], + all: tabulatorPhraseToTranslationMapper["all"], + counter: { + showing: + tabulatorPhraseToTranslationMapper["showing"], + of: tabulatorPhraseToTranslationMapper["of"], + rows: tabulatorPhraseToTranslationMapper["rows"], + pages: tabulatorPhraseToTranslationMapper["pages"], + }, + }, + }; + + let columnHeadingPhrases = phrasesData[language].filter( + (phrase) => phrase.category !== "tabulator", + ); + + let columnTitleToTranslationMapper = {}; + columnHeadingPhrases.forEach((phrase) => { + columnTitleToTranslationMapper[ + phrase.category + "/" + phrase.phrase + ] = phrase.text; + }); + + let columnFieldToTranslationMapper = {}; + tabulatorOptions.columns.forEach((column) => { + if (column.field === "collapse") return; + columnFieldToTranslationMapper[column.field] = + columnTitleToTranslationMapper[column.title]; + }); + + tabulatorOptions.langs[language].columns = + columnFieldToTranslationMapper; + }); + + return tabulatorOptions; + }, + async localizeCollapsedColumnHeadings() { + const columnHeadings = this.tabulator.getLang().columns; + + this.$refs.table + .querySelectorAll(".collapsedColumnHeading") + .forEach((collapsedColumnHeadingElement) => { + const field = collapsedColumnHeadingElement.getAttribute("field"); + if (!field?.length) return; + + collapsedColumnHeadingElement.innerHTML = + columnHeadings[field]; + }); + }, updateTabulator() { if (this.tabulator) { if (this.tableBuilt) diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 0476db2c9..fd3de09b8 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -58177,6 +58177,388 @@ I have been informed that I am under no obligation to consent to the transmissio ) ), // ### Phrases Dashboard Admin END + // generic tabulator phrases START + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'loading', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Loading', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'error', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Error', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'item', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'item', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'items', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'items', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'page_size', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Page Size', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'page_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Show Page', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'first', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'First', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'first_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'First Page', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'last', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Last', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'last_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Last Page', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'prev', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Prev', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'prev_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Prev Page', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'next', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Next', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'next_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Next Page', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'all', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'All', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'showing', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Showing', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'of', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'of', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'rows', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'rows', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'pages', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'pages', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + // generic tabulator phrases END ); From 40a0f6a7e9b684dddf2559b99966db07cad88391 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Wed, 6 May 2026 10:56:24 +0200 Subject: [PATCH 02/19] implemented tabulator localization --- public/js/components/Cis/Profil/MitarbeiterProfil.js | 7 ------- public/js/components/Cis/Profil/StudentProfil.js | 8 +------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/public/js/components/Cis/Profil/MitarbeiterProfil.js b/public/js/components/Cis/Profil/MitarbeiterProfil.js index cd40d0741..2332550b5 100644 --- a/public/js/components/Cis/Profil/MitarbeiterProfil.js +++ b/public/js/components/Cis/Profil/MitarbeiterProfil.js @@ -219,10 +219,6 @@ export default { } }); }, - setTableColumnTitles() { // reevaluates computed phrasen - if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setColumns(this.betriebsmittel_table_options.columns) - if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setColumns(this.funktionen_table_options.columns) - }, datetimeFormatterParams: function() { const params = { inputFormat:"yyyy-MM-dd", @@ -311,9 +307,6 @@ export default { 'data.mittel'(newVal) { if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setData(newVal); }, - 'language.value'(newVal) { - this.setTableColumnTitles() - } }, template: /*html*/ `
diff --git a/public/js/components/Cis/Profil/StudentProfil.js b/public/js/components/Cis/Profil/StudentProfil.js index b4debb3c3..ee7654b86 100644 --- a/public/js/components/Cis/Profil/StudentProfil.js +++ b/public/js/components/Cis/Profil/StudentProfil.js @@ -27,7 +27,7 @@ export default { FetchProfilUpdates, EditProfil, }, - inject: ["sortProfilUpdates", "collapseFunction", "language","isEditable"], + inject: ["sortProfilUpdates", "collapseFunction", "isEditable"], data() { return { showModal: false, @@ -245,12 +245,6 @@ export default { //? sorts the profil Updates: pending -> accepted -> rejected this.data.profilUpdates?.sort(this.sortProfilUpdates); }, - watch: { - 'language.value'(newVal) { - if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setColumns(this.betriebsmittel_table_options.columns) - if(this.$refs.zutrittsgruppenTable) this.$refs.zutrittsgruppenTable.tabulator.setColumns(this.zutrittsgruppen_table_options.columns) - } - }, template: /*html*/ `
Date: Tue, 12 May 2026 19:21:52 +0200 Subject: [PATCH 03/19] localizing column names accordion above table --- public/js/components/filter/Filter.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index b13cdd675..bc4525a22 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -192,14 +192,6 @@ export const CoreFilterCmpt = { return !def.frozen && def.title && def.formatter != "responsiveCollapse"; }).map(col => col.getField()); }, - fieldNames() { - if (!this.tableBuilt) - return {}; - return this.tabulator.getColumns().reduce((res, col) => { - res[col.getField()] = col.getDefinition().title; - return res; - }, {}); - }, idExtra() { if (!this.uuid) return ''; @@ -811,7 +803,18 @@ export const CoreFilterCmpt = { // parent not found return false; - } + }, + getColumnNames() { + if (!this.tableBuilt) { + return {}; + } else if (this.tabulator.getLocale()); { + return this.tabulator.getLang().columns; + } + return this.tabulator.getColumns().reduce((res, col) => { + res[col.getField()] = col.getDefinition().title; + return res; + }, {}); + }, }, beforeCreate() { if (!this.tableOnly == !this.filterType) @@ -889,7 +892,7 @@ export const CoreFilterCmpt = { :data-bs-parent="'#filterCollapsables' + idExtra" :fields="fieldIdsForVisibilty" :selected="selectedFields" - :names="fieldNames" + :names="getColumnNames()" @hide="tabulator.hideColumn($event)" @show="tabulator.showColumn($event)" v-collapse-auto-close From a3dd5a188b8a3b6be48d07898dc7aa82d52276e9 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Mon, 18 May 2026 11:39:03 +0200 Subject: [PATCH 04/19] removed redundant comment --- public/js/components/filter/Filter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index bc4525a22..ff2a62777 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -270,7 +270,6 @@ export const CoreFilterCmpt = { persistence: this.persistence, }, ...(this.tabulatorOptions || {})}; - // set up localizations if required if (tabulatorOptions.locale) { tabulatorOptions = await this.configureTabulatorLocalizations(tabulatorOptions); } From 95f09e911423639ec6ebf6af46292bb11c88e039 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Mon, 18 May 2026 14:07:06 +0200 Subject: [PATCH 05/19] fixed timing of initial localization of tabulator collapsed column headings --- public/js/components/Cis/Profil/Profil.js | 2 +- public/js/components/filter/Filter.js | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/public/js/components/Cis/Profil/Profil.js b/public/js/components/Cis/Profil/Profil.js index 37f3a8edb..0df4a86c2 100644 --- a/public/js/components/Cis/Profil/Profil.js +++ b/public/js/components/Cis/Profil/Profil.js @@ -24,7 +24,7 @@ Vue.$collapseFormatter = function (data) { let item2 = document.createElement("div"); item2.classList.add("col-6"); - item.innerHTML = "placeholder"; + item.innerHTML = "placeholder"; item2.innerHTML = col.value ? col.value : "-"; list.appendChild(item); diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index ff2a62777..720e832b9 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -215,7 +215,7 @@ export const CoreFilterCmpt = { this.tabulator.setLocale(newSelectedLanguage); if (this.$props.tabulatorOptions.responsiveLayoutCollapseFormatter) { - this.localizeCollapsedColumnHeadings(newSelectedLanguage); + this.localizeCollapsedColumnHeadings(); } }, }, @@ -316,9 +316,6 @@ export const CoreFilterCmpt = { this.tabulator.on('tableBuilt', () => { this.tableBuilt = true; this.$emit('tableBuilt'); - if (tabulatorOptions.locale && tabulatorOptions.responsiveLayoutCollapseFormatter) { - this.localizeCollapsedColumnHeadings(); - } }); this.tabulator.on("rowSelectionChanged", data => { this.selectedData = data; @@ -357,9 +354,6 @@ export const CoreFilterCmpt = { this.selectedFields = cols.filter(col => col.isVisible()).map(col => col.getField()); if (this.tabulator.options.persistence.headerFilter) this._setHeaderFilter(); - if (tabulatorOptions.locale && tabulatorOptions.responsiveLayoutCollapseFormatter) { - this.localizeCollapsedColumnHeadings(); - } }); } @@ -368,6 +362,12 @@ export const CoreFilterCmpt = { this.filterActive = filters.length > 0; this.$emit("headerFilterOn", this.filterActive); }); + + this.tabulator.on('renderComplete', () => { + if (tabulatorOptions.locale && tabulatorOptions.responsiveLayoutCollapseFormatter && this.tableBuilt) { + this.localizeCollapsedColumnHeadings(); + } + }); }, async configureTabulatorLocalizations(tabulatorOptions) { tabulatorOptions.locale = this.$p.user_language.value; @@ -485,12 +485,14 @@ export const CoreFilterCmpt = { return tabulatorOptions; }, async localizeCollapsedColumnHeadings() { - const columnHeadings = this.tabulator.getLang().columns; + const columnHeadings = this.tabulator?.getLang()?.columns; + if (!columnHeadings) return; this.$refs.table .querySelectorAll(".collapsedColumnHeading") .forEach((collapsedColumnHeadingElement) => { - const field = collapsedColumnHeadingElement.getAttribute("field"); + const field = + collapsedColumnHeadingElement.getAttribute("tabulator-column-field"); if (!field?.length) return; collapsedColumnHeadingElement.innerHTML = From ec4a04d63b648962e5770e99267b495204fceb45 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 19 May 2026 14:18:00 +0200 Subject: [PATCH 06/19] updated missing tabulator phrases in german --- system/phrasesupdate.php | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index fd3de09b8..8a68a43ff 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -58186,7 +58186,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'Lädt', 'description' => '', 'insertvon' => 'system' ), @@ -58206,7 +58206,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'Fehler', 'description' => '', 'insertvon' => 'system' ), @@ -58226,7 +58226,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'Element', 'description' => '', 'insertvon' => 'system' ), @@ -58246,7 +58246,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'Elemente', 'description' => '', 'insertvon' => 'system' ), @@ -58266,7 +58266,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'Seitengröße', 'description' => '', 'insertvon' => 'system' ), @@ -58286,7 +58286,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'Seite anzeigen', 'description' => '', 'insertvon' => 'system' ), @@ -58306,7 +58306,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'Erste', 'description' => '', 'insertvon' => 'system' ), @@ -58326,7 +58326,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'Erste Seite', 'description' => '', 'insertvon' => 'system' ), @@ -58346,7 +58346,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'Letzte', 'description' => '', 'insertvon' => 'system' ), @@ -58366,7 +58366,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'Letzte Seite', 'description' => '', 'insertvon' => 'system' ), @@ -58386,13 +58386,13 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'zurück', 'description' => '', 'insertvon' => 'system' ), array( 'sprache' => 'English', - 'text' => 'Prev', + 'text' => 'Previous', 'description' => '', 'insertvon' => 'system' ) @@ -58406,13 +58406,13 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'vorherige Seite', 'description' => '', 'insertvon' => 'system' ), array( 'sprache' => 'English', - 'text' => 'Prev Page', + 'text' => 'Previous Page', 'description' => '', 'insertvon' => 'system' ) @@ -58426,7 +58426,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'Nächste', 'description' => '', 'insertvon' => 'system' ), @@ -58446,7 +58446,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'Nächste Seite', 'description' => '', 'insertvon' => 'system' ), @@ -58466,7 +58466,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'Alle', 'description' => '', 'insertvon' => 'system' ), @@ -58486,7 +58486,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'Angezeigt', 'description' => '', 'insertvon' => 'system' ), @@ -58506,7 +58506,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'von', 'description' => '', 'insertvon' => 'system' ), @@ -58526,7 +58526,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'Zeilen', 'description' => '', 'insertvon' => 'system' ), @@ -58546,7 +58546,7 @@ I have been informed that I am under no obligation to consent to the transmissio 'phrases' => array( array( 'sprache' => 'German', - 'text' => '???', + 'text' => 'Seiten', 'description' => '', 'insertvon' => 'system' ), From 4e5bf94a15a1c689562b2a239b208bc65a72e72a Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 19 May 2026 14:33:09 +0200 Subject: [PATCH 07/19] identifying missing column heading phrases in localized tabulator --- public/js/components/filter/Filter.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index 720e832b9..8f3aa2130 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -475,9 +475,8 @@ export const CoreFilterCmpt = { tabulatorOptions.columns.forEach((column) => { if (column.field === "collapse") return; columnFieldToTranslationMapper[column.field] = - columnTitleToTranslationMapper[column.title]; + columnTitleToTranslationMapper[column.title] ?? "<< PHRASE " + column.title + " >>"; }); - tabulatorOptions.langs[language].columns = columnFieldToTranslationMapper; }); From 239eaa90e96611f81392496be17022f8c9a26543 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 19 May 2026 16:27:54 +0200 Subject: [PATCH 08/19] separate endpoint for generic tabulator phrases with cacheable response --- .../controllers/api/frontend/v1/Phrasen.php | 30 ++++++- public/js/api/factory/phrasen.js | 9 +++ public/js/components/filter/Filter.js | 81 ++++++++----------- 3 files changed, 73 insertions(+), 47 deletions(-) diff --git a/application/controllers/api/frontend/v1/Phrasen.php b/application/controllers/api/frontend/v1/Phrasen.php index 9bf68a989..f1f517ef7 100644 --- a/application/controllers/api/frontend/v1/Phrasen.php +++ b/application/controllers/api/frontend/v1/Phrasen.php @@ -32,7 +32,8 @@ class Phrasen extends FHCAPI_Controller 'setLanguage' => self::PERM_ANONYMOUS, 'getLanguage' => self::PERM_ANONYMOUS, 'getAllLanguages' => self::PERM_ANONYMOUS, - 'getPhrases' => self::PERM_ANONYMOUS + 'getPhrases' => self::PERM_ANONYMOUS, + 'getTabulatorPhrases' => self::PERM_ANONYMOUS, ]); $this->load->helper('hlp_language'); @@ -119,4 +120,31 @@ class Phrasen extends FHCAPI_Controller $this->terminateWithSuccess($result); } + public function getTabulatorPhrases() + { + $languages = json_decode($this->input->get('languages')); + if (!$languages || !count($languages)) { + $this->load->model('system/Sprache_model', 'sprachenModel'); + $activeLanguages = $this->sprachenModel->loadWhere(array('content' => true)); + $activeLanguagesData = $this->getDataOrTerminateWithError($activeLanguages); + $languages = array_map( + function ($languageData) { + return $languageData->sprache; + }, + $activeLanguagesData + ); + } + + $this->load->model('system/Phrase_model', 'phraseModel'); + $result = []; + foreach ($languages as $language) { + $tabulatorPhrases = $this->phraseModel->getPhrasesByCategoryAndLanguage(['tabulator'], $language); + $result[$language] = $this->getDataOrTerminateWithError($tabulatorPhrases); + } + + header('Pragma: private'); + header('Cache-Control: private, max-age=' . (60 * 60 * 24 * 30)); + $this->terminateWithSuccess($result); + } + } \ No newline at end of file diff --git a/public/js/api/factory/phrasen.js b/public/js/api/factory/phrasen.js index 127e64826..eafd18c4f 100644 --- a/public/js/api/factory/phrasen.js +++ b/public/js/api/factory/phrasen.js @@ -52,4 +52,13 @@ export default { }, }; }, + getTabulatorPhrases(languages = []) { + return { + method: 'get', + url: '/api/frontend/v1/phrasen/getTabulatorPhrases', + params: { + languages: JSON.stringify(languages), + }, + }; + }, }; \ No newline at end of file diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index 8f3aa2130..2c8cf59be 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -373,54 +373,14 @@ export const CoreFilterCmpt = { tabulatorOptions.locale = this.$p.user_language.value; tabulatorOptions.langs = {}; - let phrasesGroupedByCategoryRequestParam = { - tabulator: [ - "loading", - "error", - "item", - "items", - "page_size", - "page_title", - "first", - "first_title", - "last", - "last_title", - "prev", - "prev_title", - "next", - "next_title", - "all", - "showing", - "of", - "rows", - "pages", - ], - }; - tabulatorOptions.columns.forEach((column) => { - if (column.field === "collapse") return; - - let [category, phrase] = column.title.split("/"); - if (phrasesGroupedByCategoryRequestParam[category]) { - phrasesGroupedByCategoryRequestParam[category].push(phrase); - } else { - phrasesGroupedByCategoryRequestParam[category] = [phrase]; - } - }); - - const phrasesResponse = await this.$api.call( - ApiPhrases.getPhrases(phrasesGroupedByCategoryRequestParam), + const tabulatorPhrasesResponse = await this.$api.call( + ApiPhrases.getTabulatorPhrases(), ); - const phrasesData = phrasesResponse.data; - - Object.keys(phrasesData).forEach((language) => { - let genericTabulatorPhrases = phrasesData[language].filter( - (phrase) => phrase.category === "tabulator", - ); - + const tabulatorPhrasesData = tabulatorPhrasesResponse.data; + Object.keys(tabulatorPhrasesData).forEach((language) => { let tabulatorPhraseToTranslationMapper = {}; - genericTabulatorPhrases.forEach((phrase) => { - tabulatorPhraseToTranslationMapper[phrase.phrase] = - phrase.text; + tabulatorPhrasesData[language].forEach((phrase) => { + tabulatorPhraseToTranslationMapper[phrase.phrase] = phrase.text; }); tabulatorOptions.langs[language] = { @@ -459,6 +419,35 @@ export const CoreFilterCmpt = { }, }, }; + }); + + let phrasesGroupedByCategoryRequestParam = {}; + tabulatorOptions.columns.forEach((column) => { + if (column.field === "collapse") return; + + let [category, phrase] = column.title.split("/"); + if (phrasesGroupedByCategoryRequestParam[category]) { + phrasesGroupedByCategoryRequestParam[category].push(phrase); + } else { + phrasesGroupedByCategoryRequestParam[category] = [phrase]; + } + }); + + const phrasesResponse = await this.$api.call( + ApiPhrases.getPhrases(phrasesGroupedByCategoryRequestParam), + ); + const phrasesData = phrasesResponse.data; + + Object.keys(phrasesData).forEach((language) => { + let genericTabulatorPhrases = phrasesData[language].filter( + (phrase) => phrase.category === "tabulator", + ); + + let tabulatorPhraseToTranslationMapper = {}; + genericTabulatorPhrases.forEach((phrase) => { + tabulatorPhraseToTranslationMapper[phrase.phrase] = + phrase.text; + }); let columnHeadingPhrases = phrasesData[language].filter( (phrase) => phrase.category !== "tabulator", From e59bd51333fc458cd96421db30b9c1de0f93ca50 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 19 May 2026 16:35:22 +0200 Subject: [PATCH 09/19] modified getPhrases endpoint: replaced get w/ post, replaced query params w/ payload --- application/controllers/api/frontend/v1/Phrasen.php | 6 ++++-- public/js/api/factory/phrasen.js | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/application/controllers/api/frontend/v1/Phrasen.php b/application/controllers/api/frontend/v1/Phrasen.php index f1f517ef7..3e13bb61e 100644 --- a/application/controllers/api/frontend/v1/Phrasen.php +++ b/application/controllers/api/frontend/v1/Phrasen.php @@ -96,7 +96,9 @@ class Phrasen extends FHCAPI_Controller public function getPhrases() { - $languages = json_decode($this->input->get('languages')); + $postParams = $this->getPostJSON(); + + $languages = $postParams->languages; if (!$languages || !count($languages)) { $this->load->model('system/Sprache_model', 'sprachenModel'); $activeLanguages = $this->sprachenModel->loadWhere(array('content' => true)); @@ -110,7 +112,7 @@ class Phrasen extends FHCAPI_Controller } $this->load->model('system/Phrase_model', 'phraseModel'); - $phrasesGroupedByCategory = json_decode($this->input->get('phrasesGroupedByCategory')); + $phrasesGroupedByCategory = $postParams->phrasesGroupedByCategory; $result = []; foreach ($languages as $language) { $phrases = $this->phraseModel->getPhrasesByCategoryAndPhrasesAndLanguage($phrasesGroupedByCategory, $language); diff --git a/public/js/api/factory/phrasen.js b/public/js/api/factory/phrasen.js index eafd18c4f..1f8e772ba 100644 --- a/public/js/api/factory/phrasen.js +++ b/public/js/api/factory/phrasen.js @@ -44,11 +44,11 @@ export default { }, getPhrases(phrasesGroupedByCategory, languages = []) { return { - method: 'get', + method: 'post', url: '/api/frontend/v1/phrasen/getPhrases', params: { - phrasesGroupedByCategory: JSON.stringify(phrasesGroupedByCategory), - languages: JSON.stringify(languages), + phrasesGroupedByCategory, + languages, }, }; }, From b9670f3f099be6d908f28709febb6b93e05d203b Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Wed, 20 May 2026 16:30:16 +0200 Subject: [PATCH 10/19] localizing collapsed tabulator column headings; tabulator custom modules; debounce helper --- public/js/components/Cis/Profil/Profil.js | 2 +- public/js/components/filter/Filter.js | 46 +++++++++++------ public/js/helpers/DebounceHelper.js | 7 +++ public/js/helpers/StringHelpers.js | 49 ++++++++++++++++++- .../InternalToExternalEventBroadcastModule.js | 32 ++++++++++++ 5 files changed, 118 insertions(+), 18 deletions(-) create mode 100644 public/js/helpers/DebounceHelper.js create mode 100644 public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js diff --git a/public/js/components/Cis/Profil/Profil.js b/public/js/components/Cis/Profil/Profil.js index 0df4a86c2..5e656da77 100644 --- a/public/js/components/Cis/Profil/Profil.js +++ b/public/js/components/Cis/Profil/Profil.js @@ -24,7 +24,7 @@ Vue.$collapseFormatter = function (data) { let item2 = document.createElement("div"); item2.classList.add("col-6"); - item.innerHTML = "placeholder"; + item.innerHTML = "..."; item2.innerHTML = col.value ? col.value : "-"; list.appendChild(item); diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index 2c8cf59be..f25d07e7f 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -22,6 +22,9 @@ import TableDownload from './Table/Download.js'; import collapseAutoClose from '../../directives/collapseAutoClose.js'; import moduleLayoutFitDataStretchFrozen from '../../tabulator/layouts/fitDataStretchFrozen.js'; +import InternalToExternalEventBroadcastModule from "../../tabulator/customModules/InternalToExternalEventBroadcastModule.js" + +import { debounce } from "../../helpers/DebounceHelper.js"; import ApiFilter from '../../api/factory/filter.js'; import ApiPhrases from '../../api/factory/phrasen.js'; @@ -123,6 +126,27 @@ export const CoreFilterCmpt = { group: false, page: false, }, + collapsedHeadingLocalizationTimer: null, + localizeCollapsedColumnHeadings: + debounce( + () => { + const columnHeadings = this.tabulator?.getLang()?.columns; + if (!columnHeadings) return; + + this.$refs.table + .querySelectorAll(".collapsedColumnHeading") + .forEach((collapsedColumnHeadingElement) => { + const field = + collapsedColumnHeadingElement.getAttribute( + "tabulator-column-field", + ); + if (!field?.length) return; + + collapsedColumnHeadingElement.innerHTML = + columnHeadings[field]; + }); + }, 200) + , }; }, computed: { @@ -368,6 +392,11 @@ export const CoreFilterCmpt = { this.localizeCollapsedColumnHeadings(); } }); + this.tabulator.on('layoutRefreshed', () => { + if (tabulatorOptions.locale && tabulatorOptions.responsiveLayoutCollapseFormatter) { + this.localizeCollapsedColumnHeadings(); + } + }); }, async configureTabulatorLocalizations(tabulatorOptions) { tabulatorOptions.locale = this.$p.user_language.value; @@ -472,21 +501,6 @@ export const CoreFilterCmpt = { return tabulatorOptions; }, - async localizeCollapsedColumnHeadings() { - const columnHeadings = this.tabulator?.getLang()?.columns; - if (!columnHeadings) return; - - this.$refs.table - .querySelectorAll(".collapsedColumnHeading") - .forEach((collapsedColumnHeadingElement) => { - const field = - collapsedColumnHeadingElement.getAttribute("tabulator-column-field"); - if (!field?.length) return; - - collapsedColumnHeadingElement.innerHTML = - columnHeadings[field]; - }); - }, updateTabulator() { if (this.tabulator) { if (this.tableBuilt) @@ -814,6 +828,8 @@ export const CoreFilterCmpt = { alert('"nwNewEntry" listener is mandatory when sideMenu is true'); this.uuid = _uuid++; this.$emit('uuidDefined', this.uuid) + + Tabulator.registerModule(InternalToExternalEventBroadcastModule); }, mounted() { this.initTabulator().then(() => { diff --git a/public/js/helpers/DebounceHelper.js b/public/js/helpers/DebounceHelper.js new file mode 100644 index 000000000..af5e80019 --- /dev/null +++ b/public/js/helpers/DebounceHelper.js @@ -0,0 +1,7 @@ +export const debounce = (callback, wait) => { + let timeoutId = null; + return (context) => { + window.clearTimeout(timeoutId); + timeoutId = window.setTimeout(() => callback(), wait); + } +} \ No newline at end of file diff --git a/public/js/helpers/StringHelpers.js b/public/js/helpers/StringHelpers.js index a67d0138f..e68d1d381 100644 --- a/public/js/helpers/StringHelpers.js +++ b/public/js/helpers/StringHelpers.js @@ -1,4 +1,49 @@ export function capitalize(string) { - if (!string) return ''; + if (!string) return ""; return string[0].toUpperCase() + string.slice(1); -} \ No newline at end of file +} + +export function convertCase(string, from, to) { + const possibleCases = ["kebab", "snake", "camel"]; + if ( + from === to || + !possibleCases.includes(from) || + !possibleCases.includes(to) + ) + return string; + + const individualWords = + from === "kebab" + ? string.split("-") + : from === "snake" + ? string.split("_") + : splitWordsInCamelCase(string); + + const formattedIndividualWords = ["snake", "kebab"].includes(to) + ? individualWords.map((word) => word.toLowerCase()) + : individualWords.map((word, index) => { + if (!index) return word.toLowerCase(); + return word.slice(0, 1).toUpperCase() + word.slice(1).toLowerCase(); + }); + + const joinCharacter = to === "snake" ? "_" : to === "kebab" ? "-" : ""; + return formattedIndividualWords.join(joinCharacter); +} + +function splitWordsInCamelCase(string) { + const lastUpperCaseLetterIndex = string + .split("") + .findLastIndex((character) => { + return character.toUpperCase() === character; + }); + + if (lastUpperCaseLetterIndex < 1) { + return [string]; + } + + let splitWords = splitWordsInCamelCase( + string.slice(0, lastUpperCaseLetterIndex), + ); + splitWords.push(string.slice(lastUpperCaseLetterIndex)); + return splitWords; +} diff --git a/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js b/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js new file mode 100644 index 000000000..85099d9ce --- /dev/null +++ b/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js @@ -0,0 +1,32 @@ +// changes within tabulator are often not adequately broadcast to the external component +// this module exposes tabulator's internal events +// if external event name is not specified, the dispatched external event will be the camelCase version of the kebab-case internal event + +import Module from "../../../../vendor/olifolkerd/tabulator5/src/js/core/Module.js"; +import { convertCase } from "../../helpers/StringHelpers.js"; + +export default class InternalToExternalEventBroadcastModule extends Module { + static moduleName = "internalToExternalEventBroadcastModule"; + static moduleInitNumber = 1; + + constructor(table) { + super(table); + + this.eventsToBroadcast = [ + { + internal: "layout-refreshed", + external: null, + }, + ]; + } + + initialize() { + this.eventsToBroadcast.forEach((event) => { + this.subscribe(event.internal, () => { + this.dispatchExternal(event.external ?? convertCase(event.internal, "kebab", "camel")); + }); + }); + } + + +} From cf506a7b1eecb383e732c3d094dae1172964a0cc Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Wed, 20 May 2026 17:04:05 +0200 Subject: [PATCH 11/19] reused debounce helper in searchbar search --- public/js/components/searchbar/searchbar.js | 205 +++++++++++--------- 1 file changed, 111 insertions(+), 94 deletions(-) diff --git a/public/js/components/searchbar/searchbar.js b/public/js/components/searchbar/searchbar.js index d6c93198e..34060239c 100644 --- a/public/js/components/searchbar/searchbar.js +++ b/public/js/components/searchbar/searchbar.js @@ -8,6 +8,7 @@ import dms from "./result/dms.js"; import cms from "./result/cms.js"; import mergedStudent from "./result/mergedstudent.js"; import mergedPerson from "./result/mergedperson.js"; +import { debounce } from "../../helpers/DebounceHelper.js"; export default { name: "FhcSearchbar", @@ -52,9 +53,114 @@ export default { showresult: false, searching: false, error: null, - abortController: null, - settingsDropdown: null, - lastQuery: '' + abortController: null, + settingsDropdown: null, + lastQuery: '', + callsearchApi: debounce(() => { + this.error = null; + this.searchresult.splice(0, this.searchresult.length); + this.searching = true; + this.showsearchresult(); + if (this.searchsettings.types.length === 0) { + this.error = this.$p.t("search/error_missing_type"); + this.searching = false; + return; + } + + if (this.abortController) this.abortController.abort(); + this.abortController = new AbortController(); + + this.searchfunction(this.searchsettings, { + timeout: 50000, + signal: this.abortController.signal, + }) + .then((response) => { + if (!response.data) { + this.error = this.$p.t("search/error_general"); + } else { + let res = response.data.map((el) => + el.data + ? { ...el, ...JSON.parse(el.data) } + : el, + ); + this.lastQuery = response.meta.searchstring; + if (this.searchoptions.mergeResults) { + let counter = 0; + let mergeTypes = []; + let mergedType = "merged-"; + let mergeKey = ""; + + switch (this.searchoptions.mergeResults) { + case "student": + mergeTypes = ["student", "prestudent"]; + mergedType += + this.searchoptions.mergeResults; + mergeKey = "uid"; + break; + case "person": + mergeTypes = [ + "person", + "employee", + "student", + "prestudent", + ]; + mergedType += + this.searchoptions.mergeResults; + mergeKey = "person_id"; + break; + } + + if (mergeTypes.length) { + res = Object.values( + res.reduce((a, c) => { + if ( + !mergeTypes.includes(c.renderer) + ) { + a["nomerge" + counter++] = c; + } else if (c[mergeKey] === null) { + a["nomerge" + counter++] = c; + } else if ( + a[c[mergeKey]] === undefined + ) { + a[c[mergeKey]] = { + rank: c.rank, + renderer: mergedType, + type: mergedType, + list: [c], + }; + } else { + a[c[mergeKey]].list.push(c); + if ( + c.rank > a[c[mergeKey]].rank + ) + a[c[mergeKey]].rank = + c.rank; + } + return a; + }, {}), + ).sort((a, b) => b.rank - a.rank); + } + } + this.searchresult = res; + this.searchmode = response.meta.mode; + } + this.searching = false; + this.retry = 0; + }) + .catch((error) => { + if (error.code == "ERR_CANCELED") { + return (this.retry = 0); + } + if (error.code == "ECONNABORTED" && this.retry) { + this.retry--; + return this.callsearchapi(); + } + + this.error = this.$p.t("search/error_general", error); + this.searching = false; + this.retry = 0; + }); + }, 500), }; }, computed: { @@ -309,107 +415,18 @@ export default { this.abort(); if( this.searchsettings.searchstr.length >= 2 ) { this.calcSearchResultExtent(); - this.searchtimer = setTimeout( - this.callsearchapi, - 500 - ); + this.callsearchApi(); } else { this.showresult = false; } }, abort() { - if (this.searchtimer !== null) { - clearTimeout(this.searchtimer); - } - if (this.abortController) { + if (this.abortController) { this.abortController.abort(); this.abortController = null; } this.searchresult = []; }, - callsearchapi: function() { - this.error = null; - this.searchresult.splice(0, this.searchresult.length); - this.searching = true; - this.showsearchresult(); - if(this.searchsettings.types.length === 0) { - this.error = this.$p.t('search/error_missing_type'); - this.searching = false; - return; - } - - if (this.abortController) - this.abortController.abort(); - this.abortController = new AbortController(); - - this.searchfunction(this.searchsettings, { timeout: 50000, signal: this.abortController.signal }) - .then(response=>{ - if (!response.data) { - this.error = this.$p.t('search/error_general'); - } else { - let res = response.data.map(el => el.data ? {...el, ...JSON.parse(el.data)} : el); - this.lastQuery = response.meta.searchstring; - if (this.searchoptions.mergeResults) { - let counter = 0; - let mergeTypes = []; - let mergedType = 'merged-'; - let mergeKey = ''; - - switch (this.searchoptions.mergeResults) { - case 'student': - mergeTypes = ['student', 'prestudent']; - mergedType += this.searchoptions.mergeResults; - mergeKey = 'uid'; - break; - case 'person': - mergeTypes = ['person', 'employee', 'student', 'prestudent']; - mergedType += this.searchoptions.mergeResults; - mergeKey = 'person_id'; - break; - } - - if (mergeTypes.length) { - res = Object.values(res.reduce((a, c) => { - if (!mergeTypes.includes(c.renderer)) { - a['nomerge' + counter++] = c; - } else if (c[mergeKey] === null) { - a['nomerge' + counter++] = c; - } else if (a[c[mergeKey]] === undefined) { - a[c[mergeKey]] = { - rank: c.rank, - renderer: mergedType, - type: mergedType, - list: [c] - }; - } else { - a[c[mergeKey]].list.push(c); - if (c.rank > a[c[mergeKey]].rank) - a[c[mergeKey]].rank = c.rank; - } - return a; - }, {})).sort((a, b) => b.rank - a.rank); - } - } - this.searchresult = res; - this.searchmode = response.meta.mode; - } - this.searching = false; - this.retry = 0; - }) - .catch(error=> { - if (error.code == "ERR_CANCELED") { - return this.retry = 0; - } - if (error.code == "ECONNABORTED" && this.retry) { - this.retry--; - return this.callsearchapi(); - } - - this.error = this.$p.t('search/error_general', error); - this.searching = false; - this.retry = 0; - }); - }, refreshsearch: function() { this.search(); this.togglesettings(); From 0cc628a8e36a056e711b1e9a22373158c2757a25 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Wed, 20 May 2026 17:06:05 +0200 Subject: [PATCH 12/19] fixed issue where searchbar was producing an error searching with an empty input --- public/js/components/searchbar/searchbar.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/js/components/searchbar/searchbar.js b/public/js/components/searchbar/searchbar.js index 34060239c..1d9629b68 100644 --- a/public/js/components/searchbar/searchbar.js +++ b/public/js/components/searchbar/searchbar.js @@ -70,6 +70,8 @@ export default { if (this.abortController) this.abortController.abort(); this.abortController = new AbortController(); + if (!this.getSearchStr()?.length) return; + this.searchfunction(this.searchsettings, { timeout: 50000, signal: this.abortController.signal, From a704ce215b5674fef4cdd343bce0a143fe11b3f3 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Wed, 20 May 2026 17:08:31 +0200 Subject: [PATCH 13/19] minor mod --- public/js/components/searchbar/searchbar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/components/searchbar/searchbar.js b/public/js/components/searchbar/searchbar.js index 1d9629b68..5d4c4616d 100644 --- a/public/js/components/searchbar/searchbar.js +++ b/public/js/components/searchbar/searchbar.js @@ -70,7 +70,7 @@ export default { if (this.abortController) this.abortController.abort(); this.abortController = new AbortController(); - if (!this.getSearchStr()?.length) return; + if (!this.searchsettings.searchstr?.length) return; this.searchfunction(this.searchsettings, { timeout: 50000, From ca712781dcace73a510bed296cfe2288b4f7f65c Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Sat, 23 May 2026 14:28:14 +0200 Subject: [PATCH 14/19] localizing tabulator dropdown/context menus --- .../Cis/Profil/MitarbeiterProfil.js | 16 +- public/js/components/filter/Filter.js | 151 +++++++++++------- public/js/components/filter/Filter/Columns.js | 2 +- .../InternalToExternalEventBroadcastModule.js | 4 +- .../customModules/MenuExtensionModule.js | 25 +++ 5 files changed, 124 insertions(+), 74 deletions(-) create mode 100644 public/js/tabulator/customModules/MenuExtensionModule.js diff --git a/public/js/components/Cis/Profil/MitarbeiterProfil.js b/public/js/components/Cis/Profil/MitarbeiterProfil.js index 2332550b5..55a52b5c3 100644 --- a/public/js/components/Cis/Profil/MitarbeiterProfil.js +++ b/public/js/components/Cis/Profil/MitarbeiterProfil.js @@ -59,21 +59,21 @@ export default { visible: true }, { - title: "ui/bezeichnung", + titlePhrase: "ui/bezeichnung", field: "Bezeichnung", headerFilter: true, minWidth: 200, visible: true }, { - title: "lehre/organisationseinheit", + titlePhrase: "lehre/organisationseinheit", field: "Organisationseinheit", headerFilter: true, minWidth: 200, visible: true }, { - title: "global/gueltigVon", + titlePhrase: "global/gueltigVon", field: "Gültig_von", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -84,7 +84,7 @@ export default { formatterParams: this.datetimeFormatterParams() }, { - title: "global/gueltigBis", + titlePhrase: "global/gueltigBis", field: "Gültig_bis", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -95,7 +95,7 @@ export default { formatterParams: this.datetimeFormatterParams() }, { - title: "profil/wochenstunden", + titlePhrase: "profil/wochenstunden", field: "Wochenstunden", headerFilter: true, minWidth: 200, @@ -129,14 +129,14 @@ export default { visible: true }, { - title: "profil/entlehnteBetriebsmittel", + titlePhrase: "profil/entlehnteBetriebsmittel", field: "betriebsmittel", headerFilter: true, minWidth: 200, visible: true }, { - title: "profil/inventarnummer", + titlePhrase: "profil/inventarnummer", field: "Nummer", headerFilter: true, resizable: true, @@ -144,7 +144,7 @@ export default { visible: true }, { - title: "profil/ausgabedatum", + titlePhrase: "profil/ausgabedatum", field: "Ausgegeben_am", headerFilterFunc: 'dates', headerFilter: dateFilter, diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index f25d07e7f..8ae31472e 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -23,6 +23,7 @@ import collapseAutoClose from '../../directives/collapseAutoClose.js'; import moduleLayoutFitDataStretchFrozen from '../../tabulator/layouts/fitDataStretchFrozen.js'; import InternalToExternalEventBroadcastModule from "../../tabulator/customModules/InternalToExternalEventBroadcastModule.js" +import MenuExtensionModule from "../../tabulator/customModules/MenuExtensionModule.js" import { debounce } from "../../helpers/DebounceHelper.js"; @@ -131,7 +132,7 @@ export const CoreFilterCmpt = { debounce( () => { const columnHeadings = this.tabulator?.getLang()?.columns; - if (!columnHeadings) return; + if (!columnHeadings?.length) return; this.$refs.table .querySelectorAll(".collapsedColumnHeading") @@ -209,12 +210,25 @@ export const CoreFilterCmpt = { return columns; }, fieldIdsForVisibilty() { - if (!this.tableBuilt) - return []; - return this.tabulator.getColumns().filter(col => { - let def = col.getDefinition(); - return !def.frozen && def.title && def.formatter != "responsiveCollapse"; - }).map(col => col.getField()); + if (!this.tableBuilt) return []; + + const localizedColumnTitles = this.tabulator.getLang().columns; + const isTabulatorLocalized = !!this.$props.tabulatorOptions.locale; + return this.tabulator + .getColumns() + .filter((col) => { + let def = col.getDefinition(); + let title = + isTabulatorLocalized && localizedColumnTitles[def.field] + ? localizedColumnTitles[def.field] + : def.title; + return ( + !def.frozen && + title && + def.formatter != "responsiveCollapse" + ); + }) + .map((col) => col.getField()); }, idExtra() { if (!this.uuid) @@ -222,14 +236,21 @@ export const CoreFilterCmpt = { return '-' + this.uuid; }, columnsForFilter() { - if (!this.filteredColumns || !this.datasetMetadata) - return []; - const filterTitles = this.filteredColumns.reduce((a,c) => { - a[c.field] = c.title; + if (!this.filteredColumns || !this.datasetMetadata) return []; + const localizedColumnTitles = this.tabulator.getLang().columns; + const isTabulatorLocalized = !!this.$props.tabulatorOptions.locale; + const filterTitles = this.filteredColumns.reduce((a, c) => { + a[c.field] = + isTabulatorLocalized && localizedColumnTitles[c.field] + ? localizedColumnTitles[c.field] + : c.title; return a; }, {}); - return this.datasetMetadata.map(el => ({...el, ...{title: filterTitles[el.name]}})); - } + return this.datasetMetadata.map((el) => ({ + ...el, + ...{ title: filterTitles[el.name] }, + })); + }, }, watch: { "language.value": { @@ -387,12 +408,12 @@ export const CoreFilterCmpt = { this.$emit("headerFilterOn", this.filterActive); }); - this.tabulator.on('renderComplete', () => { + this.tabulator.on("renderComplete", () => { if (tabulatorOptions.locale && tabulatorOptions.responsiveLayoutCollapseFormatter && this.tableBuilt) { this.localizeCollapsedColumnHeadings(); } }); - this.tabulator.on('layoutRefreshed', () => { + this.tabulator.on("layoutRefreshed", () => { if (tabulatorOptions.locale && tabulatorOptions.responsiveLayoutCollapseFormatter) { this.localizeCollapsedColumnHeadings(); } @@ -402,49 +423,49 @@ export const CoreFilterCmpt = { tabulatorOptions.locale = this.$p.user_language.value; tabulatorOptions.langs = {}; - const tabulatorPhrasesResponse = await this.$api.call( + const genericTabulatorPhrasesResponse = await this.$api.call( ApiPhrases.getTabulatorPhrases(), ); - const tabulatorPhrasesData = tabulatorPhrasesResponse.data; - Object.keys(tabulatorPhrasesData).forEach((language) => { - let tabulatorPhraseToTranslationMapper = {}; - tabulatorPhrasesData[language].forEach((phrase) => { - tabulatorPhraseToTranslationMapper[phrase.phrase] = phrase.text; + const genericTabulatorPhrasesData = genericTabulatorPhrasesResponse.data; + Object.keys(genericTabulatorPhrasesData).forEach((language) => { + let genericTabulatorPhraseToTranslationMapper = {}; + genericTabulatorPhrasesData[language].forEach((phrase) => { + genericTabulatorPhraseToTranslationMapper[phrase.phrase] = phrase.text; }); tabulatorOptions.langs[language] = { data: { - loading: tabulatorPhraseToTranslationMapper["loading"], - error: tabulatorPhraseToTranslationMapper["error"], + loading: genericTabulatorPhraseToTranslationMapper["loading"], + error: genericTabulatorPhraseToTranslationMapper["error"], }, groups: { - item: tabulatorPhraseToTranslationMapper["item"], - items: tabulatorPhraseToTranslationMapper["items"], + item: genericTabulatorPhraseToTranslationMapper["item"], + items: genericTabulatorPhraseToTranslationMapper["items"], }, pagination: { page_size: - tabulatorPhraseToTranslationMapper["page_size"], + genericTabulatorPhraseToTranslationMapper["page_size"], page_title: - tabulatorPhraseToTranslationMapper["page_title"], - first: tabulatorPhraseToTranslationMapper["first"], + genericTabulatorPhraseToTranslationMapper["page_title"], + first: genericTabulatorPhraseToTranslationMapper["first"], first_title: - tabulatorPhraseToTranslationMapper["first_title"], - last: tabulatorPhraseToTranslationMapper["last"], + genericTabulatorPhraseToTranslationMapper["first_title"], + last: genericTabulatorPhraseToTranslationMapper["last"], last_title: - tabulatorPhraseToTranslationMapper["last_title"], - prev: tabulatorPhraseToTranslationMapper["prev"], + genericTabulatorPhraseToTranslationMapper["last_title"], + prev: genericTabulatorPhraseToTranslationMapper["prev"], prev_title: - tabulatorPhraseToTranslationMapper["prev_title"], - next: tabulatorPhraseToTranslationMapper["next"], + genericTabulatorPhraseToTranslationMapper["prev_title"], + next: genericTabulatorPhraseToTranslationMapper["next"], next_title: - tabulatorPhraseToTranslationMapper["next_title"], - all: tabulatorPhraseToTranslationMapper["all"], + genericTabulatorPhraseToTranslationMapper["next_title"], + all: genericTabulatorPhraseToTranslationMapper["all"], counter: { showing: - tabulatorPhraseToTranslationMapper["showing"], - of: tabulatorPhraseToTranslationMapper["of"], - rows: tabulatorPhraseToTranslationMapper["rows"], - pages: tabulatorPhraseToTranslationMapper["pages"], + genericTabulatorPhraseToTranslationMapper["showing"], + of: genericTabulatorPhraseToTranslationMapper["of"], + rows: genericTabulatorPhraseToTranslationMapper["rows"], + pages: genericTabulatorPhraseToTranslationMapper["pages"], }, }, }; @@ -452,9 +473,17 @@ export const CoreFilterCmpt = { let phrasesGroupedByCategoryRequestParam = {}; tabulatorOptions.columns.forEach((column) => { - if (column.field === "collapse") return; + if (!column.titlePhrase?.length) return; - let [category, phrase] = column.title.split("/"); + let [category, phrase] = column.titlePhrase.split("/"); + if (phrasesGroupedByCategoryRequestParam[category]) { + phrasesGroupedByCategoryRequestParam[category].push(phrase); + } else { + phrasesGroupedByCategoryRequestParam[category] = [phrase]; + } + }); + tabulatorOptions.menuItemPhrases?.forEach((menuItemPhrase) => { + let [category, phrase] = menuItemPhrase.split("/"); if (phrasesGroupedByCategoryRequestParam[category]) { phrasesGroupedByCategoryRequestParam[category].push(phrase); } else { @@ -468,35 +497,32 @@ export const CoreFilterCmpt = { const phrasesData = phrasesResponse.data; Object.keys(phrasesData).forEach((language) => { - let genericTabulatorPhrases = phrasesData[language].filter( - (phrase) => phrase.category === "tabulator", - ); - - let tabulatorPhraseToTranslationMapper = {}; - genericTabulatorPhrases.forEach((phrase) => { - tabulatorPhraseToTranslationMapper[phrase.phrase] = - phrase.text; - }); - - let columnHeadingPhrases = phrasesData[language].filter( - (phrase) => phrase.category !== "tabulator", - ); - - let columnTitleToTranslationMapper = {}; - columnHeadingPhrases.forEach((phrase) => { - columnTitleToTranslationMapper[ + let phraseToTranslationMapper = {}; + phrasesData[language].forEach((phrase) => { + phraseToTranslationMapper[ phrase.category + "/" + phrase.phrase ] = phrase.text; }); let columnFieldToTranslationMapper = {}; tabulatorOptions.columns.forEach((column) => { - if (column.field === "collapse") return; + if (!column.titlePhrase?.length) return; + columnFieldToTranslationMapper[column.field] = - columnTitleToTranslationMapper[column.title] ?? "<< PHRASE " + column.title + " >>"; + phraseToTranslationMapper[column.titlePhrase] ?? + "<< PHRASE " + column.titlePhrase + " >>"; }); tabulatorOptions.langs[language].columns = columnFieldToTranslationMapper; + + let menuItemPhraseToTranslationMapper = {}; + tabulatorOptions.menuItemPhrases?.forEach((menuItemPhrase) => { + menuItemPhraseToTranslationMapper[menuItemPhrase] = + phraseToTranslationMapper[menuItemPhrase] ?? + "<< PHRASE " + menuItemPhrase + " >>"; + }); + tabulatorOptions.langs[language].menuItems = + menuItemPhraseToTranslationMapper; }); return tabulatorOptions; @@ -810,7 +836,7 @@ export const CoreFilterCmpt = { getColumnNames() { if (!this.tableBuilt) { return {}; - } else if (this.tabulator.getLocale()); { + } else if (this.tabulator.options.locale); { return this.tabulator.getLang().columns; } return this.tabulator.getColumns().reduce((res, col) => { @@ -830,6 +856,7 @@ export const CoreFilterCmpt = { this.$emit('uuidDefined', this.uuid) Tabulator.registerModule(InternalToExternalEventBroadcastModule); + Tabulator.registerModule(MenuExtensionModule); }, mounted() { this.initTabulator().then(() => { diff --git a/public/js/components/filter/Filter/Columns.js b/public/js/components/filter/Filter/Columns.js index be29e5f19..75bf2abab 100644 --- a/public/js/components/filter/Filter/Columns.js +++ b/public/js/components/filter/Filter/Columns.js @@ -42,7 +42,7 @@ export default { watch: { selected(n) { this.selectedFields = n; - } + }, }, methods: { toggle(field) { diff --git a/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js b/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js index 85099d9ce..6bbb58d13 100644 --- a/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js +++ b/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js @@ -6,7 +6,7 @@ import Module from "../../../../vendor/olifolkerd/tabulator5/src/js/core/Module. import { convertCase } from "../../helpers/StringHelpers.js"; export default class InternalToExternalEventBroadcastModule extends Module { - static moduleName = "internalToExternalEventBroadcastModule"; + static moduleName = "internalToExternalEventBroadcast"; static moduleInitNumber = 1; constructor(table) { @@ -27,6 +27,4 @@ export default class InternalToExternalEventBroadcastModule extends Module { }); }); } - - } diff --git a/public/js/tabulator/customModules/MenuExtensionModule.js b/public/js/tabulator/customModules/MenuExtensionModule.js new file mode 100644 index 000000000..18df92e3d --- /dev/null +++ b/public/js/tabulator/customModules/MenuExtensionModule.js @@ -0,0 +1,25 @@ +import Menu from "../../../../vendor/olifolkerd/tabulator5/src/js/modules/Menu/Menu.js"; + +export default class MenuExtensionModule extends Menu { + static moduleName = "menu"; + static moduleInitNumber = 1; + + constructor(table) { + super(table); + } + + loadMenu(e, component, menu, parentEl, parentPopup){ + const isLocalizationEnabled = !!component.table.options.locale; + const menuItemTranslations = component.table.getLang().menuItems; + if (isLocalizationEnabled && menuItemTranslations) { + menu = menu.map((menuItem) => { + if (menuItem.phrase && menuItemTranslations[menuItem.phrase]) { + menuItem.label = menuItemTranslations[menuItem.phrase]; + } + return menuItem; + }); + } + + super.loadMenu(e, component, menu, parentEl, parentPopup); + } +} From 3483146c1a50f964e4950a114999746aff67175b Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Sat, 23 May 2026 15:53:38 +0200 Subject: [PATCH 15/19] resolved tabulator warnings --- public/js/components/filter/Filter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index 8ae31472e..1b15666a9 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -347,10 +347,10 @@ export const CoreFilterCmpt = { } // Start the tabulator with the build options - this.tabulator = new Tabulator( - this.$refs.table, - tabulatorOptions - ); + this.tabulator = new Tabulator(this.$refs.table, { + ...tabulatorOptions, + debugInvalidOptions: false, + }); // If event handlers have been provided if (Array.isArray(this.tabulatorEvents) && this.tabulatorEvents.length > 0) { From aeebc2a81a07e3fb586d28661d8f0a38baf24fc8 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 26 May 2026 14:25:22 +0200 Subject: [PATCH 16/19] reworked localization of collapsed column headings by modifying internal tabulator logic with custom module --- .../Cis/Profil/MitarbeiterProfil.js | 8 ++++ .../Cis/Profil/MitarbeiterViewProfil.js | 15 ++++--- public/js/components/Cis/Profil/Profil.js | 2 +- .../js/components/Cis/Profil/StudentProfil.js | 12 +++-- public/js/components/filter/Filter.js | 45 +++++-------------- .../InternalToExternalEventBroadcastModule.js | 9 ++-- .../customModules/MenuExtensionModule.js | 2 +- .../ResponsiveLayoutExtensionModule.js | 27 +++++++++++ 8 files changed, 70 insertions(+), 50 deletions(-) create mode 100644 public/js/tabulator/customModules/ResponsiveLayoutExtensionModule.js diff --git a/public/js/components/Cis/Profil/MitarbeiterProfil.js b/public/js/components/Cis/Profil/MitarbeiterProfil.js index 55a52b5c3..ed63850c7 100644 --- a/public/js/components/Cis/Profil/MitarbeiterProfil.js +++ b/public/js/components/Cis/Profil/MitarbeiterProfil.js @@ -59,6 +59,7 @@ export default { visible: true }, { + title: "placeholder", titlePhrase: "ui/bezeichnung", field: "Bezeichnung", headerFilter: true, @@ -66,6 +67,7 @@ export default { visible: true }, { + title: "placeholder", titlePhrase: "lehre/organisationseinheit", field: "Organisationseinheit", headerFilter: true, @@ -73,6 +75,7 @@ export default { visible: true }, { + title: "placeholder", titlePhrase: "global/gueltigVon", field: "Gültig_von", headerFilterFunc: 'dates', @@ -84,6 +87,7 @@ export default { formatterParams: this.datetimeFormatterParams() }, { + title: "placeholder", titlePhrase: "global/gueltigBis", field: "Gültig_bis", headerFilterFunc: 'dates', @@ -95,6 +99,7 @@ export default { formatterParams: this.datetimeFormatterParams() }, { + title: "placeholder", titlePhrase: "profil/wochenstunden", field: "Wochenstunden", headerFilter: true, @@ -129,6 +134,7 @@ export default { visible: true }, { + title: "placeholder", titlePhrase: "profil/entlehnteBetriebsmittel", field: "betriebsmittel", headerFilter: true, @@ -136,6 +142,7 @@ export default { visible: true }, { + title: "placeholder", titlePhrase: "profil/inventarnummer", field: "Nummer", headerFilter: true, @@ -144,6 +151,7 @@ export default { visible: true }, { + title: "placeholder", titlePhrase: "profil/ausgabedatum", field: "Ausgegeben_am", headerFilterFunc: 'dates', diff --git a/public/js/components/Cis/Profil/MitarbeiterViewProfil.js b/public/js/components/Cis/Profil/MitarbeiterViewProfil.js index 78dc53ffc..8670a2808 100644 --- a/public/js/components/Cis/Profil/MitarbeiterViewProfil.js +++ b/public/js/components/Cis/Profil/MitarbeiterViewProfil.js @@ -46,21 +46,24 @@ export default { visible: true }, { - title: "ui/bezeichnung", + title: "placeholder", + titlePhrase: "ui/bezeichnung", field: "Bezeichnung", headerFilter: true, minWidth: 200, visible: true }, { - title: "lehre/organisationseinheit", + title: "placeholder", + titlePhrase: "lehre/organisationseinheit", field: "Organisationseinheit", headerFilter: true, minWidth: 200, visible: true }, { - title: "global/gueltigVon", + title: "placeholder", + titlePhrase: "global/gueltigVon", field: "Gültig_von", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -71,7 +74,8 @@ export default { formatterParams: this.datetimeFormatterParams() }, { - title: "global/gueltigBis", + title: "placeholder", + titlePhrase: "global/gueltigBis", field: "Gültig_bis", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -82,7 +86,8 @@ export default { formatterParams: this.datetimeFormatterParams() }, { - title: "profil/wochenstunden", + title: "placeholder", + titlePhrase: "profil/wochenstunden", field: "Wochenstunden", headerFilter: true, minWidth: 200, diff --git a/public/js/components/Cis/Profil/Profil.js b/public/js/components/Cis/Profil/Profil.js index 5e656da77..da205816d 100644 --- a/public/js/components/Cis/Profil/Profil.js +++ b/public/js/components/Cis/Profil/Profil.js @@ -24,7 +24,7 @@ Vue.$collapseFormatter = function (data) { let item2 = document.createElement("div"); item2.classList.add("col-6"); - item.innerHTML = "..."; + item.innerHTML = "" + col.title + ""; item2.innerHTML = col.value ? col.value : "-"; list.appendChild(item); diff --git a/public/js/components/Cis/Profil/StudentProfil.js b/public/js/components/Cis/Profil/StudentProfil.js index ee7654b86..f448d01b1 100644 --- a/public/js/components/Cis/Profil/StudentProfil.js +++ b/public/js/components/Cis/Profil/StudentProfil.js @@ -42,7 +42,8 @@ export default { minHeight: 200, layout: "fitColumns", columns: [{ - title: "profil/zutrittsGruppen", + title: "placeholder", + titlePhrase: "profil/zutrittsGruppen", field: "bezeichnung" }], }, @@ -69,14 +70,16 @@ export default { headerClick: this.collapseFunction, }, { - title: "profil/entlehnteBetriebsmittel", + title: "placeholder", + titlePhrase: "profil/entlehnteBetriebsmittel", field: "betriebsmittel", headerFilter: true, minWidth: 200, visible: true }, { - title: "profil/inventarnummer", + title: "placeholder", + titlePhrase: "profil/inventarnummer", field: "Nummer", headerFilter: true, resizable: true, @@ -84,7 +87,8 @@ export default { visible: true }, { - title: "profil/ausgabedatum", + title: "placeholder", + titlePhrase: "profil/ausgabedatum", field: "Ausgegeben_am", headerFilterFunc: 'dates', headerFilter: dateFilter, diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index 1b15666a9..77ad648bd 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -24,6 +24,7 @@ import collapseAutoClose from '../../directives/collapseAutoClose.js'; import moduleLayoutFitDataStretchFrozen from '../../tabulator/layouts/fitDataStretchFrozen.js'; import InternalToExternalEventBroadcastModule from "../../tabulator/customModules/InternalToExternalEventBroadcastModule.js" import MenuExtensionModule from "../../tabulator/customModules/MenuExtensionModule.js" +import ResponsiveLayoutExtensionModule from "../../tabulator/customModules/ResponsiveLayoutExtensionModule.js" import { debounce } from "../../helpers/DebounceHelper.js"; @@ -128,26 +129,6 @@ export const CoreFilterCmpt = { page: false, }, collapsedHeadingLocalizationTimer: null, - localizeCollapsedColumnHeadings: - debounce( - () => { - const columnHeadings = this.tabulator?.getLang()?.columns; - if (!columnHeadings?.length) return; - - this.$refs.table - .querySelectorAll(".collapsedColumnHeading") - .forEach((collapsedColumnHeadingElement) => { - const field = - collapsedColumnHeadingElement.getAttribute( - "tabulator-column-field", - ); - if (!field?.length) return; - - collapsedColumnHeadingElement.innerHTML = - columnHeadings[field]; - }); - }, 200) - , }; }, computed: { @@ -258,10 +239,6 @@ export const CoreFilterCmpt = { if (!this.$props.tabulatorOptions.locale) return; this.tabulator.setLocale(newSelectedLanguage); - - if (this.$props.tabulatorOptions.responsiveLayoutCollapseFormatter) { - this.localizeCollapsedColumnHeadings(); - } }, }, }, @@ -349,7 +326,8 @@ export const CoreFilterCmpt = { // Start the tabulator with the build options this.tabulator = new Tabulator(this.$refs.table, { ...tabulatorOptions, - debugInvalidOptions: false, + // todo: remove comment + // debugInvalidOptions: false, }); // If event handlers have been provided if (Array.isArray(this.tabulatorEvents) && this.tabulatorEvents.length > 0) @@ -407,16 +385,8 @@ export const CoreFilterCmpt = { this.filterActive = filters.length > 0; this.$emit("headerFilterOn", this.filterActive); }); - - this.tabulator.on("renderComplete", () => { - if (tabulatorOptions.locale && tabulatorOptions.responsiveLayoutCollapseFormatter && this.tableBuilt) { - this.localizeCollapsedColumnHeadings(); - } - }); - this.tabulator.on("layoutRefreshed", () => { - if (tabulatorOptions.locale && tabulatorOptions.responsiveLayoutCollapseFormatter) { - this.localizeCollapsedColumnHeadings(); - } + this.tabulator.on("localized", () => { + this.tabulator.modules.responsiveLayout.generateCollapsedContent(); }); }, async configureTabulatorLocalizations(tabulatorOptions) { @@ -491,6 +461,10 @@ export const CoreFilterCmpt = { } }); + if (!Object.keys(phrasesGroupedByCategoryRequestParam).length) { + return tabulatorOptions; + } + const phrasesResponse = await this.$api.call( ApiPhrases.getPhrases(phrasesGroupedByCategoryRequestParam), ); @@ -857,6 +831,7 @@ export const CoreFilterCmpt = { Tabulator.registerModule(InternalToExternalEventBroadcastModule); Tabulator.registerModule(MenuExtensionModule); + Tabulator.registerModule(ResponsiveLayoutExtensionModule); }, mounted() { this.initTabulator().then(() => { diff --git a/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js b/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js index 6bbb58d13..e4d0b2811 100644 --- a/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js +++ b/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js @@ -13,10 +13,11 @@ export default class InternalToExternalEventBroadcastModule extends Module { super(table); this.eventsToBroadcast = [ - { - internal: "layout-refreshed", - external: null, - }, + // example event + // { + // internal: "layout-refreshed", + // external: null, + // }, ]; } diff --git a/public/js/tabulator/customModules/MenuExtensionModule.js b/public/js/tabulator/customModules/MenuExtensionModule.js index 18df92e3d..883e82aeb 100644 --- a/public/js/tabulator/customModules/MenuExtensionModule.js +++ b/public/js/tabulator/customModules/MenuExtensionModule.js @@ -9,7 +9,7 @@ export default class MenuExtensionModule extends Menu { } loadMenu(e, component, menu, parentEl, parentPopup){ - const isLocalizationEnabled = !!component.table.options.locale; + const isLocalizationEnabled = component.table.options.locale && component.table.options.locale !== "default"; const menuItemTranslations = component.table.getLang().menuItems; if (isLocalizationEnabled && menuItemTranslations) { menu = menu.map((menuItem) => { diff --git a/public/js/tabulator/customModules/ResponsiveLayoutExtensionModule.js b/public/js/tabulator/customModules/ResponsiveLayoutExtensionModule.js new file mode 100644 index 000000000..c0e66cff0 --- /dev/null +++ b/public/js/tabulator/customModules/ResponsiveLayoutExtensionModule.js @@ -0,0 +1,27 @@ +import ResponsiveLayout from "../../../../vendor/olifolkerd/tabulator5/src/js/modules/ResponsiveLayout/ResponsiveLayout.js"; + +export default class ResponsiveLayoutExtensionModule extends ResponsiveLayout { + static moduleName = "responsiveLayout"; + static moduleInitNumber = 1; + + constructor(table) { + super(table); + } + + generateCollapsedRowData(row) { + let result = super.generateCollapsedRowData(row) + const isLocalizationEnabled = this.table.options.locale && this.table.options.locale !== "default"; + const columnHeadingTranslations = this.table?.getLang()?.columns; + + if (isLocalizationEnabled && columnHeadingTranslations) { + result = result.map((column) => { + if (columnHeadingTranslations[column.field]) { + column.title = columnHeadingTranslations[column.field]; + } + return column; + }); + } + + return result; + } +} From eb281f3bc3d236807eaab9e601be5fe4e41c7474 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 26 May 2026 14:27:22 +0200 Subject: [PATCH 17/19] hiding redundant tabulator warnings --- public/js/components/filter/Filter.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index 77ad648bd..a12ec43f8 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -326,8 +326,7 @@ export const CoreFilterCmpt = { // Start the tabulator with the build options this.tabulator = new Tabulator(this.$refs.table, { ...tabulatorOptions, - // todo: remove comment - // debugInvalidOptions: false, + debugInvalidOptions: false, }); // If event handlers have been provided if (Array.isArray(this.tabulatorEvents) && this.tabulatorEvents.length > 0) From 1d5b915717f3a1e379c936e118a2de322d2cb1c9 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 26 May 2026 15:47:11 +0200 Subject: [PATCH 18/19] minor fix --- public/js/components/LVVerwaltung/Setup/Table.js | 2 +- public/js/components/filter/Filter.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/js/components/LVVerwaltung/Setup/Table.js b/public/js/components/LVVerwaltung/Setup/Table.js index 67c63c3bb..5ebd7f793 100644 --- a/public/js/components/LVVerwaltung/Setup/Table.js +++ b/public/js/components/LVVerwaltung/Setup/Table.js @@ -192,7 +192,7 @@ export default { columns: [ { - title: this.$p.t('lehre', 'kurzbz'), + title: "adis", field: "lv_kurzbz", headerFilterFuncParams: {field: 'lv_kurzbz'}, headerFilter: true, diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index a12ec43f8..5f0447aea 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -809,7 +809,7 @@ export const CoreFilterCmpt = { getColumnNames() { if (!this.tableBuilt) { return {}; - } else if (this.tabulator.options.locale); { + } else if (this.tabulator.options.locale) { return this.tabulator.getLang().columns; } return this.tabulator.getColumns().reduce((res, col) => { From 23b1e98a7abb89ccbbec4304182465b5337f9a0f Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 26 May 2026 16:09:21 +0200 Subject: [PATCH 19/19] minor fix --- public/js/components/LVVerwaltung/Setup/Table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/components/LVVerwaltung/Setup/Table.js b/public/js/components/LVVerwaltung/Setup/Table.js index 5ebd7f793..67c63c3bb 100644 --- a/public/js/components/LVVerwaltung/Setup/Table.js +++ b/public/js/components/LVVerwaltung/Setup/Table.js @@ -192,7 +192,7 @@ export default { columns: [ { - title: "adis", + title: this.$p.t('lehre', 'kurzbz'), field: "lv_kurzbz", headerFilterFuncParams: {field: 'lv_kurzbz'}, headerFilter: true,