@team-monolith/cds 1.117.12 → 1.117.13

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.
Files changed (46) hide show
  1. package/dist/CdsProvider.d.ts +1 -1
  2. package/dist/CdsProvider.js +1 -1
  3. package/dist/components/AlertDialog/AlertDialogTitle.js +3 -1
  4. package/dist/components/FileTypeAlertDialog.js +3 -1
  5. package/dist/components/Pagination.js +7 -3
  6. package/dist/i18n/i18n.d.ts +1 -1
  7. package/dist/i18n/i18n.js +9 -9
  8. package/dist/patterns/LexicalEditor/components/FileSelectInput.js +22 -9
  9. package/dist/patterns/LexicalEditor/components/InsertImageDialog/ImagePreview.js +3 -1
  10. package/dist/patterns/LexicalEditor/components/InsertImageDialog/InsertImageDialog.js +3 -1
  11. package/dist/patterns/LexicalEditor/components/UploadFileDialog/UploadFileDialog.js +2 -1
  12. package/dist/patterns/LexicalEditor/nodes/FileNode/FileDownloadButton.js +5 -3
  13. package/dist/patterns/LexicalEditor/nodes/ImageNode/ImageComponent.js +4 -2
  14. package/dist/patterns/LexicalEditor/nodes/ProblemInputNode/InputComponent.js +7 -4
  15. package/dist/patterns/LexicalEditor/nodes/ProblemInputNode/SettingForm/FormPlaceholder.js +2 -1
  16. package/dist/patterns/LexicalEditor/nodes/ProblemInputNode/SettingForm/FormSolution.js +4 -2
  17. package/dist/patterns/LexicalEditor/nodes/ProblemInputNode/SettingForm/SettingForm.js +19 -15
  18. package/dist/patterns/LexicalEditor/nodes/ProblemInputNode/SettingForm/TextTypeDropdown.js +6 -2
  19. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SelectBox/SelectBoxView.js +18 -9
  20. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SelectComponent.js +8 -3
  21. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/FormSelection.js +23 -4
  22. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/SettingForm.js +9 -8
  23. package/dist/patterns/LexicalEditor/nodes/SelfEvaluationNode/EvaluationComponent/Evaluation/SettingForm/FormIconAndLabel/FormIconAndLabel.js +6 -5
  24. package/dist/patterns/LexicalEditor/nodes/SelfEvaluationNode/EvaluationComponent/Evaluation/SettingForm/FormIconAndLabel/FormLabel.js +7 -5
  25. package/dist/patterns/LexicalEditor/nodes/SelfEvaluationNode/EvaluationComponent/Evaluation/SettingForm/FormQuestion.js +7 -2
  26. package/dist/patterns/LexicalEditor/nodes/SelfEvaluationNode/EvaluationComponent/Evaluation/SettingForm/SettingForm.js +7 -4
  27. package/dist/patterns/LexicalEditor/nodes/SelfEvaluationNode/EvaluationComponent/EvaluationComponent.js +5 -3
  28. package/dist/patterns/LexicalEditor/nodes/SelfEvaluationNode/iconData.d.ts +1 -1
  29. package/dist/patterns/LexicalEditor/nodes/SelfEvaluationNode/iconData.js +6 -5
  30. package/dist/patterns/LexicalEditor/nodes/SheetInputNode/InputComponent.js +9 -2
  31. package/dist/patterns/LexicalEditor/nodes/SheetInputNode/SettingForm.js +7 -2
  32. package/dist/patterns/LexicalEditor/nodes/SheetSelectNode/SelectComponent/SelectBox/SelectBoxView.js +10 -2
  33. package/dist/patterns/LexicalEditor/nodes/SheetSelectNode/SelectComponent/SelectComponent.js +8 -3
  34. package/dist/patterns/LexicalEditor/nodes/SheetSelectNode/SelectComponent/SettingForm/FormAllowMultipleAnswers.js +3 -1
  35. package/dist/patterns/LexicalEditor/nodes/SheetSelectNode/SelectComponent/SettingForm/FormSelection.js +20 -3
  36. package/dist/patterns/LexicalEditor/nodes/SheetSelectNode/SelectComponent/SettingForm/SettingForm.js +10 -9
  37. package/dist/patterns/LexicalEditor/plugins/ActionsPlugin/index.js +3 -1
  38. package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/ComponentAdderPlugin.js +8 -2
  39. package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/useContextMenuOptions.js +33 -31
  40. package/dist/patterns/LexicalEditor/plugins/ComponentPickerMenuPlugin/ComponentPickerMenuPlugin.js +44 -25
  41. package/dist/patterns/LexicalEditor/plugins/DragDropPastePlugin/index.js +2 -1
  42. package/dist/patterns/LexicalEditor/plugins/FloatingLinkEditorPlugin/FloatingLinkEditor.js +3 -1
  43. package/dist/patterns/ToggleButtonGroup/ToggleButton.js +4 -2
  44. package/dist/texts.d.ts +12 -0
  45. package/dist/texts.js +14 -0
  46. package/package.json +1 -1
@@ -8,7 +8,7 @@ interface CdsContext {
8
8
  uploadByFile: (file: File) => Promise<string>;
9
9
  showFileError: (type: "upload" | "download", message: string) => void;
10
10
  };
11
- i18nextOption?: i18nextOption;
11
+ i18nextConfig?: i18nextOption;
12
12
  }
13
13
  export declare const CdsContext: import("react").Context<CdsContext>;
14
14
  export declare function CdsProvider(props: {
@@ -318,7 +318,7 @@ export function CdsProvider(props) {
318
318
  // useEffect가 실행이 되지 않는 문제가 있습니다.
319
319
  // 따라서 useEffect를 사용하지 않고, 동기적으로 i18n 초기화를 수행합니다.
320
320
  if (!i18n.isInitialized && !i18n.isInitializing) {
321
- initI18nInstance(props.i18nextOption);
321
+ initI18nInstance(props.i18nextConfig);
322
322
  }
323
323
  const muiTheme = createTheme({
324
324
  typography: {
@@ -17,6 +17,7 @@ import { SquareButton } from "../SquareButton";
17
17
  import { CloseFillIcon } from "../../icons";
18
18
  import { AlertDialogContext } from "./AlertDialogContext";
19
19
  import { MOBILE } from "../../foundation/breakpoints";
20
+ import { useTranslation } from "react-i18next";
20
21
  export const alertDialogTitleClasses = {
21
22
  root: "AlertDialogTitle",
22
23
  content: "AlertDialogTitle-content",
@@ -26,6 +27,7 @@ export const alertDialogTitleClasses = {
26
27
  export const AlertDialogTitle = React.forwardRef(function AlertDialogTitle(props, ref) {
27
28
  const { className, component: Component = "div", onClose, children } = props, other = __rest(props, ["className", "component", "onClose", "children"]);
28
29
  const { icon } = React.useContext(AlertDialogContext);
30
+ const { t } = useTranslation();
29
31
  const theme = useTheme();
30
32
  return (_jsx(_Fragment, { children: _jsxs(Component, Object.assign({}, other, { ref: ref, className: [alertDialogTitleClasses.root, className]
31
33
  .filter(Boolean)
@@ -63,5 +65,5 @@ export const AlertDialogTitle = React.forwardRef(function AlertDialogTitle(props
63
65
  height: 32px;
64
66
  }
65
67
  `)}
66
- `, children: icon })), children] }), onClose && (_jsx(SquareButton, { className: alertDialogTitleClasses.closeButton, color: "icon", size: "small", icon: _jsx(CloseFillIcon, {}), onClick: onClose, "aria-label": "\uB2EB\uAE30" }))] })) }));
68
+ `, children: icon })), children] }), onClose && (_jsx(SquareButton, { className: alertDialogTitleClasses.closeButton, color: "icon", size: "small", icon: _jsx(CloseFillIcon, {}), onClick: onClose, "aria-label": t("닫기", { context: "다이얼로그" }) }))] })) }));
67
69
  });
@@ -4,6 +4,7 @@ import { css, useTheme } from "@emotion/react";
4
4
  import { AlertDialog, AlertDialogActions, AlertDialogContent, AlertDialogTitle, } from "./AlertDialog";
5
5
  import { AlertFillIcon } from "../icons";
6
6
  import { Button } from "./Button";
7
+ import { useTranslation } from "react-i18next";
7
8
  export const fileTypeAlertDialogClasses = {
8
9
  root: "FileTypeAlertDialog",
9
10
  title: "FileTypeAlertDialog-title",
@@ -13,11 +14,12 @@ export const fileTypeAlertDialogClasses = {
13
14
  export function FileTypeAlertDialog(props) {
14
15
  const { className, open, onClose, content, openFileBrowser } = props;
15
16
  const theme = useTheme();
17
+ const { t } = useTranslation();
16
18
  return (_jsxs(AlertDialog, { className: [fileTypeAlertDialogClasses.root, className].filter(Boolean).join(" "), open: open, onClose: onClose, icon: _jsx(AlertFillIcon, { css: css `
17
19
  color: ${theme.color.background.danger};
18
20
  ` }), children: [_jsx(AlertDialogTitle, { className: fileTypeAlertDialogClasses.title, css: css `
19
21
  font-weight: 700;
20
- `, onClose: onClose, children: "\uD30C\uC77C \uD615\uC2DD\uC744 \uD655\uC778\uD574\uC8FC\uC138\uC694." }), _jsx(AlertDialogContent, { className: fileTypeAlertDialogClasses.content, children: content }), _jsxs(AlertDialogActions, { className: fileTypeAlertDialogClasses.actions, children: [_jsx(Button, { color: "grey", label: "\uB2EB\uAE30", size: "medium", onClick: onClose, bold: true }), _jsx(Button, { color: "primary", label: "\uB2E4\uC2DC \uC5C5\uB85C\uB4DC", size: "medium", onClick: () => {
22
+ `, onClose: onClose, children: t("파일 형식을 확인해주세요.") }), _jsx(AlertDialogContent, { className: fileTypeAlertDialogClasses.content, children: content }), _jsxs(AlertDialogActions, { className: fileTypeAlertDialogClasses.actions, children: [_jsx(Button, { color: "grey", label: t("닫기", { context: "다이얼로그" }), size: "medium", onClick: onClose, bold: true }), _jsx(Button, { color: "primary", label: t("다시 업로드"), size: "medium", onClick: () => {
21
23
  onClose();
22
24
  openFileBrowser();
23
25
  }, bold: true })] })] }));
@@ -16,6 +16,7 @@ import * as React from "react";
16
16
  import styled from "@emotion/styled";
17
17
  import { Pagination as MuiPagination } from "@mui/material";
18
18
  import { HOVER } from "../utils/hover";
19
+ import { useTranslation } from "react-i18next";
19
20
  export const paginationClasses = {
20
21
  root: "Pagination",
21
22
  };
@@ -24,12 +25,15 @@ export const paginationClasses = {
24
25
  */
25
26
  export const Pagination = React.forwardRef(function Pagination(props, ref) {
26
27
  const { className } = props, other = __rest(props, ["className"]);
28
+ const { t } = useTranslation();
27
29
  return (_jsx(StyledPagination, Object.assign({ siblingCount: 2, shape: "rounded" }, other, { className: [paginationClasses.root, className].filter(Boolean).join(" "), ref: ref, getItemAriaLabel: (type, page, selected) => {
28
30
  if (type === "previous")
29
- return "이전 페이지로 이동";
31
+ return t("이전 페이지로 이동");
30
32
  if (type === "next")
31
- return "다음 페이지로 이동";
32
- return `${page}페이지${selected ? " 선택됨" : ""}`;
33
+ return t("다음 페이지로 이동");
34
+ return selected
35
+ ? t("{{page}}페이지, 선택됨", { page })
36
+ : t("{{page}}페이지", { page });
33
37
  } })));
34
38
  });
35
39
  const StyledPagination = styled(MuiPagination)(({ theme }) => css `
@@ -5,4 +5,4 @@ export type i18nextOption = {
5
5
  apiKey?: string;
6
6
  };
7
7
  export declare const i18n: import("i18next").i18n;
8
- export declare function initI18nInstance(option: i18nextOption | undefined): Promise<import("i18next").TFunction<"translation", undefined>>;
8
+ export declare function initI18nInstance(config: i18nextOption | undefined): Promise<import("i18next").TFunction<"translation", undefined>>;
package/dist/i18n/i18n.js CHANGED
@@ -31,10 +31,10 @@ export const i18n = i18next
31
31
  .createInstance()
32
32
  .use(languageDetector)
33
33
  .use(initReactI18next);
34
- export function initI18nInstance(option) {
34
+ export function initI18nInstance(config) {
35
35
  return __awaiter(this, void 0, void 0, function* () {
36
- if (!option) {
37
- // option이 없는 경우, backend 플러그인 없이 기본 i18next 설정을 사용하여,
36
+ if (!config) {
37
+ // config가 없는 경우, backend 플러그인 없이 기본 i18next 설정을 사용하여,
38
38
  // fallbackValue(=key)를 항상 사용하게 됩니다.
39
39
  return i18n.init(Object.assign(Object.assign({}, I18N_COMMON_CONFIGS), { fallbackLng: false }));
40
40
  }
@@ -44,19 +44,19 @@ export function initI18nInstance(option) {
44
44
  * 1. 프로덕션 환경이 아닐 때 (개발 or 로컬 환경) 또는,
45
45
  * 2. 언어 설정이 한국어가 아닐 때
46
46
  */
47
- const LOCIZE_ENABLED = option.environment !== "production" || !CONTAINS_KOREAN;
48
- return (LOCIZE_ENABLED ? i18n.use(Backend).use(locizePlugin) : i18n).init(Object.assign(Object.assign({}, I18N_COMMON_CONFIGS), { debug: option.environment !== "production",
47
+ const LOCIZE_ENABLED = config.environment !== "production" || !CONTAINS_KOREAN;
48
+ return (LOCIZE_ENABLED ? i18n.use(Backend).use(locizePlugin) : i18n).init(Object.assign(Object.assign({}, I18N_COMMON_CONFIGS), { debug: config.environment !== "production",
49
49
  // AIDEV-NOTE:
50
50
  // - 유저의 언어가 Korean인 경우:
51
51
  // fallbackLng를 false로 설정하여, 번역이 없는 경우 key 자체를 반환하도록 합니다.
52
52
  // 이 값을 설정하지 않는다면 i18next가 'dev'(=i18next 기본값)라는 언어를 찾으려고 시도할 것이므로,
53
53
  // 이 동작을 방지합니다.
54
54
  // - 유저의 언어가 Korean이 아닌 경우:
55
- // fallbackLng를 option.mainLanguage로 설정하여,
55
+ // fallbackLng를 env.REACT_APP_MAIN_LANGUAGE로 설정하여,
56
56
  // 유저 언어의 번역이 없을 경우, 메인 언어로 fallback합니다.
57
- fallbackLng: CONTAINS_KOREAN ? false : option.mainLanguage, saveMissing: LOCIZE_ENABLED, backend: {
58
- projectId: option.projectId,
59
- apiKey: option.apiKey,
57
+ fallbackLng: CONTAINS_KOREAN ? false : config.mainLanguage, saveMissing: LOCIZE_ENABLED, backend: {
58
+ projectId: config.projectId,
59
+ apiKey: config.apiKey,
60
60
  referenceLng: "ko",
61
61
  } }));
62
62
  });
@@ -15,11 +15,23 @@ import { FileWarningLineIcon, UploadLineIcon } from "../../../icons";
15
15
  import { css, useTheme } from "@emotion/react";
16
16
  import styled from "@emotion/styled";
17
17
  import moment from "moment";
18
- const fileTypeMap = {
19
- image: { accept: "image/*", typeStr: "이미지 파일" },
20
- pdf: { accept: ".pdf", typeStr: "PDF 파일" },
21
- file: { accept: "*", typeStr: "모든 파일" },
22
- };
18
+ import { useTranslation } from "react-i18next";
19
+ import { i18n } from "../../../i18n/i18n";
20
+ import { getTexts } from "../../../texts";
21
+ const getFileType = (fileType) => ({
22
+ image: {
23
+ accept: "image/*",
24
+ errorStr: i18n.t("업로드할 수 없는 파일입니다. 이미지 파일을 업로드해주세요."),
25
+ },
26
+ pdf: {
27
+ accept: ".pdf",
28
+ errorStr: i18n.t("업로드할 수 없는 파일입니다. PDF 파일을 업로드해주세요."),
29
+ },
30
+ file: {
31
+ accept: "*",
32
+ errorStr: i18n.t("업로드할 수 없는 파일입니다. 모든 파일을 업로드해주세요."),
33
+ },
34
+ })[fileType];
23
35
  const MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024; // 1GB
24
36
  /**
25
37
  * 파일 탐색기를 열어 특정 파일을 업로드 한 후, 그 경로를 onChange로 전달합니다.
@@ -31,15 +43,16 @@ export function FileSelectInput(props) {
31
43
  const [errorMessage, setErrorMessage] = useState(null);
32
44
  const inputRef = useRef(null);
33
45
  const cdsContext = useContext(CdsContext);
34
- const { accept, typeStr } = fileTypeMap[fileType];
46
+ const { accept, errorStr } = getFileType(fileType);
35
47
  const theme = useTheme();
48
+ const { t } = useTranslation();
36
49
  const handleFileChange = (event) => __awaiter(this, void 0, void 0, function* () {
37
50
  var _a, _b, _c;
38
51
  const file = (_a = event.target.files) === null || _a === void 0 ? void 0 : _a[0];
39
52
  if (!file)
40
53
  return;
41
54
  if (file.size >= MAX_FILE_SIZE) {
42
- setErrorMessage("용량이 너무 큽니다. 1GB 이하의 파일을 선택해 주세요.");
55
+ setErrorMessage(getTexts("errorFileTooLarge"));
43
56
  event.target.value = "";
44
57
  return;
45
58
  }
@@ -49,7 +62,7 @@ export function FileSelectInput(props) {
49
62
  ? accept.slice(1) === ((_b = file.name.split(".").pop()) === null || _b === void 0 ? void 0 : _b.toLowerCase())
50
63
  : new RegExp(accept.replace("*", ".*")).test(file.type));
51
64
  if (!isTypeValid) {
52
- setErrorMessage(`업로드할 수 없는 파일입니다. ${typeStr}을 업로드해주세요.`);
65
+ setErrorMessage(errorStr);
53
66
  event.target.value = "";
54
67
  return;
55
68
  }
@@ -67,7 +80,7 @@ export function FileSelectInput(props) {
67
80
  }
68
81
  setErrorMessage(null);
69
82
  });
70
- return (_jsxs(_Fragment, { children: [_jsxs(Container, { children: [_jsx(Button, { fullWidth: true, color: "grey", size: "medium", label: "\uD30C\uC77C \uC120\uD0DD\uD558\uAE30", onClick: () => {
83
+ return (_jsxs(_Fragment, { children: [_jsxs(Container, { children: [_jsx(Button, { fullWidth: true, color: "grey", size: "medium", label: t("파일 선택하기"), onClick: () => {
71
84
  var _a;
72
85
  (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click();
73
86
  }, startIcon: _jsx(UploadLineIcon, {}), bold: bold }), _jsx(IconAndErrorMessage, { children: errorMessage && (_jsxs(_Fragment, { children: [_jsx(FileWarningLineIcon, { css: css `
@@ -1,10 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import styled from "@emotion/styled";
3
3
  import { useState } from "react";
4
+ import { useTranslation } from "react-i18next";
4
5
  export function ImagePreview(props) {
5
6
  const { src, alt, fallback } = props;
6
7
  const [erroredSrc, setErroredSrc] = useState(null);
7
- return (_jsxs(Container, { children: ["\uC774\uBBF8\uC9C0 \uBBF8\uB9AC\uBCF4\uAE30", erroredSrc === src ? (fallback) : (_jsx("img", { src: src, alt: alt, onError: () => setErroredSrc(src) }))] }));
8
+ const { t } = useTranslation();
9
+ return (_jsxs(Container, { children: [t("이미지 미리보기"), erroredSrc === src ? (fallback) : (_jsx("img", { src: src, alt: alt, onError: () => setErroredSrc(src) }))] }));
8
10
  }
9
11
  const Container = styled.div `
10
12
  display: flex;
@@ -12,9 +12,11 @@ import { ImagePreview } from "./ImagePreview";
12
12
  import { ImageNotAvailable } from "../../nodes/ImageNode/ImageNotAvailable";
13
13
  import styled from "@emotion/styled";
14
14
  import { debounce } from "lodash";
15
+ import { useTranslation } from "react-i18next";
15
16
  export function InsertImageDialog(props) {
16
17
  const { title, open, onClose, imageProps, onChange, onDelete, shouldReset } = props;
17
18
  const theme = useTheme();
19
+ const { t } = useTranslation();
18
20
  const { control, setValue, watch, reset, handleSubmit, subscribe } = useForm({
19
21
  defaultValues: imageProps !== null && imageProps !== void 0 ? imageProps : { src: "", altText: "" },
20
22
  });
@@ -49,7 +51,7 @@ export function InsertImageDialog(props) {
49
51
  handleSubmit(onSubmit)();
50
52
  }, disableIconPadding: true, children: [_jsx(StyledAlertDialogTitle, { onClose: handleOnClose, children: title }), _jsx(AlertDialogContent, { children: _jsxs(Inputs, { children: [_jsx(FileSelectInput, { onChange: (value) => {
51
53
  setValue("src", value);
52
- }, fileType: "image" }), _jsx(FormInput, { name: "src", control: control, placeholder: "https://www.pexels.com/photo/n-2848492", size: "medium", label: "URL", fullWidth: true, startIcon: _jsx(LinkIcon, {}) }), previewSrc && (_jsx(ImagePreview, { src: previewSrc, alt: watch("altText"), fallback: _jsx(ImageNotAvailable, {}) })), _jsx(FormInput, { name: "altText", control: control, placeholder: "\uC0BD\uC785\uD558\uB294 \uC774\uBBF8\uC9C0\uC5D0 \uAD00\uD55C \uC124\uBA85", size: "medium", label: "\uB300\uCCB4 \uD14D\uC2A4\uD2B8", fullWidth: true })] }) }), _jsxs(AlertDialogActions, { children: [_jsx(Button, { type: "submit", fullWidth: true, label: "\uC0BD\uC785\uD558\uAE30", size: "medium", color: "primary", disabled: isDisabled }), onDelete && (_jsx(Button, { color: "danger", size: "medium", fullWidth: true, label: "\uC0AD\uC81C\uD558\uAE30", onClick: onDelete }))] })] }));
54
+ }, fileType: "image" }), _jsx(FormInput, { name: "src", control: control, placeholder: "https://www.pexels.com/photo/n-2848492", size: "medium", label: "URL", fullWidth: true, startIcon: _jsx(LinkIcon, {}) }), previewSrc && (_jsx(ImagePreview, { src: previewSrc, alt: watch("altText"), fallback: _jsx(ImageNotAvailable, {}) })), _jsx(FormInput, { name: "altText", control: control, placeholder: t("삽입하는 이미지에 관한 설명"), size: "medium", label: t("대체 텍스트"), fullWidth: true })] }) }), _jsxs(AlertDialogActions, { children: [_jsx(Button, { type: "submit", fullWidth: true, label: t("삽입하기"), size: "medium", color: "primary", disabled: isDisabled }), onDelete && (_jsx(Button, { color: "danger", size: "medium", fullWidth: true, label: t("삭제하기"), onClick: onDelete }))] })] }));
53
55
  }
54
56
  const StyledAlertDialog = styled(AlertDialog) `
55
57
  gap: 16px;
@@ -13,6 +13,7 @@ import { useContext, useEffect, useRef } from "react";
13
13
  import moment from "moment";
14
14
  import { CdsContext } from "../../../../CdsProvider";
15
15
  import styled from "@emotion/styled";
16
+ import { getTexts } from "../../../../texts";
16
17
  /**
17
18
  * OS 파일 다이얼로그를 열어 파일을 선택하고 업로드하는 컴포넌트입니다.
18
19
  */
@@ -29,7 +30,7 @@ export function UploadFileDialog({ open, onClose, onChange, }) {
29
30
  }
30
31
  const MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024; // 1GB
31
32
  if (file.size >= MAX_FILE_SIZE) {
32
- showFileError === null || showFileError === void 0 ? void 0 : showFileError("upload", "용량이 너무 큽니다. 1GB 이하의 파일을 선택해 주세요.");
33
+ showFileError === null || showFileError === void 0 ? void 0 : showFileError("upload", getTexts("errorFileTooLarge"));
33
34
  event.target.value = "";
34
35
  onClose();
35
36
  return;
@@ -15,6 +15,7 @@ import { OverflowTooltip } from "../../../../components/OverflowTooltip";
15
15
  import { useContext } from "react";
16
16
  import { CdsContext } from "../../../../CdsProvider";
17
17
  import { HOVER } from "../../../../utils/hover";
18
+ import { useTranslation } from "react-i18next";
18
19
  /**
19
20
  * 파일 업로드 블록 내에서 삽입하기를 통해 렉시컬에 삽입된 파일블록을 표시하는 컴포넌트입니다. (ReadOnly)
20
21
  * images/2025-02-05-10-53-19.png
@@ -22,18 +23,19 @@ import { HOVER } from "../../../../utils/hover";
22
23
  export function FileDownloadButton({ fileName, fileSize, fileUrl, }) {
23
24
  const cdsContext = useContext(CdsContext);
24
25
  const theme = useTheme();
26
+ const { t } = useTranslation();
25
27
  const handleDownload = () => __awaiter(this, void 0, void 0, function* () {
26
28
  var _a;
27
29
  const showFileError = (_a = cdsContext.lexical) === null || _a === void 0 ? void 0 : _a.showFileError;
28
30
  try {
29
31
  if (!fileUrl) {
30
- showFileError === null || showFileError === void 0 ? void 0 : showFileError("download", "다운로드 중 오류가 발생했어요");
32
+ showFileError === null || showFileError === void 0 ? void 0 : showFileError("download", t("다운로드 중 오류가 발생했어요"));
31
33
  return;
32
34
  }
33
35
  // fileUrl에 대해 GET 요청을 보냅니다.
34
36
  const response = yield fetch(fileUrl, { method: "GET" });
35
37
  if (!response.ok) {
36
- showFileError === null || showFileError === void 0 ? void 0 : showFileError("download", "파일이 삭제되어 다운로드할 수 없어요.");
38
+ showFileError === null || showFileError === void 0 ? void 0 : showFileError("download", t("파일이 삭제되어 다운로드할 수 없어요."));
37
39
  return;
38
40
  }
39
41
  // 요청이 성공하면 응답 데이터를 Blob(파일 데이터)로 변환
@@ -52,7 +54,7 @@ export function FileDownloadButton({ fileName, fileSize, fileUrl, }) {
52
54
  URL.revokeObjectURL(blobUrl);
53
55
  }
54
56
  catch (_b) {
55
- showFileError === null || showFileError === void 0 ? void 0 : showFileError("download", "다운로드 중 오류가 발생했어요");
57
+ showFileError === null || showFileError === void 0 ? void 0 : showFileError("download", t("다운로드 중 오류가 발생했어요"));
56
58
  }
57
59
  });
58
60
  return (_jsxs(ButtonContainer, { onClick: handleDownload, theme: theme, children: [_jsx(IconContainer, { children: getFileIcon(fileName) }), _jsx(FileNameLabel, { children: _jsx(OverflowTooltip, { text: fileName }) }), _jsxs(FileSize, { children: [(fileSize / (1024 * 1024)).toFixed(1), " MB"] })] }));
@@ -15,6 +15,7 @@ import { ImageResizer } from "./ImageResizer";
15
15
  import { css } from "@emotion/react";
16
16
  import { RESET_BUTTON } from "../../../../utils/reset";
17
17
  import { VISUALLY_HIDDEN } from "../../../../utils/visuallyHidden";
18
+ import { useTranslation } from "react-i18next";
18
19
  const imageCache = new Set();
19
20
  export const RIGHT_CLICK_IMAGE_COMMAND = createCommand("RIGHT_CLICK_IMAGE_COMMAND");
20
21
  function useSuspenseImage(src) {
@@ -61,6 +62,7 @@ export function ImageComponent({ src, altText, nodeKey, width, height, maxWidth,
61
62
  const [selection, setSelection] = useState(null);
62
63
  const activeEditorRef = useRef(null);
63
64
  const [isLoadError, setIsLoadError] = useState(false);
65
+ const { t } = useTranslation();
64
66
  const $onDelete = useCallback((payload) => {
65
67
  const deleteSelection = $getSelection();
66
68
  if (isSelected && $isNodeSelection(deleteSelection)) {
@@ -196,7 +198,7 @@ export function ImageComponent({ src, altText, nodeKey, width, height, maxWidth,
196
198
  cursor: pointer;
197
199
  `, src: src, altText: altText, imageRef: imageRef, width: width, height: height, maxWidth: maxWidth, onError: () => setIsLoadError(true), buttonProps: {
198
200
  onClick: () => window.open(src, "_blank"),
199
- }, title: "\uD074\uB9AD\uD574\uC11C \uC6D0\uBCF8 \uC774\uBBF8\uC9C0 \uBCF4\uAE30.", addtionalInfoLabel: _jsx("span", { css: VISUALLY_HIDDEN, children: "(\uC0C8 \uCC3D)" }) })) }) }));
201
+ }, title: t("클릭해서 원본 이미지 보기."), addtionalInfoLabel: _jsx("span", { css: VISUALLY_HIDDEN, children: t("( )", { context: "렉시컬 이미지 도구" }) }) })) }) }));
200
202
  }
201
203
  return (_jsxs(_Fragment, { children: [_jsxs(EditContainer, { children: [_jsx(Suspense, { fallback: null, children: _jsx("div", { draggable: draggable, css: css `
202
204
  // ImageResizer를 위한 relative 설정입니다.
@@ -208,7 +210,7 @@ export function ImageComponent({ src, altText, nodeKey, width, height, maxWidth,
208
210
  // maxWidth={maxWidth}
209
211
  onResizeStart: onResizeStart, onResizeEnd: onResizeEnd, captionsEnabled: false }))] })) }) }), _jsx(SquareButton, { size: "small", color: "icon", icon: _jsx(Settings3FillIcon, {}), onClick: () => {
210
212
  setOpen(true);
211
- }, "aria-label": "\uC774\uBBF8\uC9C0 \uC218\uC815\uD558\uAE30" })] }), _jsx(InsertImageDialog, { open: open, title: "\uC774\uBBF8\uC9C0 \uC218\uC815\uD558\uAE30", onClose: () => setOpen(false), imageProps: {
213
+ }, "aria-label": t("이미지 수정하기") })] }), _jsx(InsertImageDialog, { open: open, title: t("이미지 수정하기"), onClose: () => setOpen(false), imageProps: {
212
214
  src,
213
215
  altText,
214
216
  }, onChange: (props) => {
@@ -24,10 +24,13 @@ import { $getNodeByKey } from "lexical";
24
24
  import { LexicalCustomConfigContext } from "../../LexicalCustomConfigContext";
25
25
  import { TextInput } from "./TextInput";
26
26
  import { SegmentedInput } from "./SegmentedInput";
27
+ import { useTranslation } from "react-i18next";
28
+ import { getTexts } from "../../../../texts";
27
29
  export function InputComponent(props) {
28
30
  const { answer, isCorrect } = props, settingFormProps = __rest(props, ["answer", "isCorrect"]);
29
31
  const { solutions, showCharacterNumber, placeholder, nodeKey } = settingFormProps;
30
32
  const [editor] = useLexicalComposerContext();
33
+ const { t } = useTranslation();
31
34
  const [settingOpen, setSettingOpen] = useState(false);
32
35
  const isEditable = useLexicalEditable();
33
36
  // SoT를 EditorState로 설정하는 경우 한글 입력시 문제가 생김.
@@ -51,9 +54,9 @@ export function InputComponent(props) {
51
54
  // 학생 view
52
55
  if (!isEditable) {
53
56
  if (showCharacterNumber) {
54
- return (_jsx(SegmentedInput, { readOnly: freezeProblemNode, isCorrect: isCorrect, answerFormat: answerFormat, placeholder: placeholder || "여기에 입력하세요.", value: answerInput, onChange: handleChange }));
57
+ return (_jsx(SegmentedInput, { readOnly: freezeProblemNode, isCorrect: isCorrect, answerFormat: answerFormat, placeholder: placeholder || getTexts("placeholderEnterHere"), value: answerInput, onChange: handleChange }));
55
58
  }
56
- return (_jsx(TextInput, { readOnly: freezeProblemNode, isCorrect: isCorrect, size: "small", color: "default", placeholder: placeholder || "여기에 입력하세요.", value: answerInput, onChange: (e) => {
59
+ return (_jsx(TextInput, { readOnly: freezeProblemNode, isCorrect: isCorrect, size: "small", color: "default", placeholder: placeholder || getTexts("placeholderEnterHere"), value: answerInput, onChange: (e) => {
57
60
  handleChange(e.target.value);
58
61
  }, fullWidth: true }));
59
62
  }
@@ -62,9 +65,9 @@ export function InputComponent(props) {
62
65
  display: flex;
63
66
  align-items: center;
64
67
  gap: 4px;
65
- `, children: [_jsx(VirtualInput, { onClick: () => setSettingOpen(true), children: "\uC8FC\uAD00\uC2DD \uC785\uB825 \uCE78" }), _jsx(SquareButton, { size: "small", color: "icon", icon: _jsx(Settings3FillIcon, {}), onClick: () => {
68
+ `, children: [_jsx(VirtualInput, { onClick: () => setSettingOpen(true), children: t("주관식 입력 ", { context: "렉시컬 주관식 입력 도구" }) }), _jsx(SquareButton, { size: "small", color: "icon", icon: _jsx(Settings3FillIcon, {}), onClick: () => {
66
69
  setSettingOpen(true);
67
- }, "aria-label": "\uC785\uB825 \uCE78 \uC124\uC815" })] }), settingOpen && (_jsx(SettingForm, Object.assign({}, settingFormProps, { onClose: () => setSettingOpen(false) })))] }));
70
+ }, "aria-label": t("입력 설정", { context: "렉시컬 주관식 입력 도구" }) })] }), settingOpen && (_jsx(SettingForm, Object.assign({}, settingFormProps, { onClose: () => setSettingOpen(false) })))] }));
68
71
  }
69
72
  const VirtualInput = styled.div(({ theme }) => css `
70
73
  box-sizing: border-box;
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Controller } from "react-hook-form";
3
3
  import { Input } from "../../../../../components/Input";
4
+ import { getTexts } from "../../../../../texts";
4
5
  export function FormPlaceholder(props) {
5
6
  const { control } = props;
6
- return (_jsx(Controller, { name: "placeholder", control: control, render: ({ field: { value, onChange } }) => (_jsx(Input, { size: "small", color: "default", value: value, onChange: onChange, placeholder: "\uC608) \uC5EC\uAE30\uC5D0 \uC785\uB825\uD558\uC138\uC694." })) }));
7
+ return (_jsx(Controller, { name: "placeholder", control: control, render: ({ field: { value, onChange } }) => (_jsx(Input, { size: "small", color: "default", value: value, onChange: onChange, placeholder: getTexts("exampleEnterHere") })) }));
7
8
  }
@@ -7,18 +7,20 @@ import { AlertFillIcon, DeleteBinLineIcon, ErrorWarningFillIcon, } from "../../.
7
7
  import { SquareButton } from "../../../../../components/SquareButton";
8
8
  import { Tooltip } from "../../../../../components/Tooltip";
9
9
  import { TextTypeDropdown } from "./TextTypeDropdown";
10
+ import { Trans, useTranslation } from "react-i18next";
10
11
  export function FormSolution(props) {
11
12
  const { index, control, rules, onDelete } = props;
12
13
  const theme = useTheme();
14
+ const { t } = useTranslation();
13
15
  return (_jsx(Controller, { name: `solutions.${index}.value`, control: control, rules: rules, render: ({ field: { value, onChange }, fieldState: { invalid, error }, }) => {
14
16
  const disabled = (error === null || error === void 0 ? void 0 : error.type) === "enabled";
15
- return (_jsx(Input, { size: "small", color: invalid ? "activeDanger" : "default", onChange: onChange, disabled: disabled, value: value, hintIcon: !disabled && invalid ? _jsx(ErrorWarningFillIcon, {}) : undefined, hintText: !disabled ? error === null || error === void 0 ? void 0 : error.message : undefined, placeholder: "\uC548\uB155\uD558\uC138\uC694", fullWidth: true, css: css `
17
+ return (_jsx(Input, { size: "small", color: invalid ? "activeDanger" : "default", onChange: onChange, disabled: disabled, value: value, hintIcon: !disabled && invalid ? _jsx(ErrorWarningFillIcon, {}) : undefined, hintText: !disabled ? error === null || error === void 0 ? void 0 : error.message : undefined, placeholder: t("안녕하세요"), fullWidth: true, css: css `
16
18
  > div {
17
19
  padding: 4px 12px;
18
20
  }
19
21
  `, startIcon: _jsx(TextTypeDropdown, { index: index, control: control, disabled: disabled }), endIcon: _jsxs("div", { css: css `
20
22
  display: flex;
21
23
  gap: 4px;
22
- `, children: [onDelete && (_jsx(SquareButton, { color: "white", size: "xsmall", icon: _jsx(DeleteBinLineIcon, {}), onClick: onDelete, "aria-label": "\uC0AD\uC81C" })), disabled && (_jsx(Tooltip, { text: _jsxs("span", { children: ["\uC785\uB825 \uCE78 \uC124\uC815\uC774 '\uAE00\uC790 \uC218\uB300\uB85C'\uC778 \uACBD\uC6B0", _jsx("br", {}), "\uC815\uB2F5\uC744 \uD558\uB098\uB9CC \uB4F1\uB85D\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4."] }), children: _jsx(SquareButton, { color: "danger", size: "xsmall", icon: _jsx(AlertFillIcon, { color: theme.color.foreground.neutralAlt }), disabled: true, "aria-label": "\uC815\uB2F5 \uB4F1\uB85D \uBD88\uAC00" }) }))] }) }));
24
+ `, children: [onDelete && (_jsx(SquareButton, { color: "white", size: "xsmall", icon: _jsx(DeleteBinLineIcon, {}), onClick: onDelete, "aria-label": t("삭제", { context: "스퀘어버튼, 렉시컬 주관식 설정창" }) })), disabled && (_jsx(Tooltip, { text: _jsx(Trans, { i18nKey: "\uC785\uB825 \uCE78 \uC124\uC815\uC774 '\uAE00\uC790 \uC218\uB300\uB85C'\uC778 \uACBD\uC6B0<br/>\uC815\uB2F5\uC744 \uD558\uB098\uB9CC \uB4F1\uB85D\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4." }), children: _jsx(SquareButton, { color: "danger", size: "xsmall", icon: _jsx(AlertFillIcon, { color: theme.color.foreground.neutralAlt }), disabled: true, "aria-label": t("정답 등록 불가") }) }))] }) }));
23
25
  } }));
24
26
  }
@@ -13,9 +13,13 @@ import { FormSegmentedControl } from "./FormSegmentedControl";
13
13
  import { FormPlaceholder } from "./FormPlaceholder";
14
14
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
15
15
  import { $getNodeByKey } from "lexical";
16
+ import { Trans, useTranslation } from "react-i18next";
17
+ import { getTexts } from "../../../../../texts";
16
18
  export function SettingForm(props) {
17
19
  const { solutions, showCharacterNumber, placeholder, nodeKey, onClose, caseSensitive, ignoreWhitespace, } = props;
18
20
  const [editor] = useLexicalComposerContext();
21
+ const { t } = useTranslation();
22
+ const numCharOptionStr = t("글자 수대로");
19
23
  const { control, handleSubmit, watch, trigger } = useForm({
20
24
  mode: "all",
21
25
  defaultValues: {
@@ -50,19 +54,19 @@ export function SettingForm(props) {
50
54
  return (_jsxs(Form, { onSubmit: handleSubmit(onSettingSubmit), children: [_jsxs(Title, { children: [_jsx(InputMethodLineIcon, { css: css `
51
55
  width: 12px;
52
56
  height: 12px;
53
- ` }), "\uC8FC\uAD00\uC2DD \uC785\uB825 \uCE78"] }), _jsxs(Content, { children: [_jsxs(Left, { children: [_jsxs(FormArea, { children: [_jsx(Label, { children: "\uC815\uB2F5" }), fields.map((field, index) => (_jsx(FormSolution, { index: index, control: control, rules: {
57
+ ` }), t("주관식 입력 ", { context: "렉시컬 주관식 설정창" })] }), _jsxs(Content, { children: [_jsxs(Left, { children: [_jsxs(FormArea, { children: [_jsx(Label, { children: t("정답", { context: "소제목, 렉시컬 주관식 설정창" }) }), fields.map((field, index) => (_jsx(FormSolution, { index: index, control: control, rules: {
54
58
  validate: {
55
59
  // required 옵션보다 먼저 검증되어야 하는데 priority 옵션이 없어서 validate에서 통합해서 검증합니다.
56
60
  enabled: () => index === 0 ||
57
61
  !multipleSolutionsDisabled ||
58
- "복수 정답이 불가능합니다.",
59
- required: (value) => value !== "" || "정답을 입력해주세요.",
62
+ t("복수 정답이 불가능합니다."),
63
+ required: (value) => value !== "" || t("정답을 입력해주세요."),
60
64
  },
61
65
  }, onDelete: index !== 0
62
66
  ? () => {
63
67
  remove(index);
64
68
  }
65
- : undefined }, field.uid)))] }), _jsx(Button, { color: "grey", size: "small", startIcon: _jsx(AddFillIcon, {}), label: "\uBCF5\uC218 \uC815\uB2F5 \uCD94\uAC00", disabled: multipleSolutionsDisabled, onClick: () => {
69
+ : undefined }, field.uid)))] }), _jsx(Button, { color: "grey", size: "small", startIcon: _jsx(AddFillIcon, {}), label: t("복수 정답 추가"), disabled: multipleSolutionsDisabled, onClick: () => {
66
70
  append({
67
71
  textType: "normal",
68
72
  value: "",
@@ -73,22 +77,22 @@ export function SettingForm(props) {
73
77
  `, children: [_jsx(AlarmWarningFillIcon, { color: theme.color.foreground.neutralBaseDisabled, css: css `
74
78
  width: 14px;
75
79
  height: 14px;
76
- ` }), _jsx(Label, { children: "\uAC00\uB2A5\uD55C \uC815\uB2F5\uC744 \uBAA8\uB450 \uCD94\uAC00\uD574\uC57C \uC6D0\uD65C\uD558\uAC8C \uC790\uB3D9 \uCC44\uC810\uD560 \uC218 \uC788\uC5B4\uC694." })] })] }), _jsxs(Right, { children: [_jsxs(FormArea, { children: [_jsxs(Label, { children: ["\uC785\uB825 \uCE78", _jsx(Tooltip, { text: _jsxs("span", { children: [`예를 들어 정답이 '글자 수'이고`, _jsx("br", {}), _jsx("b", { children: "\uAE00\uC790 \uC218\uB300\uB85C" }), " \uC635\uC158\uC744 \uC120\uD0DD\uD588\uB2E4\uBA74", _jsx("br", {}), `입력 칸이 '☐☐ ' 처럼 표시됩니다.`] }), placement: "top", children: _jsx(QuestionFillIcon, { css: css `
80
+ ` }), _jsx(Label, { children: t("가능한 정답을 모두 추가해야 원활하게 자동 채점할 있어요.") })] })] }), _jsxs(Right, { children: [_jsxs(FormArea, { children: [_jsxs(Label, { children: [t("입력 "), _jsx(Tooltip, { text: _jsx(Trans, { i18nKey: "\uC608\uB97C \uB4E4\uC5B4 \uC815\uB2F5\uC774 '\uAE00\uC790 \uC218'\uC774\uACE0<br /><strong>{{numCharOptionStr}}</strong> \uC635\uC158\uC744 \uC120\uD0DD\uD588\uB2E4\uBA74<br />\uC785\uB825 \uCE78\uC774 '\u2610\u2610 \u2610' \uCC98\uB7FC \uD45C\uC2DC\uB429\uB2C8\uB2E4.", value: { numCharOptionStr } }), placement: "top", children: _jsx(QuestionFillIcon, { css: css `
77
81
  width: 12px;
78
82
  height: 12px;
79
83
  ` }) })] }), _jsx(FormSegmentedControl, { control: control, trigger: trigger, name: "showCharacterNumber", items: [
80
- { value: false, label: "한 칸으로" },
81
- { value: true, label: "글자 수대로" },
82
- ] })] }), _jsxs(FormArea, { children: [_jsxs(Label, { children: ["\uC790\uB9AC \uD45C\uC2DC\uC790", _jsx(Tooltip, { text: _jsx("span", { children: "\uC785\uB825 \uCE78\uC5D0 \uAE30\uBCF8\uC73C\uB85C \uB178\uCD9C\uB418\uB294 \uD14D\uC2A4\uD2B8\uC785\uB2C8\uB2E4." }), placement: "top", children: _jsx(QuestionFillIcon, { css: css `
84
+ { value: false, label: t("한 칸으로") },
85
+ { value: true, label: numCharOptionStr },
86
+ ] })] }), _jsxs(FormArea, { children: [_jsxs(Label, { children: [t("자리 표시자", { context: "렉시컬 주관식 설정창" }), _jsx(Tooltip, { text: getTexts("descriptionDefaultInputText"), placement: "top", children: _jsx(QuestionFillIcon, { css: css `
83
87
  width: 12px;
84
88
  height: 12px;
85
- ` }) })] }), _jsx(FormPlaceholder, { control: control })] }), _jsxs(FormArea, { children: [_jsx(Label, { children: "\uB744\uC5B4\uC4F0\uAE30" }), _jsx(FormSegmentedControl, { control: control, trigger: trigger, name: "ignoreWhitespace", items: [
86
- { value: true, label: "무시하기" },
87
- { value: false, label: "포함하기" },
88
- ] })] }), _jsxs(FormArea, { children: [_jsx(Label, { children: "\uB300\uC18C\uBB38\uC790" }), _jsx(FormSegmentedControl, { control: control, trigger: trigger, name: "caseSensitive", items: [
89
- { value: false, label: "무시하기" },
90
- { value: true, label: "포함하기" },
91
- ] })] })] })] }), _jsxs(Buttons, { children: [_jsx(Button, { color: "grey", size: "xsmall", label: "\uB2EB\uAE30", onClick: onClose }), _jsx(Button, { color: "primary", size: "xsmall", label: "\uC774\uB300\uB85C \uB123\uAE30", type: "submit" })] })] }));
89
+ ` }) })] }), _jsx(FormPlaceholder, { control: control })] }), _jsxs(FormArea, { children: [_jsx(Label, { children: t("띄어쓰기") }), _jsx(FormSegmentedControl, { control: control, trigger: trigger, name: "ignoreWhitespace", items: [
90
+ { value: true, label: t("무시하기") },
91
+ { value: false, label: t("포함하기") },
92
+ ] })] }), _jsxs(FormArea, { children: [_jsx(Label, { children: t("대소문자") }), _jsx(FormSegmentedControl, { control: control, trigger: trigger, name: "caseSensitive", items: [
93
+ { value: false, label: t("무시하기") },
94
+ { value: true, label: t("포함하기") },
95
+ ] })] })] })] }), _jsxs(Buttons, { children: [_jsx(Button, { color: "grey", size: "xsmall", label: t("닫기", { context: "렉시컬 도구 설정창" }), onClick: onClose }), _jsx(Button, { color: "primary", size: "xsmall", label: t("이대로 넣기", { context: "렉시컬 도구 설정창" }), type: "submit" })] })] }));
92
96
  }
93
97
  const Form = styled.form(({ theme }) => css `
94
98
  display: flex;
@@ -2,10 +2,14 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Controller } from "react-hook-form";
3
3
  import { css, useTheme } from "@emotion/react";
4
4
  import { Dropdown, DropdownItem } from "../../../../..";
5
+ import { useTranslation } from "react-i18next";
5
6
  export function TextTypeDropdown(props) {
6
7
  const theme = useTheme();
7
8
  const { index, control, disabled } = props;
8
- return (_jsx(Controller, { name: `solutions.${index}.textType`, control: control, render: ({ field: { value, onChange } }) => (_jsx(Dropdown, { label: value === "normal" ? "일반 텍스트" : "코드 텍스트", size: "xsmall", color: "textNeutral", closeOnItemClick: true, disabled: disabled, buttonCss: css `
9
+ const { t } = useTranslation();
10
+ const normalLabel = t("일반 텍스트");
11
+ const codeLabel = t("코드 텍스트");
12
+ return (_jsx(Controller, { name: `solutions.${index}.textType`, control: control, render: ({ field: { value, onChange } }) => (_jsx(Dropdown, { label: value === "normal" ? normalLabel : codeLabel, size: "xsmall", color: "textNeutral", closeOnItemClick: true, disabled: disabled, buttonCss: css `
9
13
  ${disabled && `color: ${theme.color.foreground.neutralAlt};`}
10
14
  > span {
11
15
  font-weight: 700;
@@ -19,7 +23,7 @@ export function TextTypeDropdown(props) {
19
23
  vertical: "top",
20
24
  horizontal: "center",
21
25
  },
22
- }, children: _jsx(DropdownItem, { index: 0, label: value === "normal" ? "코드 텍스트" : "일반 텍스트", onClick: () => {
26
+ }, children: _jsx(DropdownItem, { index: 0, label: value === "normal" ? codeLabel : normalLabel, onClick: () => {
23
27
  onChange(value === "normal" ? "code" : "normal");
24
28
  } }) })) }));
25
29
  }
@@ -6,6 +6,8 @@ import { SelectBoxComponent } from "./SelectBoxComponent";
6
6
  import { Tag } from "../../../../../components/Tag";
7
7
  import { useRef } from "react";
8
8
  import { useEventListener } from "usehooks-ts";
9
+ import { i18n } from "../../../../../i18n/i18n";
10
+ import { useTranslation } from "react-i18next";
9
11
  const TYPE_TO_INDEX_ICON = (type) => ({
10
12
  primary: (_jsx(CheckFillIcon, { css: css `
11
13
  width: 12px;
@@ -28,30 +30,35 @@ function getTypeAndDescription(isSelected, isAnswer, onClick) {
28
30
  if (isAnswer === undefined) {
29
31
  return {
30
32
  type: isSelected ? "primary" : undefined,
31
- description: isSelected ? "선택됨" : onClick ? "선택하기" : "",
33
+ description: isSelected
34
+ ? i18n.t("선택됨")
35
+ : onClick
36
+ ? i18n.t("선택하기")
37
+ : "",
32
38
  };
33
39
  }
34
40
  if (isAnswer) {
35
41
  return {
36
42
  type: isSelected ? "success" : undefined,
37
43
  description: isSelected
38
- ? "선택됨, 정답"
44
+ ? i18n.t("선택됨, 정답", { context: "렉시컬 선택지 하나의 상태" })
39
45
  : onClick
40
- ? "선택하기, 정답"
41
- : "정답",
46
+ ? i18n.t("선택하기, 정답", { context: "렉시컬 선택지 하나의 상태" })
47
+ : i18n.t("정답", { context: "렉시컬 선택지 하나의 상태" }),
42
48
  };
43
49
  }
44
50
  return {
45
51
  type: isSelected ? "danger" : undefined,
46
52
  description: isSelected
47
- ? "선택됨, 오답"
53
+ ? i18n.t("선택됨, 오답", { context: "렉시컬 선택지 하나의 상태" })
48
54
  : onClick
49
- ? "선택하기, 오답"
50
- : "오답",
55
+ ? i18n.t("선택하기, 오답", { context: "렉시컬 선택지 하나의 상태" })
56
+ : i18n.t("오답", { context: "렉시컬 선택지 하나의 상태" }),
51
57
  };
52
58
  }
53
59
  export function SelectBoxView(props) {
54
60
  const { multipleSelectionsEnabled, index, isSelected, isAnswer, image, text, onClick, } = props;
61
+ const { t } = useTranslation();
55
62
  const { type, description } = getTypeAndDescription(isSelected, isAnswer, onClick);
56
63
  const boxRef = useRef(null);
57
64
  const handleKeyDown = (e) => {
@@ -71,13 +78,15 @@ export function SelectBoxView(props) {
71
78
  };
72
79
  useEventListener("keydown", handleKeyDown);
73
80
  // text가 있으면 text, 없으면 image의 altText, 없으면 index를 aria-label로 사용
74
- const ariaText = text || (image && image.altText) || index.toString() + "번 선택지";
81
+ const ariaText = text ||
82
+ (image && image.altText) ||
83
+ t("{{index}}번 선택지", { index, context: "렉시컬 선택지 도구" });
75
84
  return (_jsx(SelectBoxComponent, { css: css `
76
85
  width: 100%;
77
86
  `, ref: boxRef, role: multipleSelectionsEnabled ? "checkbox" : "radio", "aria-checked": isSelected, "aria-label": [ariaText, description].filter(Boolean).join(" "), tabIndex: onClick ? 0 : -1, type: type, index: type ? TYPE_TO_INDEX_ICON(type) : index, image: image, text: text, onClick: onClick,
78
87
  // 선택되지 않았으나 정답일 때 정답 태그를 표시
79
88
  endIcon: isAnswer &&
80
- !isSelected && (_jsx(Tag, { label: "\uC815\uB2F5", icon: _jsx(CheckboxCircleFillIcon, {}), color: "green", css: css `
89
+ !isSelected && (_jsx(Tag, { label: t("정답", { context: "태그, 렉시컬 선택지" }), icon: _jsx(CheckboxCircleFillIcon, {}), color: "green", css: css `
81
90
  span {
82
91
  font-weight: 700;
83
92
  }