@@ -295,7 +310,7 @@ const Carousel: FC
= ({ isOpen, onClose }) => {
- 学籍番号
+ {review.studentId}
@@ -306,25 +321,30 @@ const Carousel: FC = ({ isOpen, onClose }) => {
-
学年
+
+ {review.grade}
+
{
- GradeList.find((grade) => grade.id === values.gradeId)
- ?.name
+ gradeOptions.find(
+ (grade) => grade.id === values.gradeId
+ )?.name
}
-
学科
+
+ {review.department}
+
{
- DepartmentList.find(
+ departmentOptions.find(
(department) =>
department.id === values.departmentId
)?.name
@@ -357,7 +377,7 @@ const Carousel: FC = ({ isOpen, onClose }) => {
icon="lessThan"
isDisable={isLoading}
>
- 修正
+ {buttons.previous}
)}
{stepIndex === 2 ? (
@@ -368,7 +388,7 @@ const Carousel: FC = ({ isOpen, onClose }) => {
onClick={handleRegisterClick}
isDisable={isLoading}
>
- 登録
+ {buttons.submit}
) : (
)}
diff --git a/user/src/components/RegisterCarousel/hooks.ts b/user/src/components/RegisterCarousel/hooks.ts
new file mode 100644
index 000000000..a36a233ca
--- /dev/null
+++ b/user/src/components/RegisterCarousel/hooks.ts
@@ -0,0 +1,54 @@
+import { useMemo } from 'react';
+import { getDepartmentOptions, getGradeOptions } from '@/utils/list';
+import { useTranslation } from 'next-i18next';
+
+export const useRegisterCarouselTexts = () => {
+ const { t } = useTranslation('common');
+
+ const gradeOptions = useMemo(() => getGradeOptions(t), [t]);
+ const departmentOptions = useMemo(() => getDepartmentOptions(t), [t]);
+
+ return {
+ steps: {
+ email: t('registerCarousel.steps.email'),
+ representative: t('registerCarousel.steps.representative'),
+ confirm: t('registerCarousel.steps.confirm'),
+ },
+ labels: {
+ email: t('registerCarousel.labels.email'),
+ password: t('registerCarousel.labels.password'),
+ passwordConfirm: t('registerCarousel.labels.passwordConfirm'),
+ name: t('registerCarousel.labels.name'),
+ tel: t('registerCarousel.labels.tel'),
+ studentId: t('registerCarousel.labels.studentId'),
+ grade: t('registerCarousel.labels.grade'),
+ department: t('registerCarousel.labels.department'),
+ },
+ notes: {
+ email: t('registerCarousel.notes.email'),
+ password: t('registerCarousel.notes.password'),
+ passwordConfirm: t('registerCarousel.notes.passwordConfirm'),
+ name: t('registerCarousel.notes.name'),
+ tel: t('registerCarousel.notes.tel'),
+ studentId: t('registerCarousel.notes.studentId'),
+ },
+ review: {
+ email: t('registerCarousel.review.email'),
+ password: t('registerCarousel.review.password'),
+ name: t('registerCarousel.review.name'),
+ tel: t('registerCarousel.review.tel'),
+ studentId: t('registerCarousel.review.studentId'),
+ grade: t('registerCarousel.review.grade'),
+ department: t('registerCarousel.review.department'),
+ },
+ buttons: {
+ previous: t('registerCarousel.buttons.previous'),
+ submit: t('registerCarousel.buttons.submit'),
+ next: t('registerCarousel.buttons.next'),
+ },
+ options: {
+ grades: gradeOptions,
+ departments: departmentOptions,
+ },
+ };
+};
diff --git a/user/src/components/RegisterCarousel/useRegistration.ts b/user/src/components/RegisterCarousel/useRegistration.ts
index 54260b2ee..6bdf1439b 100644
--- a/user/src/components/RegisterCarousel/useRegistration.ts
+++ b/user/src/components/RegisterCarousel/useRegistration.ts
@@ -1,31 +1,33 @@
import { useCallback, useState } from 'react';
import { useRouter } from 'next/router';
+import type { TFunction } from 'i18next';
import { signIn } from 'next-auth/react';
+import { useTranslation } from 'next-i18next';
import type { UseFormHandleSubmit } from 'react-hook-form';
import { toast } from 'react-toastify';
import type { RegisterFormSchema } from './schema';
type ApiErrors = Record
;
-const ERROR_MESSAGES: Record> = {
+const ERROR_MESSAGE_KEYS: Record> = {
email: {
- 'has already been taken': 'このメールアドレスは既に登録されています。',
- default: 'メールアドレスに誤りがあります。',
+ 'has already been taken': 'registerCarousel.errors.emailTaken',
+ default: 'registerCarousel.errors.emailDefault',
},
password: {
- 'is too short': 'パスワードは6文字以上である必要があります。',
- default: 'パスワードに誤りがあります。',
+ 'is too short': 'registerCarousel.errors.passwordShort',
+ default: 'registerCarousel.errors.passwordDefault',
},
password_confirmation: {
- "doesn't match Password": 'パスワードが一致しません。',
- default: 'パスワード確認に誤りがあります。',
+ "doesn't match Password": 'registerCarousel.errors.passwordConfirmMismatch',
+ default: 'registerCarousel.errors.passwordConfirmDefault',
},
user_details: {
- tel: '電話番号に誤りがあります。',
- student_id: '学籍番号に誤りがあります。',
- grade_id: '学年に誤りがあります。',
- department_id: '学科に誤りがあります。',
- default: 'ユーザー詳細情報に誤りがあります。',
+ tel: 'registerCarousel.errors.telInvalid',
+ student_id: 'registerCarousel.errors.studentIdInvalid',
+ grade_id: 'registerCarousel.errors.gradeInvalid',
+ department_id: 'registerCarousel.errors.departmentInvalid',
+ default: 'registerCarousel.errors.userDetailsDefault',
},
};
@@ -39,22 +41,23 @@ const STEP_FIELDS: Record = {
* @param errors APIから返されたエラー情報
* @returns 対応するエラーメッセージ(なければ空文字)
*/
-function mapErrorMessage(errors: ApiErrors = {}): string {
+function mapErrorMessage(
+ errors: ApiErrors = {},
+ t: TFunction<'common'>
+): string {
for (const [field, msgs] of Object.entries(errors)) {
- // 各フィールドに対応するエラーメッセージのマッピングを取得
- const mapping = ERROR_MESSAGES[field] || {};
- for (const key of Object.keys(mapping)) {
- // キーワードが一致するエラーメッセージを返す
+ const mapping = ERROR_MESSAGE_KEYS[field];
+ if (!mapping) continue;
+
+ for (const [key, translationKey] of Object.entries(mapping)) {
if (key !== 'default' && msgs.some((m) => m.includes(key))) {
- return mapping[key];
+ return t(translationKey);
}
}
- // デフォルトメッセージがあればそれを返す
if (mapping.default) {
- return mapping.default;
+ return t(mapping.default);
}
}
- // 該当するメッセージがない場合は空文字を返す
return '';
}
@@ -92,6 +95,7 @@ export const useRegistration = (
const [isLoading, setIsLoading] = useState(false); // ローディング状態
const [displayError, setDisplayError] = useState(); // 表示するエラーメッセージ
const router = useRouter(); // ルーターオブジェクト
+ const { t } = useTranslation('common');
/**
* APIエラー発生時に対応するステップに移動
@@ -149,8 +153,8 @@ export const useRegistration = (
if (result.status === 'success') {
// 登録成功時の処理
- toast.success('登録が完了しました。');
- toast.info('自動でログインします。そのままお待ちください。');
+ toast.success(t('registerCarousel.toasts.registrationSuccess'));
+ toast.info(t('registerCarousel.toasts.autoLogin'));
await signIn('credentials', {
redirect: false,
@@ -158,28 +162,28 @@ export const useRegistration = (
password: data.password,
})
.then(() => {
- toast.success('ログインしました。');
+ toast.success(t('registerCarousel.toasts.loginSuccess'));
router.push('/home'); // ホーム画面にリダイレクト
})
.catch((error) => {
console.error('Login error:', error); // ログインエラーをログ出力
- toast.error('ログインに失敗しました。');
- toast.info('再度ログインしてください。');
+ toast.error(t('registerCarousel.toasts.loginFailed'));
+ toast.info(t('registerCarousel.toasts.retryLogin'));
router.push('/'); // トップページにリダイレクト
});
return;
} else {
// エラー時の処理
const message =
- mapErrorMessage(result.errors) ||
+ mapErrorMessage(result.errors, t) ||
result.message ||
- '通信エラーが発生しました。';
+ t('registerCarousel.errors.requestError');
setDisplayError(message); // エラーメッセージを設定
navigateToStep(result.errors); // エラーが発生したステップに移動
}
} catch {
// 通信エラー時の処理
- setDisplayError('通信に失敗しました。時間をおいて再度お試しください。');
+ setDisplayError(t('registerCarousel.errors.requestFailed'));
} finally {
setIsLoading(false); // 最終的にローディング状態を終了
}
diff --git a/user/src/components/Status/Status.tsx b/user/src/components/Status/Status.tsx
index 0ce7de825..5338372d3 100755
--- a/user/src/components/Status/Status.tsx
+++ b/user/src/components/Status/Status.tsx
@@ -1,3 +1,5 @@
+import { StatusTranslationKey, useStatusTexts } from './hooks';
+
type ValidStatus =
| { statusType: 'reception'; status: 'open' | 'deadline' | 'closed' }
| { statusType: 'registration'; status: 'registered' | 'unregistered' }
@@ -12,58 +14,49 @@ type StatusProps = Extract<
>;
type StyleDefinition = {
- statusText: string;
backgroundColor: string;
textColor: string;
statusType: ValidStatus['statusType'];
};
-const STATUS_MAP: Record = {
+const STATUS_STYLE_MAP: Record = {
open: {
statusType: 'reception',
- statusText: '受付中',
backgroundColor: 'bg-main border-main',
textColor: 'text-baseColor',
},
deadline: {
statusType: 'reception',
- statusText: '締切間近',
backgroundColor: 'bg-alert border-alert',
textColor: 'text-baseColor',
},
closed: {
statusType: 'reception',
- statusText: '受付終了',
backgroundColor: 'bg-baseColor border-sub',
textColor: 'text-sub',
},
registered: {
statusType: 'registration',
- statusText: '登録済',
backgroundColor: 'bg-baseColor border-sub',
textColor: 'text-sub',
},
unregistered: {
statusType: 'registration',
- statusText: '未登録',
backgroundColor: 'bg-alert border-alert',
textColor: 'text-baseColor',
},
not_required: {
statusType: 'progress',
- statusText: '不要',
backgroundColor: 'bg-baseColor border-sub',
textColor: 'text-sub',
},
completed: {
statusType: 'progress',
- statusText: '済',
backgroundColor: 'bg-main border-main',
textColor: 'text-baseColor',
},
pending: {
statusType: 'progress',
- statusText: '末',
backgroundColor: 'bg-alert border-alert',
textColor: 'text-baseColor',
},
@@ -73,7 +66,8 @@ const Status = ({
statusType,
status,
}: StatusProps) => {
- const statusInfo = STATUS_MAP[status];
+ const { getStatusLabel } = useStatusTexts();
+ const statusInfo = STATUS_STYLE_MAP[status];
const commonBgStyle =
'flex items-center justify-center rounded-[15px] border-2 border-solid';
@@ -99,7 +93,7 @@ const Status = ({
- {statusInfo.statusText}
+ {getStatusLabel(status as StatusTranslationKey)}
);
diff --git a/user/src/components/Status/hooks.ts b/user/src/components/Status/hooks.ts
new file mode 100644
index 000000000..5172e1a66
--- /dev/null
+++ b/user/src/components/Status/hooks.ts
@@ -0,0 +1,22 @@
+import { useTranslation } from 'next-i18next';
+
+const STATUS_TRANSLATION_KEY = {
+ open: 'status.reception.open',
+ deadline: 'status.reception.deadline',
+ closed: 'status.reception.closed',
+ registered: 'status.registration.registered',
+ unregistered: 'status.registration.unregistered',
+ not_required: 'status.progress.notRequired',
+ completed: 'status.progress.completed',
+ pending: 'status.progress.pending',
+} as const;
+
+export type StatusTranslationKey = keyof typeof STATUS_TRANSLATION_KEY;
+
+export const useStatusTexts = () => {
+ const { t } = useTranslation('common');
+ const getStatusLabel = (status: StatusTranslationKey) =>
+ t(STATUS_TRANSLATION_KEY[status]);
+
+ return { getStatusLabel };
+};
diff --git a/user/src/components/Upload/Upload.tsx b/user/src/components/Upload/Upload.tsx
index 831c038a4..9f89964d8 100755
--- a/user/src/components/Upload/Upload.tsx
+++ b/user/src/components/Upload/Upload.tsx
@@ -1,5 +1,6 @@
import { FC } from 'react';
import { MdUploadFile } from 'react-icons/md';
+import { useUploadTexts } from './hooks';
// NOTE: 箇条書きで列挙できるようにnoteを配列で定義
type UploadProps = {
@@ -19,6 +20,8 @@ const Upload: FC
= ({
error = '',
required = false,
}) => {
+ const uploadTexts = useUploadTexts();
+
return (
@@ -51,7 +56,7 @@ const Upload: FC = ({
))}
- {error}
+ {uploadTexts.translateError(error)}
);
diff --git a/user/src/components/Upload/hooks.ts b/user/src/components/Upload/hooks.ts
new file mode 100644
index 000000000..8429ee9d1
--- /dev/null
+++ b/user/src/components/Upload/hooks.ts
@@ -0,0 +1,13 @@
+import { useTranslation } from 'next-i18next';
+
+export const useUploadTexts = () => {
+ const { t } = useTranslation('common');
+ return {
+ labels: {
+ required: t('form.required'),
+ upload: t('form.actions.upload'),
+ },
+ translateError: (error?: string) =>
+ error ? t(error, { defaultValue: error }) : '',
+ };
+};
diff --git a/user/src/components/UserEditModal/UserEditModal.tsx b/user/src/components/UserEditModal/UserEditModal.tsx
index 4e616f444..2ad1a5f0d 100644
--- a/user/src/components/UserEditModal/UserEditModal.tsx
+++ b/user/src/components/UserEditModal/UserEditModal.tsx
@@ -1,11 +1,10 @@
import { FC } from 'react';
import { UserInformation } from '@/api/useUserDetailApi';
-import { DepartmentList, GradeList } from '@/utils/list';
import Button from '../Button';
import Selector from '../Form/Selector';
import TextBox from '../Form/TextBox';
import Modal from '../Modal';
-import { useUserEditModalHooks } from './hooks';
+import { useUserEditModalHooks, useUserEditModalTexts } from './hooks';
type UserEditModalProps = {
isOpen: boolean;
@@ -20,6 +19,7 @@ const UserEditModal: FC
= ({
userInformation,
mutate,
}) => {
+ const userEditModalTexts = useUserEditModalTexts();
const { errors, values, setValue, trigger, validateEdit, handleSubmitForm } =
useUserEditModalHooks(userInformation, mutate);
return (
@@ -29,57 +29,57 @@ const UserEditModal: FC = ({
setValue('name', value)}
onBlur={() => trigger('name')}
/>
setValue('mail', value)}
onBlur={() => trigger('mail')}
/>
setValue('tel', value)}
onBlur={() => trigger('tel')}
/>
setValue('studentId', value)}
onBlur={() => trigger('studentId')}
/>
setValue('gradeId', Number(value))}
- options={GradeList}
+ options={userEditModalTexts.gradeOptions}
value={values?.gradeId || 0}
error={errors.gradeId?.message}
/>
setValue('departmentId', Number(value))
}
- options={DepartmentList}
+ options={userEditModalTexts.departmentOptions}
value={values?.departmentId || 0}
error={errors.departmentId?.message}
/>
@@ -89,7 +89,7 @@ const UserEditModal: FC = ({
color="main"
isDisable={validateEdit()}
>
- 修正
+ {userEditModalTexts.actions.edit}
diff --git a/user/src/components/UserEditModal/hooks.ts b/user/src/components/UserEditModal/hooks.ts
index b58986a6e..36f83f283 100644
--- a/user/src/components/UserEditModal/hooks.ts
+++ b/user/src/components/UserEditModal/hooks.ts
@@ -1,7 +1,9 @@
-import { useEffect } from 'react';
+import { useEffect, useMemo } from 'react';
import { UserInformation, useMutateUserDetails } from '@/api/useUserDetailApi';
+import { getDepartmentOptions, getGradeOptions } from '@/utils/list';
import { zodResolver } from '@hookform/resolvers/zod';
import { signOut } from 'next-auth/react';
+import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { toast } from 'react-toastify';
import { EditUserDetailsFormSchema, EditUserDetailsSchema } from './schema';
@@ -10,6 +12,7 @@ export const useUserEditModalHooks = (
userInformation?: UserInformation,
mutate?: () => void
) => {
+ const { t } = useTranslation('common');
// フォームの初期化
const formMethods = useForm({
resolver: zodResolver(EditUserDetailsSchema),
@@ -45,11 +48,9 @@ export const useUserEditModalHooks = (
// emailの変更を行う場合はリダイレクトされることをメッセージに出す
if (
isChangeEmail &&
- !window.confirm(
- 'メールアドレスを変更する場合は、変更後のメールアドレスで再度ログインする必要があります。パスワードは以前のものと同じです。'
- )
+ !window.confirm(t('userEditModal.messages.emailChangeConfirm'))
) {
- toast.success('変更はキャンセルされました。');
+ toast.success(t('userEditModal.toasts.cancelled'));
return;
}
@@ -66,12 +67,10 @@ export const useUserEditModalHooks = (
await trigger({
query: submitData,
});
- toast.success('ユーザー情報を登録しました。');
+ toast.success(t('userEditModal.toasts.updateSuccess'));
// メールアドレスを変更した場合はサインアウト
if (isChangeEmail) {
- toast.success(
- 'メールアドレスを変更しました。再度ログインしてください。'
- );
+ toast.success(t('userEditModal.toasts.emailChanged'));
// 1秒待ってからサインアウト
await new Promise((resolve) => setTimeout(resolve, 1000));
await signOut({ redirect: false });
@@ -87,9 +86,9 @@ export const useUserEditModalHooks = (
exception?.includes('RecordNotUnique') ||
exception?.includes('Duplicate entry')
) {
- toast.error('このメールアドレスはすでに使われています');
+ toast.error(t('userEditModal.errors.duplicateEmail'));
}
- toast.error('更新に失敗しました。');
+ toast.error(t('userEditModal.errors.updateFailed'));
}
};
@@ -116,3 +115,30 @@ export const useUserEditModalHooks = (
validateEdit, // フォームのバリデーション処理
};
};
+
+export const useUserEditModalTexts = () => {
+ const { t } = useTranslation('common');
+ const gradeOptions = useMemo(() => getGradeOptions(t), [t]);
+ const departmentOptions = useMemo(() => getDepartmentOptions(t), [t]);
+ return {
+ labels: {
+ name: t('userEditModal.labels.name'),
+ email: t('userEditModal.labels.email'),
+ tel: t('userEditModal.labels.tel'),
+ studentId: t('userEditModal.labels.studentId'),
+ grade: t('userEditModal.labels.grade'),
+ department: t('userEditModal.labels.department'),
+ },
+ notes: {
+ name: t('userEditModal.notes.name'),
+ email: t('userEditModal.notes.email'),
+ tel: t('userEditModal.notes.tel'),
+ studentId: t('userEditModal.notes.studentId'),
+ },
+ actions: {
+ edit: t('form.actions.edit'),
+ },
+ gradeOptions,
+ departmentOptions,
+ };
+};
diff --git a/user/src/components/UserEditModal/schema.ts b/user/src/components/UserEditModal/schema.ts
index c9c931925..8b2f3aadd 100644
--- a/user/src/components/UserEditModal/schema.ts
+++ b/user/src/components/UserEditModal/schema.ts
@@ -1,16 +1,27 @@
import { z } from 'zod';
+const VALIDATION_MESSAGES = {
+ NAME: 'userEditModal.validation.name',
+ STUDENT_ID: 'userEditModal.validation.studentId',
+ TEL: 'userEditModal.validation.tel',
+ TEL_MIN: 'userEditModal.validation.telMin',
+ TEL_MAX: 'userEditModal.validation.telMax',
+ EMAIL: 'userEditModal.validation.email',
+ DEPARTMENT: 'userEditModal.validation.department',
+ GRADE: 'userEditModal.validation.grade',
+} as const;
+
export const EditUserDetailsSchema = z.object({
- name: z.string().min(1, '名前は必須です'),
- studentId: z.string().regex(/^\d{8}$/, '8桁の学籍番号を入力してください'),
+ name: z.string().min(1, VALIDATION_MESSAGES.NAME),
+ studentId: z.string().regex(/^\d{8}$/, VALIDATION_MESSAGES.STUDENT_ID),
tel: z
.string()
- .regex(/^0\d{9,10}$/, '有効な電話番号を入力してください(例: 09012345678)')
- .min(10, '電話番号が短すぎます')
- .max(11, '電話番号が長すぎます'),
- mail: z.string().email('有効なメールアドレスを入力してください'),
- departmentId: z.number().min(1, '学科を選択してください'),
- gradeId: z.number().min(1, '学年を選択してください'),
+ .regex(/^0\d{9,10}$/, VALIDATION_MESSAGES.TEL)
+ .min(10, VALIDATION_MESSAGES.TEL_MIN)
+ .max(11, VALIDATION_MESSAGES.TEL_MAX),
+ mail: z.string().email(VALIDATION_MESSAGES.EMAIL),
+ departmentId: z.number().min(1, VALIDATION_MESSAGES.DEPARTMENT),
+ gradeId: z.number().min(1, VALIDATION_MESSAGES.GRADE),
});
export type EditUserDetailsFormSchema = z.infer;
diff --git a/user/src/components/WelcomeBox/WelcomeBox.tsx b/user/src/components/WelcomeBox/WelcomeBox.tsx
index 5775d8a36..f6b4c1030 100755
--- a/user/src/components/WelcomeBox/WelcomeBox.tsx
+++ b/user/src/components/WelcomeBox/WelcomeBox.tsx
@@ -1,5 +1,6 @@
import { FC } from 'react';
import Button from '../Button';
+import { useWelcomeBoxTexts } from './hooks';
type WelcomeBoxProps = {
handleRegisterClick?: () => void;
@@ -10,6 +11,8 @@ const WelcomeBox: FC = ({
handleLoginClick,
handleRegisterClick,
}) => {
+ const welcomeBoxTexts = useWelcomeBoxTexts();
+
return (
@@ -19,18 +22,18 @@ const WelcomeBox: FC
= ({
color="main"
onClick={handleRegisterClick}
>
- 新規登録
+ {welcomeBoxTexts.buttons.register}
- 初めての方はこちら
+ {welcomeBoxTexts.descriptions.register}
- すでにアカウントをお持ちの方はこちら
+ {welcomeBoxTexts.descriptions.login}
diff --git a/user/src/components/WelcomeBox/hooks.ts b/user/src/components/WelcomeBox/hooks.ts
new file mode 100644
index 000000000..a883b86c6
--- /dev/null
+++ b/user/src/components/WelcomeBox/hooks.ts
@@ -0,0 +1,15 @@
+import { useTranslation } from 'next-i18next';
+
+export const useWelcomeBoxTexts = () => {
+ const { t } = useTranslation('common');
+ return {
+ buttons: {
+ register: t('welcomeBox.register'),
+ login: t('welcomeBox.login'),
+ },
+ descriptions: {
+ register: t('welcomeBox.registerDescription'),
+ login: t('welcomeBox.loginDescription'),
+ },
+ };
+};
diff --git a/user/src/pages/_app.tsx b/user/src/pages/_app.tsx
index 4f87aa000..9af7fda03 100644
--- a/user/src/pages/_app.tsx
+++ b/user/src/pages/_app.tsx
@@ -1,6 +1,7 @@
import type { AppProps } from 'next/app';
import type { Session } from 'next-auth';
import { SessionProvider } from 'next-auth/react';
+import { appWithTranslation } from 'next-i18next';
import { ToastContainer } from 'react-toastify';
import AuthGuard from '@/components/AuthGuard';
import Layout from '@/components/Layout';
@@ -11,7 +12,7 @@ type CustomAppProps = AppProps<{
session: Session;
}>;
-export default function App({
+function App({
Component,
pageProps: { session, ...pageProps },
}: CustomAppProps) {
@@ -40,3 +41,5 @@ export default function App({
);
}
+
+export default appWithTranslation(App);
diff --git a/user/src/pages/home/index.tsx b/user/src/pages/home/index.tsx
index 193515ffc..2c11fe71d 100644
--- a/user/src/pages/home/index.tsx
+++ b/user/src/pages/home/index.tsx
@@ -1,7 +1,9 @@
+import type { GetStaticProps } from 'next';
import { useGetCheckAllRegisteredGroups } from '@/api/checkAllRegisteredApi';
import { useGetGroupByUserId } from '@/api/groupApi';
import { useGetUserPageSettings } from '@/api/userPageSettingAPI';
import { GROUP_CATEGORY } from '@/utils/constants';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import CookingProcessOrder from '@/components/Applications/CookingProcessOrder';
import Employees from '@/components/Applications/Employees/Employees';
import FoodProduct from '@/components/Applications/FoodProduct';
@@ -294,3 +296,9 @@ export default function HomePage() {
);
}
+
+export const getStaticProps: GetStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'ja', ['common'])),
+ },
+});
diff --git a/user/src/pages/index.tsx b/user/src/pages/index.tsx
index 5e9fc55a5..9b4b6c708 100644
--- a/user/src/pages/index.tsx
+++ b/user/src/pages/index.tsx
@@ -1,4 +1,6 @@
import { useState } from 'react';
+import type { GetStaticProps } from 'next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import LoginModal from '@/components/LoginModal';
import NewsList from '@/components/NewsList';
import RegisterCarousel from '@/components/RegisterCarousel';
@@ -42,3 +44,9 @@ export default function Home() {
>
);
}
+
+export const getStaticProps: GetStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'ja', ['common'])),
+ },
+});
diff --git a/user/src/utils/list.ts b/user/src/utils/list.ts
index f340b565f..38551f8d2 100644
--- a/user/src/utils/list.ts
+++ b/user/src/utils/list.ts
@@ -1,153 +1,102 @@
-export const GradeList: { id: number; name: string }[] = [
- { id: 0, name: '選択してください' },
- { id: 1, name: 'B1[学部1年]' },
- { id: 2, name: 'B2[学部2年]' },
- { id: 3, name: 'B3[学部3年]' },
- { id: 4, name: 'B4[学部4年]' },
- { id: 5, name: 'M1[修士1年]' },
- { id: 6, name: 'M2[修士2年]' },
- { id: 7, name: 'D1[博士1年]' },
- { id: 8, name: 'D2[博士2年]' },
- { id: 9, name: 'D3[博士3年]' },
- { id: 10, name: 'GD1[イノベ1年]' },
- { id: 11, name: 'GD2[イノベ2年]' },
- { id: 12, name: 'GD3[イノベ3年]' },
- { id: 13, name: 'GD4[イノベ4年]' },
- { id: 14, name: 'GD5[イノベ5年]' },
- { id: 15, name: '教員:university staff' },
- { id: 16, name: 'その他:other' },
-];
+import { TFunction } from 'i18next';
-export const DepartmentList: { id: number; name: string }[] = [
- { id: 0, name: '選択してください' },
- { id: 1, name: '機械工学分野/機械創造工学課程:Mechanical Engineering' },
- {
- id: 2,
- name: '電気電子情報工学分野/電気電子情報工学課程:Electrical, Electronics and Information Engineering',
- },
- {
- id: 3,
- name: '物質生物工学分野/物質材料工学課程/生物機能工学課程:Materials Science and Technology',
- },
- {
- id: 4,
- name: '環境社会基盤工学分野/環境社会基盤工学課程:Civil and Environmental Engineering',
- },
- {
- id: 5,
- name: '情報・経営システム工学分野/情報・経営システム工学課程:Information and Management Systems Engineering',
- },
- { id: 6, name: '機械工学分野/機械創造工学専攻:Mechanical Engineering' },
- {
- id: 7,
- name: '電気電子情報工学分野/電気電子情報工学専攻:Electrical, Electronics and Information Engineering',
- },
- {
- id: 8,
- name: '物質生物工学分野/物質材料工学専攻/生物機能工学専攻:Materials Science and Technology',
- },
- {
- id: 9,
- name: '環境社会基盤工学分野/環境社会基盤工学専攻:Civil and Environmental Engineering',
- },
- {
- id: 10,
- name: '情報・経営システム工学分野/情報・経営システム工学専攻:Information and Management Systems Engineering',
- },
- {
- id: 11,
- name: '量子・原子力統合工学分野/原子力システム安全工学専攻:Integrated Quantum and Nuclear Engineering',
- },
- { id: 12, name: 'システム安全工学専攻:System Safety Engineering' },
- { id: 13, name: '技術科学イノベーション専攻:GIGAKU Innovation Group' },
- {
- id: 14,
- name: '情報・制御工学分野/情報・制御工学専攻:Information Science and Control Engineering',
- },
- { id: 15, name: '材料工学分野/材料工学専攻:Materials Science' },
- {
- id: 16,
- name: 'エネルギー工学分野/エネルギー・環境工学専攻:Energy Engineering',
- },
- {
- id: 17,
- name: '社会環境・生物機能工学分野/生物統合工学専攻:Integrated Bioscience and Technology',
- },
- { id: 18, name: 'その他:other' },
-];
+export type TranslatedOption = {
+ id: number;
+ labelKey: string;
+ disabled?: boolean;
+};
-export const B1AndOtherGradeDepartmentList: { id: number; name: string }[] = [
- { id: 18, name: 'その他:other' },
-];
+export type SelectorOption = {
+ id: number;
+ name: string;
+ disabled?: boolean;
+};
-export const B2toB4GradeDepartmentList: { id: number; name: string }[] = [
- { id: 1, name: '機械工学分野/機械創造工学課程:Mechanical Engineering' },
- {
- id: 2,
- name: '電気電子情報工学分野/電気電子情報工学課程:Electrical, Electronics and Information Engineering',
- },
- {
- id: 3,
- name: '物質生物工学分野/物質材料工学課程/生物機能工学課程:Materials Science and Technology',
- },
- {
- id: 4,
- name: '環境社会基盤工学分野/環境社会基盤工学課程:Civil and Environmental Engineering',
- },
- {
- id: 5,
- name: '情報・経営システム工学分野/情報・経営システム工学課程:Information and Management Systems Engineering',
- },
-];
+const createDepartmentSubset = (ids: number[]) =>
+ ids.map((id) => ({ id, labelKey: `lists.department.${id}` }));
-export const M1toM2GradeDepartmentList: { id: number; name: string }[] = [
- { id: 6, name: '機械工学分野/機械創造工学専攻:Mechanical Engineering' },
- {
- id: 7,
- name: '電気電子情報工学分野/電気電子情報工学専攻:Electrical, Electronics and Information Engineering',
- },
- {
- id: 8,
- name: '物質生物工学分野/物質材料工学専攻/生物機能工学専攻:Materials Science and Technology',
- },
- {
- id: 9,
- name: '環境社会基盤工学分野/環境社会基盤工学専攻:Civil and Environmental Engineering',
- },
- {
- id: 10,
- name: '情報・経営システム工学分野/情報・経営システム工学専攻:Information and Management Systems Engineering',
- },
- {
- id: 11,
- name: '量子・原子力統合工学分野/原子力システム安全工学専攻:Integrated Quantum and Nuclear Engineering',
- },
- { id: 12, name: 'システム安全工学専攻:System Safety Engineering' },
+export const GradeList: TranslatedOption[] = [
+ { id: 0, labelKey: 'lists.grade.select' },
+ { id: 1, labelKey: 'lists.grade.B1' },
+ { id: 2, labelKey: 'lists.grade.B2' },
+ { id: 3, labelKey: 'lists.grade.B3' },
+ { id: 4, labelKey: 'lists.grade.B4' },
+ { id: 5, labelKey: 'lists.grade.M1' },
+ { id: 6, labelKey: 'lists.grade.M2' },
+ { id: 7, labelKey: 'lists.grade.D1' },
+ { id: 8, labelKey: 'lists.grade.D2' },
+ { id: 9, labelKey: 'lists.grade.D3' },
+ { id: 10, labelKey: 'lists.grade.GD1' },
+ { id: 11, labelKey: 'lists.grade.GD2' },
+ { id: 12, labelKey: 'lists.grade.GD3' },
+ { id: 13, labelKey: 'lists.grade.GD4' },
+ { id: 14, labelKey: 'lists.grade.GD5' },
+ { id: 15, labelKey: 'lists.grade.faculty' },
+ { id: 16, labelKey: 'lists.grade.other' },
];
-export const D1toD3GradeDepartmentList: { id: number; name: string }[] = [
- {
- id: 14,
- name: '情報・制御工学分野/情報・制御工学専攻:Information Science and Control Engineering',
- },
- { id: 15, name: '材料工学分野/材料工学専攻:Materials Science' },
- {
- id: 16,
- name: 'エネルギー工学分野/エネルギー・環境工学専攻:Energy Engineering',
- },
- {
- id: 17,
- name: '社会環境・生物機能工学分野/生物統合工学専攻:Integrated Bioscience and Technology',
- },
+export const DepartmentList: TranslatedOption[] = [
+ { id: 0, labelKey: 'lists.department.select' },
+ { id: 1, labelKey: 'lists.department.1' },
+ { id: 2, labelKey: 'lists.department.2' },
+ { id: 3, labelKey: 'lists.department.3' },
+ { id: 4, labelKey: 'lists.department.4' },
+ { id: 5, labelKey: 'lists.department.5' },
+ { id: 6, labelKey: 'lists.department.6' },
+ { id: 7, labelKey: 'lists.department.7' },
+ { id: 8, labelKey: 'lists.department.8' },
+ { id: 9, labelKey: 'lists.department.9' },
+ { id: 10, labelKey: 'lists.department.10' },
+ { id: 11, labelKey: 'lists.department.11' },
+ { id: 12, labelKey: 'lists.department.12' },
+ { id: 13, labelKey: 'lists.department.13' },
+ { id: 14, labelKey: 'lists.department.14' },
+ { id: 15, labelKey: 'lists.department.15' },
+ { id: 16, labelKey: 'lists.department.16' },
+ { id: 17, labelKey: 'lists.department.17' },
+ { id: 18, labelKey: 'lists.department.18' },
];
-export const GDGradeDepartmentList: { id: number; name: string }[] = [
- { id: 13, name: '技術科学イノベーション専攻:GIGAKU Innovation Group' },
-];
+export const B1AndOtherGradeDepartmentList: TranslatedOption[] =
+ createDepartmentSubset([18]);
+
+export const B2toB4GradeDepartmentList: TranslatedOption[] =
+ createDepartmentSubset([1, 2, 3, 4, 5]);
+
+export const M1toM2GradeDepartmentList: TranslatedOption[] =
+ createDepartmentSubset([6, 7, 8, 9, 10, 11, 12]);
+
+export const D1toD3GradeDepartmentList: TranslatedOption[] =
+ createDepartmentSubset([14, 15, 16, 17]);
+
+export const GDGradeDepartmentList: TranslatedOption[] = createDepartmentSubset(
+ [13]
+);
+
+export const mapToLocalizedOptions = (
+ options: TranslatedOption[],
+ t: TFunction
+): SelectorOption[] =>
+ options.map(({ id, labelKey, disabled }) => ({
+ id,
+ name: t(labelKey),
+ disabled,
+ }));
+
+export const createDepartmentSelectorOptions = (
+ options: TranslatedOption[],
+ t: TFunction
+) => mapToLocalizedOptions(options, t);
+
+export const getGradeOptions = (t: TFunction) =>
+ mapToLocalizedOptions(GradeList, t);
+
+export const getDepartmentOptions = (t: TFunction) =>
+ mapToLocalizedOptions(DepartmentList, t);
export const GradeWithDepartmentList: {
id: number;
- departmentList: { id: number; name: string }[];
+ departmentList: TranslatedOption[];
}[] = [
{ id: 1, departmentList: B1AndOtherGradeDepartmentList },
{ id: 2, departmentList: B2toB4GradeDepartmentList },
diff --git a/user/src/utils/validate/validate.ts b/user/src/utils/validate/validate.ts
index fa6dbc7b8..ae971e2ba 100644
--- a/user/src/utils/validate/validate.ts
+++ b/user/src/utils/validate/validate.ts
@@ -117,31 +117,39 @@ export const placeSchema = z.object({
// stage登録のバリデーション
export const stageSchema = z
.object({
- date: z.string().nonempty('入力してください'),
- sunnyFirstChoice: z.string().nonempty('晴れの第1希望を選択してください'),
- sunnySecondChoice: z.string().nonempty('晴れの第2希望を選択してください'),
- rainyFirstChoice: z.string().nonempty('雨の第1希望を選択してください'),
- rainySecondChoice: z.string().nonempty('雨の第2希望を選択してください'),
+ date: z.string().nonempty('form.validation.required'),
+ sunnyFirstChoice: z
+ .string()
+ .nonempty('applications.stage.validation.sunnyFirst'),
+ sunnySecondChoice: z
+ .string()
+ .nonempty('applications.stage.validation.sunnySecond'),
+ rainyFirstChoice: z
+ .string()
+ .nonempty('applications.stage.validation.rainyFirst'),
+ rainySecondChoice: z
+ .string()
+ .nonempty('applications.stage.validation.rainySecond'),
prepTime: z
.string()
- .nonempty('準備時間を入力してください')
+ .nonempty('applications.stage.validation.prepTimeRequired')
.refine(
(val) => !isNaN(Number(val)) && Number(val) >= 0,
- '有効な準備時間を入力してください'
+ 'applications.stage.validation.prepTimeInvalid'
),
performTime: z
.string()
- .nonempty('本番時間を入力してください')
+ .nonempty('applications.stage.validation.performTimeRequired')
.refine(
(val) => !isNaN(Number(val)) && Number(val) >= 0,
- '有効な本番時間を入力してください'
+ 'applications.stage.validation.performTimeInvalid'
),
cleanupTime: z
.string()
- .nonempty('片付け時間を入力してください')
+ .nonempty('applications.stage.validation.cleanupTimeRequired')
.refine(
(val) => !isNaN(Number(val)) && Number(val) >= 0,
- '有効な片付け時間を入力してください'
+ 'applications.stage.validation.cleanupTimeInvalid'
),
remarks: z.string().optional(),
groupId: z.string().optional(),
@@ -155,7 +163,7 @@ export const stageSchema = z
return total <= 120;
},
{
- message: '準備、本番、片付けの合計時間が120分を超えています',
+ message: 'applications.stage.validation.totalTime',
path: ['totalTime'],
}
)
@@ -167,7 +175,7 @@ export const stageSchema = z
);
},
{
- message: '第1希望と異なるステージを選んでください',
+ message: 'applications.stage.validation.sunnyChoiceDuplicate',
path: ['sunnySecondChoice'],
}
)
@@ -179,7 +187,7 @@ export const stageSchema = z
);
},
{
- message: '第1希望と異なるステージを選んでください',
+ message: 'applications.stage.validation.rainyChoiceDuplicate',
path: ['rainySecondChoice'],
}
);