@micha.bigler/ui-core-micha 1.4.18 → 1.4.20

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.
@@ -1,74 +1,37 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // src/components/ProfileComponent.jsx
3
- import React, { useEffect, useState } from 'react';
4
- import axios from 'axios';
3
+ import React, { useEffect, useState, useContext } from 'react';
5
4
  import { Box, Stack, TextField, FormControlLabel, Checkbox, Button, CircularProgress, Alert, Typography, } from '@mui/material';
6
5
  import { useTranslation } from 'react-i18next';
7
- import { USERS_BASE } from '../auth/authConfig';
8
- /**
9
- * ProfileComponent
10
- *
11
- * - Loads the current user from `${USERS_BASE}/current/`
12
- * - Shows basic fields (name, email, etc.)
13
- * - Optional privacy/cookie checkboxes
14
- * - Calls onSubmit(payload) to save changes
15
- */
16
- export function ProfileComponent({ onLoad, onSubmit, submitText, // optional, falls der Host etwas Eigenes anzeigen will
17
- showName = true, showPrivacy = true, showCookies = true, }) {
6
+ // WICHTIG: Importiere den Context, um die bereits geladenen Daten zu nutzen
7
+ import { AuthContext } from '../auth/AuthContext';
8
+ export function ProfileComponent({ onSubmit, submitText, showName = true, showPrivacy = true, showCookies = true, }) {
18
9
  const { t } = useTranslation();
19
- const [loading, setLoading] = useState(true);
10
+ // WICHTIG: Wir holen den User direkt aus dem globalen State
11
+ // Das verhindert den doppelten Request und den ReferenceError
12
+ const { user, loading: authLoading } = useContext(AuthContext);
20
13
  const [saving, setSaving] = useState(false);
21
14
  const [errorKey, setErrorKey] = useState(null);
22
15
  const [successKey, setSuccessKey] = useState(null);
23
- const [userId, setUserId] = useState(null);
16
+ // Lokaler State für das Formular
24
17
  const [username, setUsername] = useState('');
25
18
  const [email, setEmail] = useState('');
26
19
  const [firstName, setFirstName] = useState('');
27
20
  const [lastName, setLastName] = useState('');
28
21
  const [acceptedPrivacy, setAcceptedPrivacy] = useState(false);
29
22
  const [acceptedCookies, setAcceptedCookies] = useState(false);
30
- // Load current user on mount
23
+ // Synchronisiere Formular-Daten, sobald der User aus dem Context da ist
31
24
  useEffect(() => {
32
- let mounted = true;
33
- const loadUser = async () => {
34
- var _a, _b, _c, _d, _e;
35
- setLoading(true);
36
- setErrorKey(null);
37
- setSuccessKey(null);
38
- try {
39
- const res = await apiClient.get(`${USERS_BASE}/current/`, {
40
- withCredentials: true,
41
- });
42
- if (!mounted)
43
- return;
44
- const data = res.data;
45
- setUserId((_a = data.id) !== null && _a !== void 0 ? _a : null);
46
- setUsername((_b = data.username) !== null && _b !== void 0 ? _b : '');
47
- setEmail((_c = data.email) !== null && _c !== void 0 ? _c : '');
48
- setFirstName((_d = data.first_name) !== null && _d !== void 0 ? _d : '');
49
- setLastName((_e = data.last_name) !== null && _e !== void 0 ? _e : '');
50
- setAcceptedPrivacy(Boolean(data.accepted_privacy_statement));
51
- setAcceptedCookies(Boolean(data.accepted_convenience_cookies));
52
- if (onLoad) {
53
- onLoad(data);
54
- }
55
- }
56
- catch (err) {
57
- if (!mounted)
58
- return;
59
- // Kein Leak von Backend-Texten → stabiler Code
60
- setErrorKey('Auth.PROFILE_LOAD_FAILED');
61
- }
62
- finally {
63
- if (mounted)
64
- setLoading(false);
65
- }
66
- };
67
- loadUser();
68
- return () => {
69
- mounted = false;
70
- };
71
- }, [onLoad]);
25
+ var _a, _b, _c, _d;
26
+ if (user) {
27
+ setUsername((_a = user.username) !== null && _a !== void 0 ? _a : '');
28
+ setEmail((_b = user.email) !== null && _b !== void 0 ? _b : '');
29
+ setFirstName((_c = user.first_name) !== null && _c !== void 0 ? _c : '');
30
+ setLastName((_d = user.last_name) !== null && _d !== void 0 ? _d : '');
31
+ setAcceptedPrivacy(Boolean(user.accepted_privacy_statement));
32
+ setAcceptedCookies(Boolean(user.accepted_convenience_cookies));
33
+ }
34
+ }, [user]);
72
35
  const handleSubmit = async (event) => {
73
36
  event.preventDefault();
74
37
  if (!onSubmit)
@@ -79,7 +42,6 @@ showName = true, showPrivacy = true, showCookies = true, }) {
79
42
  const payload = {
80
43
  first_name: firstName,
81
44
  last_name: lastName,
82
- // Serializer-Felder sind flach (`source="profile.*"` im Backend)
83
45
  accepted_privacy_statement: acceptedPrivacy,
84
46
  accepted_convenience_cookies: acceptedCookies,
85
47
  };
@@ -88,20 +50,22 @@ showName = true, showPrivacy = true, showCookies = true, }) {
88
50
  setSuccessKey('Profile.UPDATE_SUCCESS');
89
51
  }
90
52
  catch (err) {
91
- // Wenn onSubmit z. B. authApi.updateUserProfile nutzt,
92
- // kommt hier ein normalisierter Error mit .code
53
+ // eslint-disable-next-line no-console
54
+ console.error("Profile update error:", err);
93
55
  setErrorKey(err.code || 'Auth.PROFILE_UPDATE_FAILED');
94
56
  }
95
57
  finally {
96
58
  setSaving(false);
97
59
  }
98
60
  };
99
- if (loading) {
61
+ // Wenn der AuthContext noch initial lädt
62
+ if (authLoading) {
100
63
  return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', py: 4 }, children: _jsx(CircularProgress, {}) }));
101
64
  }
102
- // Fallback für den Button-Text:
103
- // - Wenn submitText übergeben wurde, direkt anzeigen (Host kümmert sich selbst um Übersetzung)
104
- // - Sonst i18n-Schlüssel verwenden
65
+ // Falls kein User eingeloggt ist
66
+ if (!user) {
67
+ return (_jsx(Alert, { severity: "warning", children: t('Auth.NOT_LOGGED_IN', 'User not logged in.') }));
68
+ }
105
69
  const submitLabel = submitText || t('Profile.SAVE_BUTTON');
106
70
  const submitLabelLoading = t('Profile.SAVE_BUTTON_LOADING');
107
71
  return (_jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { maxWidth: 600, display: 'flex', flexDirection: 'column', gap: 2 }, children: [errorKey && (_jsx(Alert, { severity: "error", children: t(errorKey) })), successKey && (_jsx(Alert, { severity: "success", children: t(successKey) })), _jsxs(Stack, { spacing: 2, children: [_jsx(TextField, { label: t('Profile.USERNAME_LABEL'), value: username, fullWidth: true, disabled: true }), _jsx(TextField, { label: t('Auth.EMAIL_LABEL'), type: "email", value: email, fullWidth: true, disabled: true })] }), showName && (_jsxs(Stack, { spacing: 2, direction: { xs: 'column', sm: 'row' }, children: [_jsx(TextField, { label: t('Profile.FIRST_NAME_LABEL'), value: firstName, onChange: (e) => setFirstName(e.target.value), fullWidth: true }), _jsx(TextField, { label: t('Profile.LAST_NAME_LABEL'), value: lastName, onChange: (e) => setLastName(e.target.value), fullWidth: true })] })), (showPrivacy || showCookies) && (_jsxs(Box, { sx: { mt: 1 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: t('Profile.PRIVACY_SECTION_TITLE') }), _jsxs(Stack, { spacing: 1, children: [showPrivacy && (_jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: acceptedPrivacy, onChange: (e) => setAcceptedPrivacy(e.target.checked) }), label: t('Profile.ACCEPT_PRIVACY_LABEL') })), showCookies && (_jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: acceptedCookies, onChange: (e) => setAcceptedCookies(e.target.checked) }), label: t('Profile.ACCEPT_COOKIES_LABEL') }))] })] })), _jsx(Box, { sx: { mt: 2 }, children: _jsx(Button, { type: "submit", variant: "contained", disabled: saving, children: saving ? submitLabelLoading : submitLabel }) })] }));
@@ -72,7 +72,7 @@ const SupportRecoveryRequestsTab = () => {
72
72
  }
73
73
  return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Support.RECOVERY_REQUESTS_TITLE', 'Account recovery requests') }), _jsx(Typography, { variant: "body2", sx: { mb: 2 }, children: t('Support.RECOVERY_REQUESTS_DESCRIPTION', 'Users who cannot complete MFA can request support. You can review their request and approve or reject it.') }), errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey) })), _jsxs(Stack, { direction: "row", spacing: 2, sx: { mb: 2 }, children: [_jsx(Button, { variant: statusFilter === 'pending' ? 'contained' : 'outlined', size: "small", onClick: () => setStatusFilter('pending'), children: t('Support.RECOVERY_FILTER_PENDING', 'Open') }), _jsx(Button, { variant: statusFilter === 'approved' ? 'contained' : 'outlined', size: "small", onClick: () => setStatusFilter('approved'), children: t('Support.RECOVERY_FILTER_APPROVED', 'Approved') }), _jsx(Button, { variant: statusFilter === 'rejected' ? 'contained' : 'outlined', size: "small", onClick: () => setStatusFilter('rejected'), children: t('Support.RECOVERY_FILTER_REJECTED', 'Rejected') })] }), requests.length === 0 ? (_jsx(Typography, { variant: "body2", children: t('Support.RECOVERY_REQUESTS_EMPTY', 'No recovery requests for this filter.') })) : (_jsx(TableContainer, { component: Paper, children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: t('Support.RECOVERY_COL_CREATED', 'Created') }), _jsx(TableCell, { children: t('Support.RECOVERY_USER', 'User') }), _jsx(TableCell, { children: t('Support.RECOVERY_COL_STATUS', 'Status') }), _jsx(TableCell, { children: t('Support.RECOVERY_COL_ACTIONS', 'Actions') })] }) }), _jsx(TableBody, { children: requests.map((req) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: req.created_at
74
74
  ? new Date(req.created_at).toLocaleString()
75
- : '-' }), _jsx(TableCell, { children: req.user_email || req.user }), _jsx(TableCell, { children: req.status }), _jsx(TableCell, { children: _jsx(Button, { variant: "contained", size: "small", onClick: () => openDialog(req), disabled: req.status !== 'pending', children: t('Support.RECOVERY_ACTION_REVIEW', 'Review') }) })] }, req.id))) })] }) })), _jsxs(Dialog, { open: dialogOpen, onClose: closeDialog, fullWidth: true, maxWidth: "sm", children: [_jsx(DialogTitle, { children: t('Support.RECOVERY_REVIEW_DIALOG_TITLE', 'Review recovery request') }), _jsxs(DialogContent, { dividers: true, children: [selectedRequest && (_jsxs(Box, { sx: { mb: 2 }, children: [_jsxs(Typography, { variant: "body2", sx: { mb: 1 }, children: [_jsxs("strong", { children: [t('Support.RECOVERY_USER', 'User'), ":"] }), ' ', selectedRequest.user_email || selectedRequest.user] }), _jsxs(Typography, { variant: "body2", sx: { mb: 1 }, children: [_jsxs("strong", { children: [t('Support.RECOVERY_REVIEW_CREATED', 'Requested at'), ":"] }), ' ', selectedRequest.created_at
75
+ : '-' }), _jsx(TableCell, { children: req.user_email || req.user }), _jsx(TableCell, { children: t(`Support.RECOVERY_STATUS_${req.status}`, req.status) }), _jsx(TableCell, { children: _jsx(Button, { variant: "contained", size: "small", onClick: () => openDialog(req), disabled: req.status !== 'pending', children: t('Support.RECOVERY_ACTION_REVIEW', 'Review') }) })] }, req.id))) })] }) })), _jsxs(Dialog, { open: dialogOpen, onClose: closeDialog, fullWidth: true, maxWidth: "sm", children: [_jsx(DialogTitle, { children: t('Support.RECOVERY_REVIEW_DIALOG_TITLE', 'Review recovery request') }), _jsxs(DialogContent, { dividers: true, children: [selectedRequest && (_jsxs(Box, { sx: { mb: 2 }, children: [_jsxs(Typography, { variant: "body2", sx: { mb: 1 }, children: [_jsxs("strong", { children: [t('Support.RECOVERY_USER', 'User'), ":"] }), ' ', selectedRequest.user_email || selectedRequest.user] }), _jsxs(Typography, { variant: "body2", sx: { mb: 1 }, children: [_jsxs("strong", { children: [t('Support.RECOVERY_REVIEW_CREATED', 'Requested at'), ":"] }), ' ', selectedRequest.created_at
76
76
  ? new Date(selectedRequest.created_at).toLocaleString()
77
77
  : '-'] }), _jsx(Typography, { variant: "body2", sx: { mb: 1 }, children: _jsxs("strong", { children: [t('Support.RECOVERY_REVIEW_MESSAGE', 'User’s explanation'), ":"] }) }), _jsx(Box, { sx: {
78
78
  border: 1,
@@ -931,4 +931,29 @@ export const authTranslations = {
931
931
  "fr": "Si un compte existe avec cette adresse e-mail, votre demande a été transmise au support.",
932
932
  "en": "If an account with this email exists, your request has been forwarded to support."
933
933
  },
934
+ "Support.RECOVERY_STATUS_pending": {
935
+ "de": "Offen",
936
+ "fr": "En attente",
937
+ "en": "Pending"
938
+ },
939
+ "Support.RECOVERY_STATUS_approved": {
940
+ "de": "Genehmigt",
941
+ "fr": "Approuvée",
942
+ "en": "Approved"
943
+ },
944
+ "Support.RECOVERY_STATUS_rejected": {
945
+ "de": "Abgelehnt",
946
+ "fr": "Refusée",
947
+ "en": "Rejected"
948
+ },
949
+ "Support.RECOVERY_STATUS_completed": {
950
+ "de": "Abgeschlossen",
951
+ "fr": "Terminée",
952
+ "en": "Completed"
953
+ },
954
+ "Support.RECOVERY_STATUS_expired": {
955
+ "de": "Abgelaufen",
956
+ "fr": "Expirée",
957
+ "en": "Expired"
958
+ }
934
959
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "1.4.18",
3
+ "version": "1.4.20",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "private": false,
@@ -1,6 +1,5 @@
1
1
  // src/components/ProfileComponent.jsx
2
- import React, { useEffect, useState } from 'react';
3
- import axios from 'axios';
2
+ import React, { useEffect, useState, useContext } from 'react';
4
3
  import {
5
4
  Box,
6
5
  Stack,
@@ -13,83 +12,45 @@ import {
13
12
  Typography,
14
13
  } from '@mui/material';
15
14
  import { useTranslation } from 'react-i18next';
16
- import { USERS_BASE } from '../auth/authConfig';
17
-
18
- /**
19
- * ProfileComponent
20
- *
21
- * - Loads the current user from `${USERS_BASE}/current/`
22
- * - Shows basic fields (name, email, etc.)
23
- * - Optional privacy/cookie checkboxes
24
- * - Calls onSubmit(payload) to save changes
25
- */
15
+ // WICHTIG: Importiere den Context, um die bereits geladenen Daten zu nutzen
16
+ import { AuthContext } from '../auth/AuthContext';
17
+
26
18
  export function ProfileComponent({
27
- onLoad,
28
19
  onSubmit,
29
- submitText, // optional, falls der Host etwas Eigenes anzeigen will
20
+ submitText,
30
21
  showName = true,
31
22
  showPrivacy = true,
32
23
  showCookies = true,
33
24
  }) {
34
25
  const { t } = useTranslation();
26
+
27
+ // WICHTIG: Wir holen den User direkt aus dem globalen State
28
+ // Das verhindert den doppelten Request und den ReferenceError
29
+ const { user, loading: authLoading } = useContext(AuthContext);
35
30
 
36
- const [loading, setLoading] = useState(true);
37
31
  const [saving, setSaving] = useState(false);
38
-
39
32
  const [errorKey, setErrorKey] = useState(null);
40
33
  const [successKey, setSuccessKey] = useState(null);
41
34
 
42
- const [userId, setUserId] = useState(null);
35
+ // Lokaler State für das Formular
43
36
  const [username, setUsername] = useState('');
44
37
  const [email, setEmail] = useState('');
45
-
46
38
  const [firstName, setFirstName] = useState('');
47
39
  const [lastName, setLastName] = useState('');
48
-
49
40
  const [acceptedPrivacy, setAcceptedPrivacy] = useState(false);
50
41
  const [acceptedCookies, setAcceptedCookies] = useState(false);
51
42
 
52
- // Load current user on mount
43
+ // Synchronisiere Formular-Daten, sobald der User aus dem Context da ist
53
44
  useEffect(() => {
54
- let mounted = true;
55
-
56
- const loadUser = async () => {
57
- setLoading(true);
58
- setErrorKey(null);
59
- setSuccessKey(null);
60
-
61
- try {
62
- const res = await apiClient.get(`${USERS_BASE}/current/`, {
63
- withCredentials: true,
64
- });
65
- if (!mounted) return;
66
-
67
- const data = res.data;
68
- setUserId(data.id ?? null);
69
- setUsername(data.username ?? '');
70
- setEmail(data.email ?? '');
71
- setFirstName(data.first_name ?? '');
72
- setLastName(data.last_name ?? '');
73
- setAcceptedPrivacy(Boolean(data.accepted_privacy_statement));
74
- setAcceptedCookies(Boolean(data.accepted_convenience_cookies));
75
-
76
- if (onLoad) {
77
- onLoad(data);
78
- }
79
- } catch (err) {
80
- if (!mounted) return;
81
- // Kein Leak von Backend-Texten → stabiler Code
82
- setErrorKey('Auth.PROFILE_LOAD_FAILED');
83
- } finally {
84
- if (mounted) setLoading(false);
85
- }
86
- };
87
-
88
- loadUser();
89
- return () => {
90
- mounted = false;
91
- };
92
- }, [onLoad]);
45
+ if (user) {
46
+ setUsername(user.username ?? '');
47
+ setEmail(user.email ?? '');
48
+ setFirstName(user.first_name ?? '');
49
+ setLastName(user.last_name ?? '');
50
+ setAcceptedPrivacy(Boolean(user.accepted_privacy_statement));
51
+ setAcceptedCookies(Boolean(user.accepted_convenience_cookies));
52
+ }
53
+ }, [user]);
93
54
 
94
55
  const handleSubmit = async (event) => {
95
56
  event.preventDefault();
@@ -102,7 +63,6 @@ export function ProfileComponent({
102
63
  const payload = {
103
64
  first_name: firstName,
104
65
  last_name: lastName,
105
- // Serializer-Felder sind flach (`source="profile.*"` im Backend)
106
66
  accepted_privacy_statement: acceptedPrivacy,
107
67
  accepted_convenience_cookies: acceptedCookies,
108
68
  };
@@ -111,15 +71,16 @@ export function ProfileComponent({
111
71
  await onSubmit(payload);
112
72
  setSuccessKey('Profile.UPDATE_SUCCESS');
113
73
  } catch (err) {
114
- // Wenn onSubmit z. B. authApi.updateUserProfile nutzt,
115
- // kommt hier ein normalisierter Error mit .code
74
+ // eslint-disable-next-line no-console
75
+ console.error("Profile update error:", err);
116
76
  setErrorKey(err.code || 'Auth.PROFILE_UPDATE_FAILED');
117
77
  } finally {
118
78
  setSaving(false);
119
79
  }
120
80
  };
121
81
 
122
- if (loading) {
82
+ // Wenn der AuthContext noch initial lädt
83
+ if (authLoading) {
123
84
  return (
124
85
  <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
125
86
  <CircularProgress />
@@ -127,9 +88,15 @@ export function ProfileComponent({
127
88
  );
128
89
  }
129
90
 
130
- // Fallback für den Button-Text:
131
- // - Wenn submitText übergeben wurde, direkt anzeigen (Host kümmert sich selbst um Übersetzung)
132
- // - Sonst i18n-Schlüssel verwenden
91
+ // Falls kein User eingeloggt ist
92
+ if (!user) {
93
+ return (
94
+ <Alert severity="warning">
95
+ {t('Auth.NOT_LOGGED_IN', 'User not logged in.')}
96
+ </Alert>
97
+ );
98
+ }
99
+
133
100
  const submitLabel = submitText || t('Profile.SAVE_BUTTON');
134
101
  const submitLabelLoading = t('Profile.SAVE_BUTTON_LOADING');
135
102
 
@@ -150,7 +117,7 @@ export function ProfileComponent({
150
117
  </Alert>
151
118
  )}
152
119
 
153
- {/* Basic data (always visible, usually read-only) */}
120
+ {/* Read-Only Felder */}
154
121
  <Stack spacing={2}>
155
122
  <TextField
156
123
  label={t('Profile.USERNAME_LABEL')}
@@ -167,7 +134,7 @@ export function ProfileComponent({
167
134
  />
168
135
  </Stack>
169
136
 
170
- {/* Name only if showName == true */}
137
+ {/* Editierbare Felder */}
171
138
  {showName && (
172
139
  <Stack spacing={2} direction={{ xs: 'column', sm: 'row' }}>
173
140
  <TextField
@@ -185,7 +152,6 @@ export function ProfileComponent({
185
152
  </Stack>
186
153
  )}
187
154
 
188
- {/* Privacy / Cookies */}
189
155
  {(showPrivacy || showCookies) && (
190
156
  <Box sx={{ mt: 1 }}>
191
157
  <Typography variant="subtitle1" gutterBottom>
@@ -233,4 +199,4 @@ export function ProfileComponent({
233
199
  );
234
200
  }
235
201
 
236
- export default ProfileComponent;
202
+ export default ProfileComponent;
@@ -175,7 +175,9 @@ const SupportRecoveryRequestsTab = () => {
175
175
  : '-'}
176
176
  </TableCell>
177
177
  <TableCell>{req.user_email || req.user}</TableCell>
178
- <TableCell>{req.status}</TableCell>
178
+ <TableCell>
179
+ {t(`Support.RECOVERY_STATUS_${req.status}`, req.status)}
180
+ </TableCell>
179
181
  <TableCell>
180
182
  <Button
181
183
  variant="contained"
@@ -978,4 +978,29 @@ export const authTranslations = {
978
978
  "fr": "Si un compte existe avec cette adresse e-mail, votre demande a été transmise au support.",
979
979
  "en": "If an account with this email exists, your request has been forwarded to support."
980
980
  },
981
+ "Support.RECOVERY_STATUS_pending": {
982
+ "de": "Offen",
983
+ "fr": "En attente",
984
+ "en": "Pending"
985
+ },
986
+ "Support.RECOVERY_STATUS_approved": {
987
+ "de": "Genehmigt",
988
+ "fr": "Approuvée",
989
+ "en": "Approved"
990
+ },
991
+ "Support.RECOVERY_STATUS_rejected": {
992
+ "de": "Abgelehnt",
993
+ "fr": "Refusée",
994
+ "en": "Rejected"
995
+ },
996
+ "Support.RECOVERY_STATUS_completed": {
997
+ "de": "Abgeschlossen",
998
+ "fr": "Terminée",
999
+ "en": "Completed"
1000
+ },
1001
+ "Support.RECOVERY_STATUS_expired": {
1002
+ "de": "Abgelaufen",
1003
+ "fr": "Expirée",
1004
+ "en": "Expired"
1005
+ }
981
1006
  };