Merged PR 585: 画面実装(ポップアップ表示)

## 概要
[Task3120: 画面実装(ポップアップ表示)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3120)

- ディクテーション画面から開くバックアップポップアップを実装しました。
  - ポップアップを開いて、対象タスクが表示されるところまでの実装です。
  - バックアップボタンの挙動は対象外です。

## レビューポイント
- チェックボックスの挙動は適切でしょうか?
  - SharePointの挙動を参考にしています。
- デザインの適用で不自然な点はないでしょうか?

## UIの変更
- [Task3120](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3120?csf=1&web=1&e=KxCeU4)

## 動作確認状況
- ローカルで確認
This commit is contained in:
makabe.t 2023-11-20 07:52:43 +00:00
parent 87dd0f6d6b
commit 99ac6be9fd
16 changed files with 485 additions and 13 deletions

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 28.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#282828;}
</style>
<path class="st0" d="M10.7,38.9c-0.7,0-1.3-0.3-1.9-0.8C8.3,37.6,8,37,8,36.3V13.8c0-0.7,0.3-1.3,0.8-1.9c0.5-0.5,1.2-0.8,1.9-0.8
h9.1v2.7h-9.1v22.5h23.6v-9.1h2.7v9.1c0,0.7-0.3,1.3-0.8,1.9s-1.2,0.8-1.9,0.8H10.7z M19.6,30.2l-1.9-1.9l17.7-17.7h-9.7V7.9H40
v14.2h-2.7v-9.7L19.6,30.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 707 B

View File

@ -95,3 +95,7 @@ export const INIT_DISPLAY_INFO: DisplayInfoType = {
OptionItem9: false,
OptionItem10: false,
} as const;
export const BACKUP_POPUP_LIST_SIZE = 10;
export const BACKUP_POPUP_LIST_STATUS = [STATUS.FINISHED, STATUS.BACKUP];

View File

@ -3,6 +3,7 @@ import { Assignee, Task } from "api/api";
import { DictationState } from "./state";
import {
getSortColumnAsync,
listBackupPopupTasksAsync,
listTasksAsync,
listTypistGroupsAsync,
listTypistsAsync,
@ -27,6 +28,11 @@ const initialState: DictationState = {
tasks: [],
typists: [],
typistGroups: [],
backup: {
tasks: [],
offset: 0,
total: 0,
},
},
apps: {
displayInfo: INIT_DISPLAY_INFO,
@ -38,6 +44,7 @@ const initialState: DictationState = {
pool: [],
},
isLoading: true,
isBackupListLoading: false,
},
};
@ -97,6 +104,30 @@ export const dictationSlice = createSlice({
state.apps.assignee.selected = selected;
state.apps.assignee.pool = pool;
},
changeBackupTaskChecked: (
state,
action: PayloadAction<{ audioFileId: number; checked: boolean }>
) => {
const { audioFileId, checked } = action.payload;
const tasks = state.domain.backup.tasks.map((task) => {
if (task.audioFileId === audioFileId) {
task.checked = checked;
}
return task;
});
state.domain.backup.tasks = tasks;
},
changeBackupTaskAllCheched: (
state,
action: PayloadAction<{ checked: boolean }>
) => {
const { checked } = action.payload;
const tasks = state.domain.backup.tasks.map((task) => {
task.checked = checked;
return task;
});
state.domain.backup.tasks = tasks;
},
},
extraReducers: (builder) => {
builder.addCase(listTasksAsync.pending, (state) => {
@ -145,6 +176,23 @@ export const dictationSlice = createSlice({
builder.addCase(playbackAsync.rejected, (state) => {
state.apps.isLoading = false;
});
builder.addCase(listBackupPopupTasksAsync.pending, (state) => {
state.apps.isBackupListLoading = true;
});
builder.addCase(listBackupPopupTasksAsync.fulfilled, (state, action) => {
const { offset, total, tasks } = action.payload;
state.domain.backup.tasks = tasks.map((task) => ({
...task,
checked: true,
}));
state.domain.backup.offset = offset;
state.domain.backup.total = total;
state.apps.isBackupListLoading = false;
});
builder.addCase(listBackupPopupTasksAsync.rejected, (state) => {
state.apps.isBackupListLoading = false;
});
},
});
@ -154,6 +202,8 @@ export const {
changeParamName,
changeSelectedTask,
changeAssignee,
changeBackupTaskChecked,
changeBackupTaskAllCheched,
} = dictationSlice.actions;
export default dictationSlice.reducer;

View File

@ -3,3 +3,4 @@ export * from "./constants";
export * from "./selectors";
export * from "./dictationSlice";
export * from "./operations";
export * from "./types";

View File

@ -15,6 +15,8 @@ import {
import { Configuration } from "../../api/configuration";
import { ErrorObject, createErrorObject } from "../../common/errors";
import {
BACKUP_POPUP_LIST_SIZE,
BACKUP_POPUP_LIST_STATUS,
DIRECTION,
DirectionType,
SORTABLE_COLUMN,
@ -352,3 +354,53 @@ export const playbackAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error });
}
});
export const listBackupPopupTasksAsync = createAsyncThunk<
TasksResponse,
{
// パラメータ
offset: number;
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("dictations/listBackupPopupTasksAsync", async (args, thunkApi) => {
const { offset } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
const { configuration } = state.auth;
const accessToken = getAccessToken(state.auth);
const config = new Configuration(configuration);
const tasksApi = new TasksApi(config);
try {
const res = await tasksApi.getTasks(
BACKUP_POPUP_LIST_SIZE,
offset,
BACKUP_POPUP_LIST_STATUS.join(","), // ステータスはFinished,Backupのみ
DIRECTION.ASC,
SORTABLE_COLUMN.Status,
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
return res.data;
} catch (e) {
// e ⇒ errorObjectに変換"
const error = createErrorObject(e);
thunkApi.dispatch(
openSnackbar({
level: "error",
message: getTranslationID("common.message.internalServerError"),
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -1,5 +1,6 @@
import { RootState } from "app/store";
import { ceil, floor } from "lodash";
import { BACKUP_POPUP_LIST_SIZE } from "./constants";
export const selectTasks = (state: RootState) => state.dictation.domain.tasks;
@ -41,3 +42,29 @@ export const selectPoolTranscriptionists = (state: RootState) =>
export const selectIsLoading = (state: RootState) =>
state.dictation.apps.isLoading;
export const selectBackupTasks = (state: RootState) =>
state.dictation.domain.backup.tasks;
export const selectTotalBackupPage = (state: RootState) => {
const { total } = state.dictation.domain.backup;
const page = ceil(total / BACKUP_POPUP_LIST_SIZE);
return page;
};
export const selectCurrentBackupPage = (state: RootState) => {
const { offset } = state.dictation.domain.backup;
const page = floor(offset / BACKUP_POPUP_LIST_SIZE) + 1;
return page;
};
export const selectBackupTotal = (state: RootState) =>
state.dictation.domain.backup.total;
export const selectBackupAllChecked = (state: RootState) => {
const { tasks } = state.dictation.domain.backup;
return tasks.every((task) => task.checked);
};
export const selectIsBackupListLoading = (state: RootState) =>
state.dictation.apps.isBackupListLoading;

View File

@ -4,6 +4,7 @@ import {
DisplayInfoType,
SortableColumnType,
} from "./constants";
import { BackupTask } from "./types";
export interface DictationState {
domain: Domain;
@ -17,6 +18,7 @@ export interface Domain {
tasks: Task[];
typists: Typist[];
typistGroups: TypistGroup[];
backup: Backup;
}
export interface Apps {
@ -29,4 +31,11 @@ export interface Apps {
pool: Assignee[];
};
isLoading: boolean;
isBackupListLoading: boolean;
}
export interface Backup {
tasks: BackupTask[];
offset: number;
total: number;
}

View File

@ -0,0 +1,5 @@
import { Task } from "api";
export interface BackupTask extends Task {
checked: boolean;
}

View File

@ -0,0 +1,224 @@
import React, { useCallback, useEffect } from "react";
import styles from "styles/app.module.scss";
import { useDispatch, useSelector } from "react-redux";
import {
BACKUP_POPUP_LIST_SIZE,
changeBackupTaskAllCheched,
changeBackupTaskChecked,
listBackupPopupTasksAsync,
selectBackupAllChecked,
selectBackupTasks,
selectBackupTotal,
selectCurrentBackupPage,
selectIsBackupListLoading,
selectTotalBackupPage,
} from "features/dictation";
import { AppDispatch } from "app/store";
import { getTranslationID } from "translation";
import { useTranslation } from "react-i18next";
import progress_activit from "../../assets/images/progress_activit.svg";
import close from "../../assets/images/close.svg";
interface BackupPopupProps {
onClose: (isChanged: boolean) => void;
isOpen: boolean;
}
export const BackupPopup: React.FC<BackupPopupProps> = (props) => {
const { onClose, isOpen } = props;
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
const isBackupListLoading = useSelector(selectIsBackupListLoading);
const backupTasks = useSelector(selectBackupTasks);
const total = useSelector(selectBackupTotal);
const totalPage = useSelector(selectTotalBackupPage);
const currentPage = useSelector(selectCurrentBackupPage);
const allChecked = useSelector(selectBackupAllChecked);
// ポップアップを閉じる処理
const closePopup = useCallback(() => {
onClose(false);
}, [onClose]);
useEffect(() => {
if (isOpen) {
dispatch(listBackupPopupTasksAsync({ offset: 0 }));
}
}, [dispatch, isOpen]);
// ページネーションの制御
const getFirstPage = useCallback(() => {
dispatch(listBackupPopupTasksAsync({ offset: 0 }));
}, [dispatch]);
const getLastPage = useCallback(() => {
const lastPageOffset = (totalPage - 1) * BACKUP_POPUP_LIST_SIZE;
dispatch(listBackupPopupTasksAsync({ offset: lastPageOffset }));
}, [dispatch, totalPage]);
const getPrevPage = useCallback(() => {
const prevPageOffset = (currentPage - 2) * BACKUP_POPUP_LIST_SIZE;
dispatch(listBackupPopupTasksAsync({ offset: prevPageOffset }));
}, [dispatch, currentPage]);
const getNextPage = useCallback(() => {
const nextPageOffset = currentPage * BACKUP_POPUP_LIST_SIZE;
dispatch(listBackupPopupTasksAsync({ offset: nextPageOffset }));
}, [dispatch, currentPage]);
return (
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("dictationPage.label.fileBackup"))}
<button type="button" onClick={closePopup}>
<img src={close} className={styles.modalTitleIcon} alt="close" />
</button>
</p>
<form className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dd className={styles.full}>
<div className={styles.tableWrap}>
<table className={`${styles.table} ${styles.backup}`}>
<tr className={styles.tableHeader}>
<th className={styles.noLine}>
<input
type="checkbox"
checked={allChecked}
className={styles.formCheck}
onChange={(e) =>
dispatch(
changeBackupTaskAllCheched({
checked: e.target.checked,
})
)
}
/>
</th>
<th className={styles.noLine}>
{t(getTranslationID("dictationPage.label.jobNumber"))}
</th>
<th className={styles.noLine}>
{t(getTranslationID("dictationPage.label.status"))}
</th>
<th className={styles.noLine}>
{t(getTranslationID("dictationPage.label.fileName"))}
</th>
<th>
{t(
getTranslationID(
"dictationPage.label.transcriptionFinishedDate"
)
)}
</th>
</tr>
{!isBackupListLoading &&
backupTasks.map((task) => (
<tr key={task.audioFileId}>
<td>
<input
type="checkbox"
className={styles.formCheck}
checked={task.checked}
onChange={(e) => {
dispatch(
changeBackupTaskChecked({
audioFileId: task.audioFileId,
checked: e.target.checked,
})
);
}}
/>
</td>
<td>{task.jobNumber}</td>
<td>{task.status}</td>
<td>{task.fileName}</td>
<td>{task.transcriptionFinishedDate}</td>
</tr>
))}
{isBackupListLoading && (
<img
style={{ position: "sticky" }}
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
)}
</table>
</div>
{/** <!-- pagenation --> */}
<div className={styles.pagenation}>
<nav className={styles.pagenationNav}>
<span className={styles.pagenationTotal}>{`${total} ${t(
getTranslationID("dictationPage.label.title")
)}`}</span>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
onClick={getFirstPage}
className={
!isBackupListLoading && currentPage !== 1
? styles.isActive
: ""
}
>
«
</a>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
onClick={getPrevPage}
className={
!isBackupListLoading && currentPage !== 1
? styles.isActive
: ""
}
>
</a>
{`${currentPage} of ${totalPage}`}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
onClick={getNextPage}
className={
!isBackupListLoading && currentPage < totalPage
? styles.isActive
: ""
}
>
</a>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
onClick={getLastPage}
className={
!isBackupListLoading && currentPage < totalPage
? styles.isActive
: ""
}
>
»
</a>
</nav>
</div>
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
type="submit"
name="submit"
value={t(
getTranslationID("dictationPage.label.downloadForBackup")
)}
className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`}
/>
</dd>
</dl>
</form>
</div>
</div>
);
};

View File

@ -42,8 +42,11 @@ import finished from "../../assets/images/finished.svg";
import backup from "../../assets/images/backup.svg";
import lock from "../../assets/images/lock.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
import download from "../../assets/images/download.svg";
import open_in_new from "../../assets/images/open_in_new.svg";
import { DisPlayInfo } from "./displayInfo";
import { ChangeTranscriptionistPopup } from "./changeTranscriptionistPopup";
import { BackupPopup } from "./backupPopup";
const DictationPage: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
@ -59,6 +62,7 @@ const DictationPage: React.FC = (): JSX.Element => {
isChangeTranscriptionistPopupOpen,
setIsChangeTranscriptionistPopupOpen,
] = useState(false);
const [isBackupPopupOpen, setIsBackupPopupOpen] = useState(false);
const onChangeTranscriptionistPopupOpen = useCallback(
(task: Task) => {
@ -413,6 +417,14 @@ const DictationPage: React.FC = (): JSX.Element => {
]
);
const onCloseBackupPopup = useCallback(() => {
setIsBackupPopupOpen(false);
}, []);
const onClickBackup = useCallback(() => {
setIsBackupPopupOpen(true);
}, []);
const sortIconClass = (
currentParam: SortableColumnType,
currentDirection: DirectionType,
@ -467,6 +479,7 @@ const DictationPage: React.FC = (): JSX.Element => {
return (
<>
<BackupPopup isOpen={isBackupPopupOpen} onClose={onCloseBackupPopup} />
<ChangeTranscriptionistPopup
isOpen={isChangeTranscriptionistPopupOpen}
onClose={onClosePopup}
@ -1271,6 +1284,34 @@ const DictationPage: React.FC = (): JSX.Element => {
</a>
</nav>
</div>
<ul className={`${styles.menuAction} ${styles.alignRight}`}>
<li className={styles.alignLeft}>
<a
href=""
className={`${styles.menuLink} ${styles.isActive}`}
target="_blank"
>
Applications
<img
src={open_in_new}
alt=""
className={styles.menuIcon}
/>
</a>
</li>
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
<a
onClick={onClickBackup}
className={`${styles.menuLink} ${
isAdmin ? styles.isActive : ""
}`}
>
<img src={download} alt="" className={styles.menuIcon} />
{t(getTranslationID("dictationPage.label.fileBackup"))}
</a>
</li>
</ul>
</div>
</section>
</div>

View File

@ -890,6 +890,11 @@ h3 + .brCrumb .tlIcon {
width: 42%;
text-align: left;
position: relative;
white-space: pre-line;
}
.listVertical dt.overLine {
padding: 0.4rem 4%;
line-height: 1.15;
}
.listVertical dd {
width: 42%;
@ -1107,6 +1112,25 @@ h3 + .brCrumb .tlIcon {
width: inherit;
padding: 0.2rem 0.5rem;
}
.modal .form .table.backup .formCheck {
margin-right: 0;
}
.modal .form .table.backup th:first-child {
padding: 0 0.2rem;
}
.modal .form .table.backup td {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.modal .form .table.backup td:first-child {
padding: 0.6rem 0.2rem;
}
.modal .form .pagenation {
margin-bottom: 1.5rem;
padding-right: 2.5%;
}
.modal .encryptionPass {
display: none;
}
@ -1898,6 +1922,19 @@ tr.isSelected .menuInTable li a.isDisable {
.dictation .menuAction {
margin-top: -1rem;
height: 34px;
position: relative;
}
.dictation .menuAction .alignLeft {
position: absolute;
left: 0;
}
.dictation .menuAction .alignLeft .menuLink {
padding: 0.3rem 0.3rem 0.3rem 0.5rem;
}
.dictation .menuAction .alignLeft .menuIcon {
margin-right: 0;
margin-left: 0.4rem;
}
.dictation .displayOptions {
display: none;
@ -2047,7 +2084,7 @@ tr.isSelected .menuInTable li a.isDisable {
.dictation .table.dictation td .menuInTable li:nth-child(3) {
border-right: none;
}
.dictation .table.dictation td .menuInTable li a.mnBack {
.dictation .table.dictation td .menuInTable li a.mnCancel {
margin-left: 3rem;
}
.dictation .table.dictation td:has(img[alt="encrypted"]) {
@ -2266,7 +2303,8 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input + label:hover,
.formChange ul.holdMember li input + label:hover {
background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center;
background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left
center;
background-size: 1.3rem;
}
.formChange ul.chooseMember li input:checked + label,
@ -2277,8 +2315,8 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input:checked + label:hover,
.formChange ul.holdMember li input:checked + label:hover {
background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right
center;
background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat
right center;
background-size: 1.3rem;
}
.formChange > p {
@ -2431,7 +2469,8 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input + label:hover,
.formChange ul.holdMember li input + label:hover {
background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center;
background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left
center;
background-size: 1.3rem;
}
.formChange ul.chooseMember li input:checked + label,
@ -2442,7 +2481,7 @@ tr.isSelected .menuInTable li a.isDisable {
}
.formChange ul.chooseMember li input:checked + label:hover,
.formChange ul.holdMember li input:checked + label:hover {
background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right
background: #e6e6e6 url(../images/arrow_circle_right.svg) no-repeat right
center;
background-size: 1.3rem;
}

View File

@ -72,11 +72,12 @@ declare const classNames: {
readonly tableWrap: "tableWrap";
readonly table: "table";
readonly tableHeader: "tableHeader";
readonly backup: "backup";
readonly pagenation: "pagenation";
readonly encryptionPass: "encryptionPass";
readonly pageHeader: "pageHeader";
readonly pageTitle: "pageTitle";
readonly pageTx: "pageTx";
readonly pagenation: "pagenation";
readonly pagenationNav: "pagenationNav";
readonly pagenationTotal: "pagenationTotal";
readonly widthMid: "widthMid";
@ -124,10 +125,11 @@ declare const classNames: {
readonly cardHistory: "cardHistory";
readonly partner: "partner";
readonly isOpen: "isOpen";
readonly alignLeft: "alignLeft";
readonly displayOptions: "displayOptions";
readonly tableFilter: "tableFilter";
readonly tableFilter2: "tableFilter2";
readonly mnBack: "mnBack";
readonly mnCancel: "mnCancel";
readonly txWsline: "txWsline";
readonly hidePri: "hidePri";
readonly opPri: "opPri";
@ -197,7 +199,6 @@ declare const classNames: {
readonly template: "template";
readonly worktype: "worktype";
readonly selectMenu: "selectMenu";
readonly alignLeft: "alignLeft";
readonly floatNone: "floatNone";
readonly floatLeft: "floatLeft";
readonly floatRight: "floatRight";

View File

@ -245,7 +245,9 @@
"changeTranscriptionist": "Transkriptionist ändern",
"deleteDictation": "Diktat löschen",
"selectedTranscriptionist": "Ausgewählter transkriptionist",
"poolTranscriptionist": "Transkriptionsliste"
"poolTranscriptionist": "Transkriptionsliste",
"fileBackup": "(de)File Backup",
"downloadForBackup": "(de)Download for backup"
}
},
"cardLicenseIssuePopupPage": {

View File

@ -245,7 +245,9 @@
"changeTranscriptionist": "Change Transcriptionist",
"deleteDictation": "Delete Dictation",
"selectedTranscriptionist": "Selected Transcriptionist",
"poolTranscriptionist": "Transcription List"
"poolTranscriptionist": "Transcription List",
"fileBackup": "File Backup",
"downloadForBackup": "Download for backup"
}
},
"cardLicenseIssuePopupPage": {

View File

@ -245,7 +245,9 @@
"changeTranscriptionist": "Cambiar transcriptor",
"deleteDictation": "Borrar dictado",
"selectedTranscriptionist": "Transcriptor seleccionado",
"poolTranscriptionist": "Lista de transcriptor"
"poolTranscriptionist": "Lista de transcriptor",
"fileBackup": "(es)File Backup",
"downloadForBackup": "(es)Download for backup"
}
},
"cardLicenseIssuePopupPage": {

View File

@ -245,7 +245,9 @@
"changeTranscriptionist": "Changer de transcriptionniste ",
"deleteDictation": "Supprimer la dictée",
"selectedTranscriptionist": "Transcriptionniste sélectionné",
"poolTranscriptionist": "Liste de transcriptionniste"
"poolTranscriptionist": "Liste de transcriptionniste",
"fileBackup": "(fr)File Backup",
"downloadForBackup": "(fr)Download for backup"
}
},
"cardLicenseIssuePopupPage": {