@saooti/octopus-sdk 41.9.0 → 41.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/index.ts +2 -1
- package/package.json +1 -1
- package/src/api/transcriptionApi.ts +67 -1
- package/src/components/composable/podcasts/usePodcastView.ts +2 -3
- package/src/components/composable/useDayjs.ts +24 -0
- package/src/components/composable/useRights.ts +11 -1
- package/src/components/composable/useTranslation.ts +67 -41
- package/src/components/display/podcasts/PodcastItemInfo.vue +3 -1
- package/src/components/display/podcasts/PodcastModuleBox.vue +13 -2
- package/src/components/display/podcasts/PodcastRawTranscript.vue +16 -5
- package/src/components/form/ClassicInputText.vue +11 -1
- package/src/components/misc/ClassicAlert.vue +26 -14
- package/src/components/pages/EmissionPage.vue +6 -3
- package/src/helper/language.ts +47 -28
- package/src/i18n.ts +0 -7
- package/src/stores/ParamSdkStore.ts +1 -1
- package/tests/components/composable/useRights.spec.ts +44 -2
- package/tests/components/composable/useTranslation.spec.ts +30 -0
- package/tests/components/display/podcasts/PodcastModuleBox.spec.ts +2 -2
- package/tests/components/pages/EmissionPage.spec.ts +1 -0
- package/tests/helper/language.spec.ts +11 -11
- package/vite.config.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 41.9.2 (07/04/2026)
|
|
4
|
+
|
|
5
|
+
**Features**
|
|
6
|
+
|
|
7
|
+
- **12535** - Amélioration de la sélection de la langue utilisée
|
|
8
|
+
- Prend maintenant en compte les langues alternatives du navigateur
|
|
9
|
+
- Prend en compte les langue "de base" des variantes (par exemple "fr" sera
|
|
10
|
+
utilisé pour un utilisateur "fr-CH")
|
|
11
|
+
|
|
12
|
+
**Fixes**
|
|
13
|
+
|
|
14
|
+
- Correction tests
|
|
15
|
+
|
|
16
|
+
## 41.9.1 (07/04/2026)
|
|
17
|
+
|
|
18
|
+
**Features**
|
|
19
|
+
|
|
20
|
+
- **12535** - Améliorations pour la traduction
|
|
21
|
+
- La régénration de la transcription invalide les données de
|
|
22
|
+
`PodcastRawTranscript`
|
|
23
|
+
- **14406** - Ajout de `useDayjs` qui permet d'avoir des dates réactives
|
|
24
|
+
- Mise à jour des droits relatifs aux transcriptions/traductions
|
|
25
|
+
- Ajout de nouveaux endpoints à `transcriptionApi`
|
|
26
|
+
- Ajout nouveaux exports:
|
|
27
|
+
- composable `useTranslation`
|
|
28
|
+
- types `TranslationData`, `ModifyPodcastConfig`
|
|
29
|
+
- énumération `TranslationState`
|
|
30
|
+
- Améliorations composants :
|
|
31
|
+
- `ClassicInputText` en mode `textarea` scrolle en haut du contenu par défaut
|
|
32
|
+
- `ClassicAlert` a maintenant un props `text` qui enlève le font et la bordure
|
|
33
|
+
|
|
34
|
+
**Fixes**
|
|
35
|
+
|
|
36
|
+
- **14330** - La page d'émission propose uniquement le dernier épisode
|
|
37
|
+
**valide** à la lecture
|
|
38
|
+
|
|
3
39
|
## 41.9.0 (31/03/2026)
|
|
4
40
|
|
|
5
41
|
**Features**
|
package/index.ts
CHANGED
|
@@ -135,6 +135,7 @@ export { useSharePath } from "./src/components/composable/share/useSharePath.ts"
|
|
|
135
135
|
export { useOrgaComputed } from "./src/components/composable/useOrgaComputed.ts";
|
|
136
136
|
export { useSeoTitleUrl } from "./src/components/composable/route/useSeoTitleUrl.ts";
|
|
137
137
|
export { useSeasonsManagement } from "./src/components/composable/useSeasonsManagement.ts";
|
|
138
|
+
export { useTranslation } from "./src/components/composable/useTranslation.ts";
|
|
138
139
|
|
|
139
140
|
//helper
|
|
140
141
|
import domHelper from "./src/helper/domHelper.ts";
|
|
@@ -161,7 +162,7 @@ import classicApi from "./src/api/classicApi.ts";
|
|
|
161
162
|
|
|
162
163
|
// API
|
|
163
164
|
export { emissionApi } from "./src/api/emissionApi.ts";
|
|
164
|
-
export { transcriptionApi } from "./src/api/transcriptionApi.ts";
|
|
165
|
+
export { transcriptionApi, type TranslationData, TranslationState } from "./src/api/transcriptionApi.ts";
|
|
165
166
|
export * from "./src/api/groupsApi.ts";
|
|
166
167
|
export { organisationApi } from "./src/api/organisationApi.ts";
|
|
167
168
|
export { playlistApi } from "./src/api/playlistApi.ts";
|
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ModifyPodcastConfig } from "../stores/class/transcript/transcriptParams";
|
|
1
2
|
import { ModuleApi } from "./apiConnection";
|
|
2
3
|
import classicApi from "./classicApi";
|
|
3
4
|
|
|
@@ -100,8 +101,73 @@ async function getRawTranscription(podcastId: number): Promise<string> {
|
|
|
100
101
|
});
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Generate the transcription on the given podcast
|
|
106
|
+
* @param podcastId ID of the podcast on which to do the transcription
|
|
107
|
+
* @param langauge Target language. Should be the native language of the podcast
|
|
108
|
+
* @param params Transcription parameters
|
|
109
|
+
*/
|
|
110
|
+
async function generateTranscription(podcastId: number, language: string, params: ModifyPodcastConfig): Promise<void> {
|
|
111
|
+
await classicApi.putData({
|
|
112
|
+
api: ModuleApi.SPEECHTOTEXT,
|
|
113
|
+
path: `convert/${language}/${podcastId}`,
|
|
114
|
+
dataToSend: params
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Regenerate the transcription on the given podcast
|
|
120
|
+
* Currently does the same as `generateTranscription`, but should be used when
|
|
121
|
+
* relevant in case of future evolutions.
|
|
122
|
+
* @param podcastId ID of the podcast on which to do the transcription
|
|
123
|
+
* @param langauge Target language. Should be the native language of the podcast
|
|
124
|
+
* @param params Transcription parameters
|
|
125
|
+
*/
|
|
126
|
+
async function regenerateTranscription(podcastId: number, language: string, params: ModifyPodcastConfig): Promise<void> {
|
|
127
|
+
await classicApi.putData({
|
|
128
|
+
api: ModuleApi.SPEECHTOTEXT,
|
|
129
|
+
path: `regenerate/${language}/${podcastId}`,
|
|
130
|
+
dataToSend: params
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Change the visibility of the podcast
|
|
136
|
+
* @param podcastId ID of the podcast for which to change the visibility
|
|
137
|
+
* @param visibility New visibility state
|
|
138
|
+
*/
|
|
139
|
+
async function changeTranscriptionVisibility(podcastId: number, visibility: boolean): Promise<void> {
|
|
140
|
+
await classicApi.putData({
|
|
141
|
+
api: 11,
|
|
142
|
+
path: "visibility/" + podcastId,
|
|
143
|
+
parameters: {
|
|
144
|
+
visibility
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Update the transcription of a podcast
|
|
151
|
+
* @param podcastId ID of the podcast
|
|
152
|
+
* @param transcript The new transcript
|
|
153
|
+
*/
|
|
154
|
+
async function updateTranscription(podcastId: number, transcript: string): Promise<void> {
|
|
155
|
+
await classicApi.postData({
|
|
156
|
+
api: ModuleApi.SPEECHTOTEXT,
|
|
157
|
+
path: "update/" + podcastId,
|
|
158
|
+
dataToSend: {
|
|
159
|
+
file: new File([transcript], "file")
|
|
160
|
+
},
|
|
161
|
+
contentType: "formData"
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
103
165
|
export const transcriptionApi = {
|
|
166
|
+
changeTranscriptionVisibility,
|
|
104
167
|
getTranslations,
|
|
105
168
|
getTranslation,
|
|
106
|
-
getRawTranscription
|
|
169
|
+
getRawTranscription,
|
|
170
|
+
generateTranscription,
|
|
171
|
+
regenerateTranscription,
|
|
172
|
+
updateTranscription
|
|
107
173
|
};
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { Conference } from '@/stores/class/conference/conference';
|
|
2
2
|
import { Podcast } from '@/stores/class/general/podcast';
|
|
3
|
-
import dayjs from 'dayjs';
|
|
4
|
-
import duration from "dayjs/plugin/duration";
|
|
5
|
-
dayjs.extend(duration);
|
|
6
3
|
// @ts-expect-error Bibliothèque non typée
|
|
7
4
|
import humanizeDuration from "humanize-duration";
|
|
8
5
|
import {computed, Ref} from 'vue';
|
|
9
6
|
import { useI18n } from 'vue-i18n';
|
|
10
7
|
import {useOrgaComputed} from "../useOrgaComputed"
|
|
11
8
|
import { state } from '../../../stores/ParamSdkStore';
|
|
9
|
+
import { useDayjs } from '../useDayjs';
|
|
12
10
|
|
|
13
11
|
export const usePodcastView = (podcast: Ref<Podcast|undefined>, podcastConference: Ref<Conference|undefined>)=>{
|
|
14
12
|
|
|
15
13
|
const {locale} = useI18n();
|
|
14
|
+
const { dayjs } = useDayjs();
|
|
16
15
|
|
|
17
16
|
const {isEditRights, isPodcastmaker} = useOrgaComputed();
|
|
18
17
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import dayjs, { Dayjs } from "dayjs";
|
|
2
|
+
import { useI18n } from "vue-i18n"
|
|
3
|
+
|
|
4
|
+
import "dayjs/locale/de";
|
|
5
|
+
import "dayjs/locale/es";
|
|
6
|
+
import "dayjs/locale/fr";
|
|
7
|
+
import "dayjs/locale/it";
|
|
8
|
+
import "dayjs/locale/sl";
|
|
9
|
+
|
|
10
|
+
import duration from "dayjs/plugin/duration";
|
|
11
|
+
dayjs.extend(duration);
|
|
12
|
+
|
|
13
|
+
export const useDayjs = () => {
|
|
14
|
+
const { locale } = useI18n();
|
|
15
|
+
|
|
16
|
+
function composableDayjs(param?: string|number|Date|Dayjs): Dayjs {
|
|
17
|
+
return dayjs(param).locale(locale.value);
|
|
18
|
+
}
|
|
19
|
+
composableDayjs.duration = dayjs.duration;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
dayjs: composableDayjs
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -154,13 +154,21 @@ export const useRights = () => {
|
|
|
154
154
|
return true;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
if (roleContainsAny('RESTRICTED_PRODUCTION', '
|
|
157
|
+
if (roleContainsAny('RESTRICTED_PRODUCTION', 'PODCAST_CRUD')) {
|
|
158
158
|
return podcast.createdByUserId === authStore.authProfile?.userId;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
return false;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
function canEditTranscriptVisibility(podcast: Podcast): boolean {
|
|
165
|
+
return roleContainsAny('ADMIN', 'ORGANISATION', 'PRODUCTION');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function canEditTranslation(podcast: Podcast): boolean {
|
|
169
|
+
return canEditTranscript(podcast);
|
|
170
|
+
}
|
|
171
|
+
|
|
164
172
|
function canSeeHistory(): boolean {
|
|
165
173
|
return roleContainsAny('ADMIN', 'ORGANISATION');
|
|
166
174
|
}
|
|
@@ -198,6 +206,8 @@ export const useRights = () => {
|
|
|
198
206
|
// Other
|
|
199
207
|
canEditCodeInsertPlayer,
|
|
200
208
|
canEditTranscript,
|
|
209
|
+
canEditTranslation,
|
|
210
|
+
canEditTranscriptVisibility,
|
|
201
211
|
canSeeHistory,
|
|
202
212
|
isRestrictedProduction
|
|
203
213
|
}
|
|
@@ -14,6 +14,12 @@ interface RelevantLanguages {
|
|
|
14
14
|
available?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
enum Availability {
|
|
18
|
+
Available,
|
|
19
|
+
ToGenerate,
|
|
20
|
+
Unavailable
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
export const useTranslation = () => {
|
|
18
24
|
|
|
19
25
|
const authStore = useAuthStore();
|
|
@@ -64,7 +70,7 @@ export const useTranslation = () => {
|
|
|
64
70
|
* @param emission *(optional)* If set, will also check in emission settings
|
|
65
71
|
* @returns Whether the language is available or not
|
|
66
72
|
*/
|
|
67
|
-
function
|
|
73
|
+
function getTranslationConfig(language: string, emission?: Emission): CreateTranslation {
|
|
68
74
|
const orgAttributes = authStore.authOrganisation.attributes;
|
|
69
75
|
const emissionTranslation = parseOrDefault(emission?.annotations['translation-config'] as string|undefined, true);
|
|
70
76
|
const orgTranslation = parseOrDefault(orgAttributes?.['translation-config']);
|
|
@@ -72,6 +78,32 @@ export const useTranslation = () => {
|
|
|
72
78
|
return getConfigurationFor(language, emissionTranslation, orgTranslation);
|
|
73
79
|
}
|
|
74
80
|
|
|
81
|
+
function getLanguageAvailability(language: string, emission: Emission, translationData: PodcastTranslationData): Availability {
|
|
82
|
+
|
|
83
|
+
// 1. If language of podcast == language of browser, use that language
|
|
84
|
+
if (language === translationData.nativeLanguage) {
|
|
85
|
+
return Availability.Available;
|
|
86
|
+
} else {
|
|
87
|
+
const langConfig = getTranslationConfig(language, emission);
|
|
88
|
+
|
|
89
|
+
// 2. If the language of the browser is available, use it
|
|
90
|
+
if (langConfig === CreateTranslation.ALWAYS) {
|
|
91
|
+
return Availability.Available;
|
|
92
|
+
}
|
|
93
|
+
// 2b. If the language is on demand, check if generated
|
|
94
|
+
else if (langConfig === CreateTranslation.ON_DEMAND) {
|
|
95
|
+
// Check if translation is already generated
|
|
96
|
+
if (translationData.translations.find(t => t.language === language && t.state === TranslationState.FINISHED)) {
|
|
97
|
+
return Availability.Available;
|
|
98
|
+
} else {
|
|
99
|
+
return Availability.ToGenerate;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Availability.Unavailable;
|
|
105
|
+
}
|
|
106
|
+
|
|
75
107
|
/**
|
|
76
108
|
* Get the relevant language for the current user
|
|
77
109
|
* @param translationData The translation data for the podcast
|
|
@@ -81,56 +113,49 @@ export const useTranslation = () => {
|
|
|
81
113
|
* undefined if *ready* is better)
|
|
82
114
|
*/
|
|
83
115
|
async function getMostRelevantLanguage(translationData: PodcastTranslationData): Promise<RelevantLanguages> {
|
|
84
|
-
const
|
|
85
|
-
ready: translationData.nativeLanguage
|
|
86
|
-
};
|
|
87
|
-
const baseLanguage = getLanguage();
|
|
88
|
-
|
|
89
|
-
// 1. If language of podcast == language of browser, use that language
|
|
90
|
-
if (baseLanguage === translationData.nativeLanguage) {
|
|
91
|
-
languages.ready = baseLanguage;
|
|
92
|
-
} else {
|
|
93
|
-
const podcast = await podcastApi.get(translationData.podcastId);
|
|
94
|
-
const emission = podcast.emission;
|
|
116
|
+
const userLanguage = getLanguage(false);
|
|
95
117
|
|
|
96
|
-
|
|
97
|
-
|
|
118
|
+
// If user language is native, do not go further
|
|
119
|
+
if (userLanguage === translationData.nativeLanguage) {
|
|
120
|
+
return { ready: userLanguage };
|
|
121
|
+
}
|
|
98
122
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
123
|
+
// Compute all available languages. Use language set on site, then
|
|
124
|
+
// languages defined on browser, then default, and finally native language
|
|
125
|
+
const languages: string[] = [];
|
|
126
|
+
[userLanguage, ...navigator.languages, DEFAULT_LANGUAGE, translationData.nativeLanguage].forEach(l => {
|
|
127
|
+
if (!languages.includes(l)) {
|
|
128
|
+
languages.push(l);
|
|
102
129
|
}
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
languages.available = baseLanguage;
|
|
130
|
+
// For each language, if it is a variant (for example fr-CH), also
|
|
131
|
+
// add the base language
|
|
132
|
+
if (l.includes('-')) {
|
|
133
|
+
const base = l.split('-')[0];
|
|
134
|
+
if (!languages.includes(base)) {
|
|
135
|
+
languages.push(base);
|
|
110
136
|
}
|
|
111
137
|
}
|
|
138
|
+
});
|
|
112
139
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
140
|
+
const podcast = await podcastApi.get(translationData.podcastId);
|
|
141
|
+
const emission = podcast.emission;
|
|
142
|
+
|
|
143
|
+
let bestReady: string|null = null;
|
|
144
|
+
let bestAvailable: string|null = null;
|
|
145
|
+
|
|
146
|
+
for (const language of languages) {
|
|
147
|
+
const avaibility = getLanguageAvailability(language, emission, translationData);
|
|
148
|
+
if (avaibility === Availability.Available && bestReady === null) {
|
|
149
|
+
bestReady = language;
|
|
150
|
+
break;
|
|
125
151
|
}
|
|
126
152
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
languages.ready = translationData.nativeLanguage;
|
|
153
|
+
if (avaibility === Availability.ToGenerate && bestAvailable === null) {
|
|
154
|
+
bestAvailable = language;
|
|
130
155
|
}
|
|
131
156
|
}
|
|
132
157
|
|
|
133
|
-
return
|
|
158
|
+
return { ready: bestReady, available: bestAvailable ?? undefined };
|
|
134
159
|
}
|
|
135
160
|
|
|
136
161
|
/**
|
|
@@ -169,6 +194,7 @@ export const useTranslation = () => {
|
|
|
169
194
|
return {
|
|
170
195
|
convertSrtToPlainText,
|
|
171
196
|
getMostRelevantLanguage,
|
|
172
|
-
getMostRelevantTranslation
|
|
197
|
+
getMostRelevantTranslation,
|
|
198
|
+
getTranslationConfig
|
|
173
199
|
}
|
|
174
200
|
};
|
|
@@ -60,7 +60,6 @@
|
|
|
60
60
|
<script setup lang="ts">
|
|
61
61
|
import AnimatorsItem from "./AnimatorsItem.vue";
|
|
62
62
|
import {useOrgaComputed} from "../../composable/useOrgaComputed";
|
|
63
|
-
import dayjs from "dayjs";
|
|
64
63
|
import { computed, defineAsyncComponent } from "vue";
|
|
65
64
|
import { Podcast, PodcastType } from "../../../stores/class/general/podcast";
|
|
66
65
|
import { state } from "../../../stores/ParamSdkStore";
|
|
@@ -68,6 +67,7 @@ import { useI18n } from "vue-i18n";
|
|
|
68
67
|
import { useSeasonsManagement } from "../../composable/useSeasonsManagement";
|
|
69
68
|
import BullhornIcon from 'vue-material-design-icons/Bullhorn.vue';
|
|
70
69
|
import GiftIcon from 'vue-material-design-icons/Gift.vue';
|
|
70
|
+
import { useDayjs } from "../../composable/useDayjs";
|
|
71
71
|
const PodcastPlayBar = defineAsyncComponent(
|
|
72
72
|
() => import("./PodcastPlayBar.vue"),
|
|
73
73
|
);
|
|
@@ -81,6 +81,7 @@ const props = defineProps({
|
|
|
81
81
|
const { t } = useI18n();
|
|
82
82
|
const { isPodcastmaker } = useOrgaComputed();
|
|
83
83
|
const { formatSeason } = useSeasonsManagement();
|
|
84
|
+
const { dayjs } = useDayjs();
|
|
84
85
|
|
|
85
86
|
//Computed
|
|
86
87
|
const date = computed(() => {
|
|
@@ -90,6 +91,7 @@ const date = computed(() => {
|
|
|
90
91
|
}
|
|
91
92
|
return dayjs(props.podcast.pubDate).format(format);
|
|
92
93
|
});
|
|
94
|
+
|
|
93
95
|
const orgaNameDisplay = computed(() =>{
|
|
94
96
|
if (props.podcast.organisation.name.length > 30) {
|
|
95
97
|
return props.podcast.organisation.name.substring(0, 30) + "...";
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
v-else-if="editRight && isEditBox"
|
|
13
13
|
:podcast="podcast"
|
|
14
14
|
:display-studio-access="isDebriefing"
|
|
15
|
+
@update-transcription="resetTranscription"
|
|
15
16
|
@validate-podcast="emit('updatePodcast', $event)"
|
|
16
17
|
/>
|
|
17
18
|
<div class="mb-2 w-100">
|
|
@@ -158,7 +159,10 @@
|
|
|
158
159
|
:orga-id="podcast.organisation.id"
|
|
159
160
|
:rubrique-ids="podcastRubriques"
|
|
160
161
|
/>
|
|
161
|
-
<PodcastRawTranscript
|
|
162
|
+
<PodcastRawTranscript
|
|
163
|
+
ref="podcastRawTranscript"
|
|
164
|
+
:podcast-id="podcast.podcastId"
|
|
165
|
+
/>
|
|
162
166
|
<SubscribeButtons
|
|
163
167
|
v-if="isPodcastmaker"
|
|
164
168
|
class="mt-4"
|
|
@@ -185,7 +189,7 @@ import { useRouter } from "vue-router";
|
|
|
185
189
|
import { useSeasonsManagement } from "../../composable/useSeasonsManagement";
|
|
186
190
|
import { SeasonMode } from "../../../stores/class/general/emission";
|
|
187
191
|
|
|
188
|
-
import { defineAsyncComponent, toRefs, computed } from "vue";
|
|
192
|
+
import { defineAsyncComponent, toRefs, computed, useTemplateRef } from "vue";
|
|
189
193
|
const ErrorMessage = defineAsyncComponent(
|
|
190
194
|
() => import("../../misc/ErrorMessage.vue"),
|
|
191
195
|
);
|
|
@@ -243,6 +247,9 @@ const authStore = useAuthStore();
|
|
|
243
247
|
const router = useRouter();
|
|
244
248
|
const { areSeasonsEnabled } = useSeasonsManagement();
|
|
245
249
|
|
|
250
|
+
/** Reference to the PodcastRqwTranscript */
|
|
251
|
+
const podcastRawTranscript = useTemplateRef('podcastRawTranscript');
|
|
252
|
+
|
|
246
253
|
//Computed
|
|
247
254
|
const podcastRubriques = computed(() => {
|
|
248
255
|
let rubriques = props.podcast?.rubriqueIds ?? [];
|
|
@@ -342,6 +349,10 @@ function removeDeleted(): void {
|
|
|
342
349
|
}
|
|
343
350
|
}
|
|
344
351
|
|
|
352
|
+
function resetTranscription(): void {
|
|
353
|
+
podcastRawTranscript.value.reset();
|
|
354
|
+
}
|
|
355
|
+
|
|
345
356
|
const showTags = computed((): boolean => {
|
|
346
357
|
if (state.podcastPage.hideTags === true) {
|
|
347
358
|
return false;
|
|
@@ -137,12 +137,14 @@ function saveAccessibility(accessibility: {fontSize: number,background: string,c
|
|
|
137
137
|
isAccessibilityModal.value = false;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
async function fetchTranscripts(): Promise<void> {
|
|
140
|
+
async function fetchTranscripts(language?: string): Promise<void> {
|
|
141
141
|
try {
|
|
142
142
|
const translation = await transcriptionApi.getTranslations(props.podcastId);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
if (language === undefined) {
|
|
144
|
+
const { ready, available } = await getMostRelevantLanguage(translation);
|
|
145
|
+
// If available language is set, it is better than ready, so use it
|
|
146
|
+
language = available ?? ready;
|
|
147
|
+
}
|
|
146
148
|
const srt = await transcriptionApi.getTranslation(props.podcastId, language, true);
|
|
147
149
|
transcript.value = convertSrtToPlainText(srt);
|
|
148
150
|
|
|
@@ -165,7 +167,7 @@ async function fetchTranscripts(): Promise<void> {
|
|
|
165
167
|
async function changeLanguage(language: string): Promise<void> {
|
|
166
168
|
loadingDone.value = false;
|
|
167
169
|
currentLanguage.value = language;
|
|
168
|
-
transcript.value =
|
|
170
|
+
transcript.value = undefined;
|
|
169
171
|
try {
|
|
170
172
|
const srt = await transcriptionApi.getTranslation(props.podcastId, language);
|
|
171
173
|
transcript.value = convertSrtToPlainText(srt);
|
|
@@ -174,6 +176,15 @@ async function changeLanguage(language: string): Promise<void> {
|
|
|
174
176
|
}
|
|
175
177
|
loadingDone.value = true;
|
|
176
178
|
}
|
|
179
|
+
|
|
180
|
+
/** Force reloading */
|
|
181
|
+
function reset(): void {
|
|
182
|
+
if (isOpen.value || loadingDone.value) {
|
|
183
|
+
fetchTranscripts(currentLanguage.value);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
defineExpose({ reset });
|
|
177
188
|
</script>
|
|
178
189
|
|
|
179
190
|
<style scoped lang="scss">
|
|
@@ -259,7 +259,17 @@ onMounted(()=>{
|
|
|
259
259
|
if (props.errorVariable !== isError.value) {
|
|
260
260
|
emit("update:errorVariable", isError.value);
|
|
261
261
|
}
|
|
262
|
-
|
|
262
|
+
|
|
263
|
+
if (props.isTextarea) {
|
|
264
|
+
// Delay a scroll back to the top of the text area
|
|
265
|
+
setTimeout(() => {
|
|
266
|
+
const textArea = document.getElementById(computedInputId.value);
|
|
267
|
+
if (textArea) {
|
|
268
|
+
textArea.scrollTop = 0;
|
|
269
|
+
}
|
|
270
|
+
}, 100);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
263
273
|
|
|
264
274
|
//Methods
|
|
265
275
|
function addEmojiSelected(emoji: string) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
-->
|
|
9
9
|
<template>
|
|
10
10
|
<div
|
|
11
|
-
class="p-2 pe-4
|
|
11
|
+
class="p-2 pe-4 rounded d-flex alert"
|
|
12
12
|
:class="cardClass"
|
|
13
13
|
>
|
|
14
14
|
<!-- The icon -->
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
:is="iconComponent"
|
|
17
17
|
v-if="!noIcon"
|
|
18
18
|
class="icon"
|
|
19
|
-
:size="30"
|
|
19
|
+
:size="text ? 22 : 30"
|
|
20
20
|
/>
|
|
21
21
|
|
|
22
22
|
<!-- Main content -->
|
|
@@ -39,18 +39,26 @@ import CheckCircle from 'vue-material-design-icons/CheckCircle.vue';
|
|
|
39
39
|
import CloseCircle from 'vue-material-design-icons/CloseCircle.vue';
|
|
40
40
|
import Information from 'vue-material-design-icons/Information.vue';
|
|
41
41
|
|
|
42
|
-
const { type } = defineProps<{
|
|
42
|
+
const { type, text } = defineProps<{
|
|
43
43
|
/** Disables the icon when true */
|
|
44
44
|
noIcon?: boolean;
|
|
45
45
|
/** An optional title for the alert */
|
|
46
46
|
title?: string;
|
|
47
47
|
/** The type of message */
|
|
48
48
|
type: 'info'|'success'|'warning'|'error';
|
|
49
|
+
/** Use a simpler display */
|
|
50
|
+
text?: boolean;
|
|
49
51
|
}>();
|
|
50
52
|
|
|
51
53
|
/** The class applied to the alert */
|
|
52
|
-
const cardClass = computed(() => {
|
|
53
|
-
|
|
54
|
+
const cardClass = computed((): Array<string> => {
|
|
55
|
+
const classes = ['alert-' + type];
|
|
56
|
+
if (text === true) {
|
|
57
|
+
classes.push('text-alert');
|
|
58
|
+
} else {
|
|
59
|
+
classes.push('my-2');
|
|
60
|
+
}
|
|
61
|
+
return classes;
|
|
54
62
|
});
|
|
55
63
|
|
|
56
64
|
const iconComponent = computed(() => {
|
|
@@ -70,9 +78,10 @@ const iconComponent = computed(() => {
|
|
|
70
78
|
|
|
71
79
|
<style lang="scss" scoped>
|
|
72
80
|
.alert {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
&:not(.text-alert) {
|
|
82
|
+
border: 1px solid;
|
|
83
|
+
border-left: 8px solid;
|
|
84
|
+
}
|
|
76
85
|
|
|
77
86
|
.icon {
|
|
78
87
|
align-items: start !important;
|
|
@@ -98,14 +107,17 @@ const iconComponent = computed(() => {
|
|
|
98
107
|
'error': var(--octopus-danger),
|
|
99
108
|
);
|
|
100
109
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
@each $type, $color in $types {
|
|
111
|
+
&-#{$type} {
|
|
112
|
+
&:not(.text-alert) {
|
|
104
113
|
background-color: hsl(from #{$color} h s 95) !important;
|
|
105
114
|
border-color: #{$color} !important;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
115
|
+
}
|
|
116
|
+
&.text-alert {
|
|
117
|
+
color: #{$color} !important;
|
|
118
|
+
}
|
|
119
|
+
.icon {
|
|
120
|
+
color: #{$color};
|
|
109
121
|
}
|
|
110
122
|
}
|
|
111
123
|
}
|
|
@@ -269,8 +269,11 @@ async function getEmissionDetails(): Promise<void> {
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
function isReadyAndVisibleAndValid(p: Podcast): boolean {
|
|
273
|
+
return "READY" === p.processingStatus && p.availability.visibility && p.valid === true;
|
|
274
|
+
}
|
|
275
|
+
|
|
272
276
|
function podcastsFetched(podcasts: Array<Podcast>, season: number|undefined) {
|
|
273
|
-
const isReadyAndVisible = (p: Podcast) => "READY" === p.processingStatus && p.availability.visibility;
|
|
274
277
|
|
|
275
278
|
if (areSeasonsEnabled(emission.value)) {
|
|
276
279
|
// Ignore results that are not from the last season
|
|
@@ -279,11 +282,11 @@ function podcastsFetched(podcasts: Array<Podcast>, season: number|undefined) {
|
|
|
279
282
|
return;
|
|
280
283
|
}
|
|
281
284
|
// If seasons are enabled, take last element
|
|
282
|
-
lastPodcast.value = podcasts.findLast(
|
|
285
|
+
lastPodcast.value = podcasts.findLast(isReadyAndVisibleAndValid);
|
|
283
286
|
} else {
|
|
284
287
|
// If seasons are disabled, we have a standard date desc sort, so we take
|
|
285
288
|
// first element
|
|
286
|
-
lastPodcast.value = podcasts.find(
|
|
289
|
+
lastPodcast.value = podcasts.find(isReadyAndVisibleAndValid);
|
|
287
290
|
}
|
|
288
291
|
}
|
|
289
292
|
|
package/src/helper/language.ts
CHANGED
|
@@ -1,38 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utility function to retrieve most relevant language for the user
|
|
3
|
+
* @params forceToKnown When set to true (default), will limit language to one
|
|
4
|
+
* one of 6 known languages. When set to false, allow all
|
|
5
|
+
* languages.
|
|
3
6
|
* @returns The most relevant language
|
|
4
7
|
*/
|
|
5
|
-
export function getLanguage(): string {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
export function getLanguage(forceToKnown = true): string {
|
|
9
|
+
const nameEQ = "octopus-language=";
|
|
10
|
+
const ca = document.cookie.split(";");
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
let language = "";
|
|
13
|
+
for (const valueCookie of ca) {
|
|
14
|
+
let c = valueCookie;
|
|
15
|
+
while (c.startsWith(" ")) {
|
|
16
|
+
c = c.substring(1, c.length);
|
|
17
|
+
}
|
|
18
|
+
if (0 === c.indexOf(nameEQ)) {
|
|
19
|
+
language = c.substring(nameEQ.length, c.length);
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
14
22
|
}
|
|
15
|
-
if (0 === c.indexOf(nameEQ)) {
|
|
16
|
-
language = c.substring(nameEQ.length, c.length);
|
|
17
|
-
break;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
if (0 === language.length) {
|
|
25
|
+
if (forceToKnown === true) {
|
|
26
|
+
// Choose best avilable language
|
|
27
|
+
for (const navigatorLang of navigator.languages) {
|
|
28
|
+
if (navigatorLang.includes("fr")) {
|
|
29
|
+
language = "fr";
|
|
30
|
+
break;
|
|
31
|
+
} else if (navigatorLang.includes("en")) {
|
|
32
|
+
language = "en";
|
|
33
|
+
break;
|
|
34
|
+
} else if (navigatorLang.includes("it")) {
|
|
35
|
+
language = "it";
|
|
36
|
+
break;
|
|
37
|
+
} else if (navigatorLang.includes("sl")) {
|
|
38
|
+
language = "sl";
|
|
39
|
+
break;
|
|
40
|
+
} else if (navigatorLang.includes("es")) {
|
|
41
|
+
language = "es";
|
|
42
|
+
break;
|
|
43
|
+
} else if (navigatorLang.includes("de")) {
|
|
44
|
+
language = "de";
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!language) {
|
|
49
|
+
language = "fr";
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
language = navigator.language;
|
|
53
|
+
}
|
|
34
54
|
}
|
|
35
|
-
}
|
|
36
55
|
|
|
37
|
-
|
|
56
|
+
return language;
|
|
38
57
|
}
|
package/src/i18n.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { nextTick } from "vue";
|
|
2
2
|
import { createI18n } from "vue-i18n";
|
|
3
|
-
import dayjs from "dayjs";
|
|
4
|
-
import "dayjs/locale/de";
|
|
5
|
-
import "dayjs/locale/es";
|
|
6
|
-
import "dayjs/locale/fr";
|
|
7
|
-
import "dayjs/locale/it";
|
|
8
|
-
import "dayjs/locale/sl";
|
|
9
3
|
|
|
10
4
|
export function setupI18n(options: { [key:string]: string|boolean }, isAuthenticated: boolean, isEducation: boolean) {
|
|
11
5
|
const i18n = createI18n(options);
|
|
@@ -15,7 +9,6 @@ export function setupI18n(options: { [key:string]: string|boolean }, isAuthentic
|
|
|
15
9
|
|
|
16
10
|
export function setI18nLanguage(i18n: any, locale: string) {
|
|
17
11
|
i18n.locale.value= locale;
|
|
18
|
-
dayjs.locale(locale);
|
|
19
12
|
const html = document.querySelector("html");
|
|
20
13
|
if (html) {
|
|
21
14
|
html.setAttribute("lang", locale);
|
|
@@ -63,7 +63,7 @@ export interface ParamStore {
|
|
|
63
63
|
/** If true, do not display subtitles on podcast pages */
|
|
64
64
|
hideSubtitle?: boolean;
|
|
65
65
|
/** Select whether to display description, summary, or both */
|
|
66
|
-
descriptionOrSummary
|
|
66
|
+
descriptionOrSummary?: 'description'|'summary'|'both';
|
|
67
67
|
};
|
|
68
68
|
emissionPage: {
|
|
69
69
|
ShareButtons?: boolean;
|
|
@@ -256,7 +256,7 @@ describe('useRights', () => {
|
|
|
256
256
|
});
|
|
257
257
|
});
|
|
258
258
|
|
|
259
|
-
['RESTRICTED_PRODUCTION', '
|
|
259
|
+
['RESTRICTED_PRODUCTION', 'PODCAST_CRUD'].forEach(role => {
|
|
260
260
|
it(`${role} can only edit transcript of own podcast`, async () => {
|
|
261
261
|
await setup([role]);
|
|
262
262
|
expect(useRights().canEditTranscript(ownPodcast)).toBe(true);
|
|
@@ -265,11 +265,53 @@ describe('useRights', () => {
|
|
|
265
265
|
});
|
|
266
266
|
|
|
267
267
|
it('denies unrelated roles', async () => {
|
|
268
|
-
await setup(['
|
|
268
|
+
await setup(['RESTRICTED_ANIMATION']);
|
|
269
269
|
expect(useRights().canEditTranscript(ownPodcast)).toBe(false);
|
|
270
270
|
});
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
+
describe('canEditTranslation', () => {
|
|
274
|
+
const ownPodcast = { podcastId: 1, createdByUserId: 'test-user-123' } as Podcast;
|
|
275
|
+
const otherPodcast = { podcastId: 2, createdByUserId: 'other-user' } as Podcast;
|
|
276
|
+
|
|
277
|
+
['ADMIN', 'ORGANISATION', 'PRODUCTION'].forEach(role => {
|
|
278
|
+
it(`allows ${role} to edit transcript of any podcast`, async () => {
|
|
279
|
+
await setup([role]);
|
|
280
|
+
expect(useRights().canEditTranslation(otherPodcast)).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
['RESTRICTED_PRODUCTION', 'PODCAST_CRUD'].forEach(role => {
|
|
285
|
+
it(`${role} can only edit transcript of own podcast`, async () => {
|
|
286
|
+
await setup([role]);
|
|
287
|
+
expect(useRights().canEditTranslation(ownPodcast)).toBe(true);
|
|
288
|
+
expect(useRights().canEditTranslation(otherPodcast)).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('denies unrelated roles', async () => {
|
|
293
|
+
await setup(['RESTRICTED_ANIMATION']);
|
|
294
|
+
expect(useRights().canEditTranslation(ownPodcast)).toBe(false);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('canEditTranscriptVisibility', () => {
|
|
299
|
+
const ownPodcast = { podcastId: 1, createdByUserId: 'test-user-123' } as Podcast;
|
|
300
|
+
const otherPodcast = { podcastId: 2, createdByUserId: 'other-user' } as Podcast;
|
|
301
|
+
|
|
302
|
+
['ADMIN', 'ORGANISATION', 'PRODUCTION'].forEach(role => {
|
|
303
|
+
it(`allows ${role} to edit transcript of any podcast`, async () => {
|
|
304
|
+
await setup([role]);
|
|
305
|
+
expect(useRights().canEditTranscriptVisibility(otherPodcast)).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('denies unrelated roles', async () => {
|
|
310
|
+
await setup(['PODCAST_CRUD']);
|
|
311
|
+
expect(useRights().canEditTranscriptVisibility(ownPodcast)).toBe(false);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
273
315
|
describe('Other permissions', () => {
|
|
274
316
|
describe('canEditCodeInsertPlayer', () => {
|
|
275
317
|
['ADMIN', 'ORGANISATION'].forEach(role => {
|
|
@@ -76,6 +76,36 @@ describe('useTranslation', () => {
|
|
|
76
76
|
});
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
describe('getMostRelevantLanguage', () => {
|
|
80
|
+
it('bases its results on the language alternatives in the browser', async () => {
|
|
81
|
+
vi.mocked(getLanguage).mockReturnValue('de');
|
|
82
|
+
|
|
83
|
+
Object.defineProperty(navigator, 'languages', {
|
|
84
|
+
value: ['fr', 'it', 'de'],
|
|
85
|
+
configurable: true
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const translationData = { nativeLanguage: 'it', podcastId: 0, translations: [] };
|
|
89
|
+
|
|
90
|
+
const result = await composable.getMostRelevantLanguage(translationData)
|
|
91
|
+
expect(result.ready).toBe('it');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('takes into consideration the "base" languages of variants', async () => {
|
|
95
|
+
vi.mocked(getLanguage).mockReturnValue('fr-CH');
|
|
96
|
+
|
|
97
|
+
Object.defineProperty(navigator, 'languages', {
|
|
98
|
+
value: ['fr-CH', 'it', 'de'],
|
|
99
|
+
configurable: true
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const translationData = { nativeLanguage: 'fr', podcastId: 0, translations: [] };
|
|
103
|
+
|
|
104
|
+
const result = await composable.getMostRelevantLanguage(translationData)
|
|
105
|
+
expect(result.ready).toBe('fr');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
79
109
|
describe('getMostRelevantTranslation', () => {
|
|
80
110
|
it('uses the native language when it matches the browser language', async () => {
|
|
81
111
|
await composable.getMostRelevantTranslation(makeTranslationData({ nativeLanguage: 'fr' }));
|
|
@@ -21,14 +21,14 @@ describe('PodcastModuleBox', () => {
|
|
|
21
21
|
|
|
22
22
|
it('shows the date without time by default', async() => {
|
|
23
23
|
const wrapper = await mount(podcast);
|
|
24
|
-
expect(wrapper.text()).toContain('1
|
|
24
|
+
expect(wrapper.text()).toContain('1 décembre 2025');
|
|
25
25
|
expect(wrapper.text()).not.toContain('11:21');
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
it('shows the date with time when enabled in SdkParams', async() => {
|
|
29
29
|
initialize({ generalParameters: { showTimeWithDates: true } });
|
|
30
30
|
const wrapper = await mount(podcast);
|
|
31
|
-
expect(wrapper.text()).toContain('1
|
|
31
|
+
expect(wrapper.text()).toContain('1 décembre 2025');
|
|
32
32
|
expect(wrapper.text()).toContain('11:21');
|
|
33
33
|
});
|
|
34
34
|
});
|
|
@@ -2,8 +2,8 @@ import { describe, it, expect, afterEach } from 'vitest';
|
|
|
2
2
|
import { getLanguage } from '../../src/helper/language';
|
|
3
3
|
|
|
4
4
|
function setNavigatorLanguage(lang: string) {
|
|
5
|
-
Object.defineProperty(navigator, '
|
|
6
|
-
value: lang,
|
|
5
|
+
Object.defineProperty(navigator, 'languages', {
|
|
6
|
+
value: [lang],
|
|
7
7
|
configurable: true,
|
|
8
8
|
});
|
|
9
9
|
}
|
|
@@ -29,15 +29,15 @@ describe('language', () => {
|
|
|
29
29
|
|
|
30
30
|
describe('navigator-based detection', () => {
|
|
31
31
|
it.each([
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
])('returns "
|
|
40
|
-
setNavigatorLanguage(
|
|
32
|
+
{ input: 'en-US', expected: 'en' },
|
|
33
|
+
{ input: 'it-IT', expected: 'it' },
|
|
34
|
+
{ input: 'sl-SI', expected: 'sl' },
|
|
35
|
+
{ input: 'de-DE', expected: 'de' },
|
|
36
|
+
{ input: 'es-ES', expected: 'es' },
|
|
37
|
+
{ input: 'fr-FR', expected: 'fr' },
|
|
38
|
+
{ input: 'ja-JP', expected: 'fr' }
|
|
39
|
+
])('returns "$expected" for navigator language $input', ({ input , expected }) => {
|
|
40
|
+
setNavigatorLanguage(input);
|
|
41
41
|
expect(getLanguage()).toBe(expected);
|
|
42
42
|
});
|
|
43
43
|
});
|
package/vite.config.js
CHANGED