@mundogamernetwork/shared-ui 1.8.23 → 1.9.0

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.
@@ -0,0 +1,355 @@
1
+ <template>
2
+ <section id="comments" class="campaign-comments">
3
+ <header class="campaign-comments__header">
4
+ <h3 class="campaign-comments__title">
5
+ {{ $t("keys.campaigns.comments.title") }}
6
+ <span v-if="total" class="campaign-comments__count">{{ total }}</span>
7
+ </h3>
8
+ <p class="campaign-comments__subtitle">
9
+ {{ $t("keys.campaigns.comments.subtitle") }}
10
+ </p>
11
+ </header>
12
+
13
+ <!-- Composer. Rendered only when the API says this viewer may write, so
14
+ the rules live in one place instead of being re-derived here. -->
15
+ <form v-if="canComment" class="campaign-comments__composer" @submit.prevent="submit">
16
+ <textarea
17
+ v-model="draft"
18
+ class="campaign-comments__input"
19
+ :placeholder="$t('keys.campaigns.comments.placeholder')"
20
+ :maxlength="2200"
21
+ rows="3"
22
+ :disabled="posting"
23
+ />
24
+ <div class="campaign-comments__composer-footer">
25
+ <span class="campaign-comments__counter">{{ draft.length }}/2200</span>
26
+ <button type="submit" class="campaign-comments__submit" :disabled="!canSubmit">
27
+ {{ posting ? $t("keys.campaigns.comments.sending") : $t("keys.campaigns.comments.send") }}
28
+ </button>
29
+ </div>
30
+ </form>
31
+
32
+ <p v-else-if="!signedIn" class="campaign-comments__notice">
33
+ {{ $t("keys.campaigns.comments.sign_in_notice") }}
34
+ </p>
35
+ <p v-else class="campaign-comments__notice">
36
+ {{ $t("keys.campaigns.comments.creators_only_notice") }}
37
+ </p>
38
+
39
+ <p v-if="error" class="campaign-comments__error">{{ error }}</p>
40
+
41
+ <div v-if="loading && !comments.length" class="campaign-comments__skeleton">
42
+ <div v-for="n in 3" :key="n" class="campaign-comments__skeleton-row" />
43
+ </div>
44
+
45
+ <!-- The empty state is a statement of fact ("nobody has posted yet"), so
46
+ it must not show when the list is empty merely because the request
47
+ failed — the error message above already says what happened. -->
48
+ <p v-else-if="!comments.length && !error" class="campaign-comments__empty">
49
+ {{ $t("keys.campaigns.comments.empty") }}
50
+ </p>
51
+
52
+ <ul v-else class="campaign-comments__list">
53
+ <li v-for="comment in comments" :key="comment.uuid" class="campaign-comments__item">
54
+ <KeyCampaignCommentItem
55
+ :comment="comment"
56
+ :can-moderate="canModerate"
57
+ :can-reply="canComment"
58
+ @edit="onEdit"
59
+ @delete="onDelete"
60
+ @reply="onReply"
61
+ @hide="onHide"
62
+ @unhide="onUnhide"
63
+ />
64
+ </li>
65
+ </ul>
66
+
67
+ <button
68
+ v-if="hasMore"
69
+ class="campaign-comments__more"
70
+ :disabled="loading"
71
+ @click="loadMore"
72
+ >
73
+ {{ $t("keys.campaigns.comments.load_more") }}
74
+ </button>
75
+ </section>
76
+ </template>
77
+
78
+ <script setup lang="ts">
79
+ import { ref, computed, onMounted } from "vue"
80
+ import { useNuxtApp } from "#app"
81
+ import {
82
+ fetchCampaignComments,
83
+ postCampaignComment,
84
+ replyCampaignComment,
85
+ updateCampaignComment,
86
+ deleteCampaignComment,
87
+ hideCampaignComment,
88
+ unhideCampaignComment,
89
+ type CampaignComment,
90
+ } from "../../services/campaignService"
91
+
92
+ const props = defineProps<{
93
+ keyId: number
94
+ /**
95
+ * Permission flags come from the campaign payload (KeyTransformer) so the
96
+ * composer's visibility matches what the API will actually accept. They are
97
+ * refreshed from the comments response, which computes them too.
98
+ */
99
+ canComment?: boolean
100
+ canModerate?: boolean
101
+ signedIn?: boolean
102
+ }>()
103
+
104
+ const { $i18n } = useNuxtApp()
105
+
106
+ const comments = ref<CampaignComment[]>([])
107
+ const page = ref(1)
108
+ const lastPage = ref(1)
109
+ const total = ref(0)
110
+ const loading = ref(false)
111
+ const posting = ref(false)
112
+ const error = ref("")
113
+ const draft = ref("")
114
+
115
+ const canComment = ref(props.canComment ?? false)
116
+ const canModerate = ref(props.canModerate ?? false)
117
+
118
+ const hasMore = computed(() => page.value < lastPage.value)
119
+ const canSubmit = computed(() => !posting.value && draft.value.trim().length >= 2)
120
+
121
+ async function load(targetPage = 1) {
122
+ loading.value = true
123
+ error.value = ""
124
+ try {
125
+ const { data } = await fetchCampaignComments(props.keyId, targetPage)
126
+ comments.value = targetPage === 1 ? data.data : [...comments.value, ...data.data]
127
+ page.value = data.meta.current_page
128
+ lastPage.value = data.meta.last_page
129
+ total.value = data.meta.total
130
+ canComment.value = data.can_comment
131
+ canModerate.value = data.can_moderate
132
+ } catch (_) {
133
+ // $i18n from useNuxtApp, not globalThis.$nuxt — the latter is undefined
134
+ // during SSR and on a fresh client load, which rendered the literal
135
+ // string "undefined" where the error message should be.
136
+ error.value = $i18n.t("keys.campaigns.comments.load_error")
137
+ } finally {
138
+ loading.value = false
139
+ }
140
+ }
141
+
142
+ const loadMore = () => load(page.value + 1)
143
+
144
+ async function submit() {
145
+ if (!canSubmit.value) return
146
+ posting.value = true
147
+ error.value = ""
148
+ try {
149
+ await postCampaignComment(props.keyId, draft.value.trim())
150
+ draft.value = ""
151
+ // Reload rather than unshifting locally: the server owns ordering and
152
+ // the permission flags, and a fresh page keeps them honest.
153
+ await load(1)
154
+ } catch (e: any) {
155
+ error.value = e?.response?.data?.message || e?.response?.data?.errors?.body?.[0] || ""
156
+ } finally {
157
+ posting.value = false
158
+ }
159
+ }
160
+
161
+ async function onReply({ uuid, body }: { uuid: string; body: string }) {
162
+ await replyCampaignComment(uuid, body)
163
+ await load(1)
164
+ }
165
+
166
+ async function onEdit({ uuid, body }: { uuid: string; body: string }) {
167
+ await updateCampaignComment(uuid, body)
168
+ await load(page.value)
169
+ }
170
+
171
+ async function onDelete(uuid: string) {
172
+ await deleteCampaignComment(uuid)
173
+ await load(1)
174
+ }
175
+
176
+ async function onHide({ uuid, reason }: { uuid: string; reason?: string }) {
177
+ await hideCampaignComment(uuid, reason)
178
+ await load(page.value)
179
+ }
180
+
181
+ async function onUnhide(uuid: string) {
182
+ await unhideCampaignComment(uuid)
183
+ await load(page.value)
184
+ }
185
+
186
+ onMounted(() => load(1))
187
+ </script>
188
+
189
+ <style lang="scss" scoped>
190
+ /* Theme tokens only — no literal colours. Every variable used here is defined
191
+ in both _light.scss and _dark.scss of agency-frontend, community-frontend and
192
+ tv-frontend, which are the fronts that render this page. Note the themes are
193
+ not "light = pale": agency's .light-mode sets --body-bg to #18181a, so a
194
+ hardcoded translucent-white surface would be invisible in one of them. */
195
+ .campaign-comments {
196
+ margin-top: 2.5rem;
197
+ }
198
+
199
+ .campaign-comments__header {
200
+ margin-bottom: 1.25rem;
201
+ }
202
+
203
+ .campaign-comments__title {
204
+ display: flex;
205
+ align-items: center;
206
+ gap: 0.5rem;
207
+ font-size: 1.25rem;
208
+ font-weight: 700;
209
+ margin: 0;
210
+ color: var(--title-fg);
211
+ }
212
+
213
+ .campaign-comments__count {
214
+ font-size: 0.875rem;
215
+ font-weight: 600;
216
+ padding: 0.125rem 0.5rem;
217
+ color: var(--title-fg);
218
+ background: var(--card-article-bg);
219
+ border: 1px solid var(--card-article-border);
220
+ }
221
+
222
+ .campaign-comments__subtitle {
223
+ margin: 0.25rem 0 0;
224
+ font-size: 0.875rem;
225
+ color: var(--secondary-info-fg);
226
+ }
227
+
228
+ .campaign-comments__composer {
229
+ margin-bottom: 1.5rem;
230
+ }
231
+
232
+ .campaign-comments__input {
233
+ width: 100%;
234
+ padding: 0.75rem;
235
+ /* Deliberately NOT --input-bg/--input-fg: --input-bg is #CCCCCC in both
236
+ themes while --input-fg is #FFFFFF in dark, giving 1.61:1 — unreadable.
237
+ These two invert with the theme and measure above 4.5:1 in both. */
238
+ background: var(--card-article-bg);
239
+ border: 1px solid var(--input-border-color);
240
+ color: var(--title-fg);
241
+ font: inherit;
242
+ resize: vertical;
243
+
244
+ &::placeholder {
245
+ color: var(--secondary-info-fg);
246
+ }
247
+
248
+ &:focus {
249
+ outline: none;
250
+ border-color: var(--key-accent, var(--primary));
251
+ }
252
+ }
253
+
254
+ .campaign-comments__composer-footer {
255
+ display: flex;
256
+ align-items: center;
257
+ justify-content: space-between;
258
+ gap: 0.75rem;
259
+ margin-top: 0.5rem;
260
+ }
261
+
262
+ .campaign-comments__counter {
263
+ font-size: 0.75rem;
264
+ color: var(--secondary-info-fg);
265
+ }
266
+
267
+ /* Matches the .btn convention of the campaign page itself. */
268
+ .campaign-comments__submit {
269
+ height: 44px;
270
+ padding: 0 1.25rem;
271
+ background: var(--key-accent, var(--primary));
272
+ color: #000;
273
+ border: 0;
274
+ font-weight: 600;
275
+ cursor: pointer;
276
+ }
277
+
278
+ .campaign-comments__submit:disabled {
279
+ opacity: 0.5;
280
+ cursor: not-allowed;
281
+ }
282
+
283
+ .campaign-comments__notice,
284
+ .campaign-comments__empty {
285
+ font-size: 0.875rem;
286
+ color: var(--secondary-info-fg);
287
+ margin: 0 0 1.25rem;
288
+ }
289
+
290
+ .campaign-comments__error {
291
+ font-size: 0.875rem;
292
+ color: var(--danger);
293
+ margin: 0 0 1rem;
294
+ }
295
+
296
+ .campaign-comments__list {
297
+ list-style: none;
298
+ margin: 0;
299
+ padding: 0;
300
+ display: grid;
301
+ gap: 1rem;
302
+ }
303
+
304
+ .campaign-comments__skeleton {
305
+ display: grid;
306
+ gap: 1rem;
307
+ }
308
+
309
+ /* Skeleton, not spinner — the list shape is known before the data arrives. */
310
+ .campaign-comments__skeleton-row {
311
+ height: 4.5rem;
312
+ background: var(--card-article-bg);
313
+ animation: campaign-comments-pulse 1.4s ease-in-out infinite;
314
+ }
315
+
316
+ @keyframes campaign-comments-pulse {
317
+ 0%, 100% { opacity: 1; }
318
+ 50% { opacity: 0.45; }
319
+ }
320
+
321
+ @media (prefers-reduced-motion: reduce) {
322
+ .campaign-comments__skeleton-row { animation: none; }
323
+ }
324
+
325
+ .campaign-comments__more {
326
+ margin-top: 1.25rem;
327
+ height: 44px;
328
+ padding: 0 1rem;
329
+ background: transparent;
330
+ border: 1px solid var(--card-article-border);
331
+ color: var(--title-fg);
332
+ cursor: pointer;
333
+ }
334
+
335
+ /* Same breakpoint the campaign page uses for its own stacking. */
336
+ @media (max-width: 768px) {
337
+ .campaign-comments {
338
+ margin-top: 1.75rem;
339
+ }
340
+
341
+ .campaign-comments__title {
342
+ font-size: 1.125rem;
343
+ }
344
+
345
+ .campaign-comments__composer-footer {
346
+ flex-direction: column;
347
+ align-items: stretch;
348
+ }
349
+
350
+ .campaign-comments__submit,
351
+ .campaign-comments__more {
352
+ width: 100%;
353
+ }
354
+ }
355
+ </style>
@@ -0,0 +1,250 @@
1
+ <template>
2
+ <section v-if="canRate || submitted" class="campaign-feedback">
3
+ <header class="campaign-feedback__header">
4
+ <h3 class="campaign-feedback__title">{{ $t("keys.campaigns.feedback.title") }}</h3>
5
+ <p class="campaign-feedback__subtitle">
6
+ {{ $t("keys.campaigns.feedback.subtitle", { game: gameName }) }}
7
+ </p>
8
+ </header>
9
+
10
+ <p v-if="submitted" class="campaign-feedback__done">
11
+ {{ $t("keys.campaigns.feedback.submitted", { game: gameName }) }}
12
+ <a v-if="gameSlug" :href="gameUrl" class="campaign-feedback__link">
13
+ {{ $t("keys.campaigns.feedback.view_on_game_page") }}
14
+ </a>
15
+ </p>
16
+
17
+ <form v-else class="campaign-feedback__form" @submit.prevent="submit">
18
+ <div class="campaign-feedback__stars" role="radiogroup" :aria-label="$t('keys.campaigns.feedback.rating_label')">
19
+ <button
20
+ v-for="star in 5"
21
+ :key="star"
22
+ type="button"
23
+ class="campaign-feedback__star"
24
+ :class="{ 'campaign-feedback__star--on': star <= rating }"
25
+ role="radio"
26
+ :aria-checked="star === rating"
27
+ :aria-label="String(star)"
28
+ @click="rating = star"
29
+ >
30
+
31
+ </button>
32
+ </div>
33
+
34
+ <input
35
+ v-model="title"
36
+ type="text"
37
+ class="campaign-feedback__input"
38
+ :maxlength="191"
39
+ :placeholder="$t('keys.campaigns.feedback.title_placeholder')"
40
+ >
41
+
42
+ <textarea
43
+ v-model="body"
44
+ class="campaign-feedback__input campaign-feedback__textarea"
45
+ rows="4"
46
+ :maxlength="2200"
47
+ :placeholder="$t('keys.campaigns.feedback.body_placeholder')"
48
+ />
49
+
50
+ <!-- Not decoration: the API rejects the submission without it, because
51
+ this rating counts towards the game's public average. -->
52
+ <label class="campaign-feedback__disclosure">
53
+ <input v-model="disclosure" type="checkbox">
54
+ <span>{{ $t("keys.campaigns.feedback.disclosure") }}</span>
55
+ </label>
56
+
57
+ <p class="campaign-feedback__note">{{ $t("keys.campaigns.feedback.average_note") }}</p>
58
+
59
+ <p v-if="error" class="campaign-feedback__error">{{ error }}</p>
60
+
61
+ <button type="submit" class="campaign-feedback__submit" :disabled="!canSubmit">
62
+ {{ sending ? $t("keys.campaigns.feedback.sending") : $t("keys.campaigns.feedback.send") }}
63
+ </button>
64
+ </form>
65
+ </section>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { ref, computed } from "vue"
70
+ import { submitCampaignGameRating } from "../../services/campaignService"
71
+
72
+ const props = defineProps<{
73
+ keyId: number
74
+ /** From the campaign payload — true only for creators who revealed a key. */
75
+ canRate?: boolean
76
+ gameName?: string
77
+ gameSlug?: string
78
+ locale?: string
79
+ }>()
80
+
81
+ const rating = ref(0)
82
+ const title = ref("")
83
+ const body = ref("")
84
+ const disclosure = ref(false)
85
+ const sending = ref(false)
86
+ const submitted = ref(false)
87
+ const error = ref("")
88
+
89
+ const gameName = computed(() => props.gameName || "")
90
+ const gameUrl = computed(() => `/${props.locale || "en"}/game/${props.gameSlug}?tab=ratings`)
91
+
92
+ const canSubmit = computed(() => !sending.value && rating.value > 0 && disclosure.value)
93
+
94
+ async function submit() {
95
+ if (!canSubmit.value) return
96
+ sending.value = true
97
+ error.value = ""
98
+ try {
99
+ await submitCampaignGameRating(props.keyId, {
100
+ rating: rating.value,
101
+ comment_title: title.value || undefined,
102
+ user_comment: body.value || undefined,
103
+ disclosure_acknowledged: disclosure.value,
104
+ })
105
+ submitted.value = true
106
+ } catch (e: any) {
107
+ error.value =
108
+ e?.response?.data?.message ||
109
+ e?.response?.data?.errors?.disclosure_acknowledged?.[0] ||
110
+ e?.response?.data?.errors?.rating?.[0] ||
111
+ ""
112
+ } finally {
113
+ sending.value = false
114
+ }
115
+ }
116
+ </script>
117
+
118
+ <style lang="scss" scoped>
119
+ /* Theme tokens only — see KeyCampaignComments.vue for why no literal colours. */
120
+ .campaign-feedback {
121
+ margin-top: 2.5rem;
122
+ padding: 1.25rem;
123
+ background: var(--card-article-bg);
124
+ border: 1px solid var(--card-article-border);
125
+ }
126
+
127
+ .campaign-feedback__title {
128
+ margin: 0;
129
+ font-size: 1.125rem;
130
+ font-weight: 700;
131
+ color: var(--title-fg);
132
+ }
133
+
134
+ .campaign-feedback__subtitle {
135
+ margin: 0.25rem 0 1rem;
136
+ font-size: 0.875rem;
137
+ color: var(--secondary-info-fg);
138
+ }
139
+
140
+ .campaign-feedback__stars {
141
+ display: flex;
142
+ gap: 0.25rem;
143
+ margin-bottom: 0.875rem;
144
+ }
145
+
146
+ .campaign-feedback__star {
147
+ background: none;
148
+ border: 0;
149
+ padding: 0.125rem;
150
+ font-size: 1.75rem;
151
+ line-height: 1;
152
+ cursor: pointer;
153
+ color: var(--secondary-info-fg);
154
+ }
155
+
156
+ .campaign-feedback__star--on {
157
+ color: var(--warning);
158
+ }
159
+
160
+ .campaign-feedback__input {
161
+ display: block;
162
+ width: 100%;
163
+ margin-bottom: 0.75rem;
164
+ padding: 0.625rem;
165
+ /* See KeyCampaignComments.vue: --input-bg/--input-fg is 1.61:1 in dark. */
166
+ background: var(--card-article-bg);
167
+ border: 1px solid var(--input-border-color);
168
+ color: var(--title-fg);
169
+ font: inherit;
170
+
171
+ &::placeholder {
172
+ color: var(--secondary-info-fg);
173
+ }
174
+
175
+ &:focus {
176
+ outline: none;
177
+ border-color: var(--key-accent, var(--primary));
178
+ }
179
+ }
180
+
181
+ .campaign-feedback__textarea {
182
+ resize: vertical;
183
+ }
184
+
185
+ .campaign-feedback__disclosure {
186
+ display: flex;
187
+ align-items: flex-start;
188
+ gap: 0.5rem;
189
+ font-size: 0.8125rem;
190
+ margin-bottom: 0.5rem;
191
+ color: var(--card-article-title);
192
+ cursor: pointer;
193
+ }
194
+
195
+ .campaign-feedback__note {
196
+ margin: 0 0 0.875rem;
197
+ font-size: 0.75rem;
198
+ color: var(--secondary-info-fg);
199
+ }
200
+
201
+ .campaign-feedback__error {
202
+ margin: 0 0 0.75rem;
203
+ font-size: 0.8125rem;
204
+ color: var(--danger);
205
+ }
206
+
207
+ .campaign-feedback__submit {
208
+ height: 44px;
209
+ padding: 0 1.25rem;
210
+ background: var(--key-accent, var(--primary));
211
+ color: #000;
212
+ border: 0;
213
+ font-weight: 600;
214
+ cursor: pointer;
215
+ }
216
+
217
+ .campaign-feedback__submit:disabled {
218
+ opacity: 0.5;
219
+ cursor: not-allowed;
220
+ }
221
+
222
+ .campaign-feedback__done {
223
+ margin: 0;
224
+ font-size: 0.9375rem;
225
+ color: var(--card-article-title);
226
+ }
227
+
228
+ .campaign-feedback__link {
229
+ margin-left: 0.5rem;
230
+ color: var(--key-accent, var(--primary));
231
+ text-decoration: underline;
232
+ }
233
+
234
+ @media (max-width: 768px) {
235
+ .campaign-feedback {
236
+ margin-top: 1.75rem;
237
+ padding: 1rem;
238
+ }
239
+
240
+ /* Bigger hit area: 28px stars at 4px gaps miss constantly on touch. */
241
+ .campaign-feedback__star {
242
+ font-size: 2rem;
243
+ padding: 0.25rem;
244
+ }
245
+
246
+ .campaign-feedback__submit {
247
+ width: 100%;
248
+ }
249
+ }
250
+ </style>
package/locales/de.json CHANGED
@@ -22,13 +22,54 @@
22
22
  "no_requests": "Sie haben noch keine Keys angefordert"
23
23
  },
24
24
  "campaigns": {
25
+ "feedback": {
26
+ "title": "Dieses Spiel bewerten",
27
+ "subtitle": "Du hast einen Key für {game} erhalten — sag anderen Creators, was du davon hältst.",
28
+ "rating_label": "Deine Bewertung",
29
+ "title_placeholder": "In einem Satz zusammengefasst (optional)",
30
+ "body_placeholder": "Was funktioniert, was nicht, für wen es ist (optional)",
31
+ "disclosure": "Ich habe einen Key aus dieser Kampagne erhalten und diese Bewertung ist meine ehrliche Meinung.",
32
+ "average_note": "Deine Bewertung zählt für den öffentlichen Durchschnitt des Spiels und wird mit einem Hinweis angezeigt, dass du einen Key erhalten hast.",
33
+ "send": "Bewertung veröffentlichen",
34
+ "sending": "Wird veröffentlicht…",
35
+ "submitted": "Deine Bewertung ist jetzt auf der Seite von {game}.",
36
+ "view_on_game_page": "Dort ansehen"
37
+ },
38
+ "comments": {
39
+ "title": "Creator-Feedback",
40
+ "subtitle": "Eindrücke von Creators, die mit einem Key aus dieser Kampagne gespielt haben.",
41
+ "placeholder": "Erzähl, was du vom Spiel hältst…",
42
+ "send": "Veröffentlichen",
43
+ "sending": "Wird veröffentlicht…",
44
+ "empty": "Noch kein Feedback. Teile als Erste:r deine Eindrücke.",
45
+ "load_more": "Mehr laden",
46
+ "load_error": "Das Feedback konnte nicht geladen werden. Versuch es gleich noch einmal.",
47
+ "sign_in_notice": "Melde dich an, um mitzureden.",
48
+ "creators_only_notice": "Nur Creators, die einen Key aus dieser Kampagne erhalten haben, können hier posten.",
49
+ "reply": "Antworten",
50
+ "reply_placeholder": "Antwort schreiben…",
51
+ "edited": "bearbeitet",
52
+ "hide": "Ausblenden",
53
+ "unhide": "Wieder einblenden",
54
+ "hidden_badge": "Ausgeblendet",
55
+ "unknown_author": "Creator",
56
+ "save": "Speichern",
57
+ "cancel": "Abbrechen",
58
+ "edit": "Bearbeiten",
59
+ "delete": "Löschen"
60
+ },
25
61
  "title": "Key Campaigns",
26
62
  "my_requests": "My Requests",
63
+ "calendar_export_label": "Diese Fristen in deinen Kalender übernehmen:",
64
+ "calendar_export_ics": "Kalender (.ics)",
65
+ "calendar_export_csv": "Tabelle (.csv)",
66
+ "calendar_export_busy": "Wird vorbereitet...",
67
+ "calendar_export_error": "Die Datei konnte nicht erstellt werden. Bitte versuche es erneut.",
27
68
  "all": "All",
28
69
  "details": "Details",
29
70
  "see_campaign": "See campaign",
30
71
  "see_content": "See content",
31
- "redeem": "Redeem",
72
+ "redeem": "Request",
32
73
  "load_more": "Load more",
33
74
  "no_results": "No campaigns found",
34
75
  "days": "days",
@@ -67,7 +108,7 @@
67
108
  "title_span_0": "ended",
68
109
  "obs_0": "The redemption period for the key has expired.",
69
110
  "title_1": "Now you need to",
70
- "title_span_1": "redeem the key",
111
+ "title_span_1": "request the key",
71
112
  "obs_1": "Check the campaign requirements before requesting your key(s)",
72
113
  "title_2": "Now just",
73
114
  "title_span_2": "wait",
@@ -131,7 +172,7 @@
131
172
  "minutes": "min",
132
173
  "seconds": "sec",
133
174
  "days_hours": "days and hours",
134
- "redeem": "Redeem key",
175
+ "redeem": "Request key",
135
176
  "actions": "Complementary actions",
136
177
  "no_actions": "No actions configured",
137
178
  "requirements": "Requirements",
@@ -195,6 +236,7 @@
195
236
  "required_data": "Erforderliche Angaben",
196
237
  "already_requested": "Sie haben bereits eine Anfrage für diese Kampagne gestellt.",
197
238
  "sold_out": "Alle Keys für diese Kampagne wurden bereits eingelöst.",
239
+ "options_unavailable": "Diese Kampagne hat noch Keys, aber es ist noch keine Plattform oder Region dafür eingerichtet, daher können keine Anfragen gesendet werden. Das Studio muss die Einrichtung abschließen — auf deiner Seite ist alles in Ordnung.",
198
240
  "keys_left": "{n} Key übrig | {n} Keys übrig",
199
241
  "keys_low_stock": "Letzter Key | Letzte {n}",
200
242
  "back_to_campaign": "Zur Kampagne zurückkehren",