From 37eced5048885d3df4f656f3f3b60ec40288cfb3 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Tue, 30 Dec 2025 03:43:40 +0900 Subject: [PATCH 01/57] =?UTF-8?q?=E8=A8=80=E8=AA=9E=E5=88=87=E3=82=8A?= =?UTF-8?q?=E6=9B=BF=E3=81=88=E6=A9=9F=E8=83=BD=E3=81=AE=E3=81=9F=E3=82=81?= =?UTF-8?q?=E3=81=AE=E8=8B=B1=E8=AA=9E=E3=81=A8=E6=97=A5=E6=9C=AC=E8=AA=9E?= =?UTF-8?q?=E3=81=AE=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=A9=E3=82=A4=E3=82=BA?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/public/locales/en/common.json | 100 +++++++++++++++++++++++++++++ user/public/locales/ja/common.json | 100 +++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100644 user/public/locales/en/common.json create mode 100644 user/public/locales/ja/common.json diff --git a/user/public/locales/en/common.json b/user/public/locales/en/common.json new file mode 100644 index 000000000..72c2d500e --- /dev/null +++ b/user/public/locales/en/common.json @@ -0,0 +1,100 @@ +{ + "languageSwitcher": { + "label": "Language selector", + "japanese": "Japanese", + "english": "English" + }, + "news": { + "title": "News", + "none": "No news available.", + "loading": "Loading...", + "error": "An error has occurred" + }, + "welcomeBox": { + "register": "Register", + "registerDescription": "New users start here", + "login": "Log in", + "loginDescription": "Existing users sign in here" + }, + "loginModal": { + "emailLabel": "Email address", + "emailNote": "e.g., s123456@stn.nagaokaut.ac.jp", + "passwordLabel": "Password", + "submit": "Log in", + "submitting": "Signing in...", + "errors": { + "invalidCredentials": "Email or password is incorrect" + }, + "toasts": { + "loginFailed": "Failed to sign in", + "loginSuccess": "Signed in successfully" + } + }, + "registerCarousel": { + "steps": { + "email": "Email", + "representative": "Representative", + "confirm": "Review" + }, + "placeholders": { + "select": "Please select" + }, + "labels": { + "email": "Email address", + "password": "Password", + "passwordConfirm": "Confirm password", + "name": "Name", + "tel": "Phone number", + "studentId": "Student ID", + "grade": "Grade", + "department": "Department" + }, + "notes": { + "email": "e.g., s123456@stn.nagaokaut.ac.jp", + "password": "At least 8 alphanumeric characters", + "passwordConfirm": "At least 8 alphanumeric characters", + "name": "e.g., Taro Nagaoka", + "tel": "e.g., 09012345678", + "studentId": "e.g., 12345678" + }, + "review": { + "email": "Email address", + "password": "Password", + "name": "Name", + "tel": "Phone number", + "studentId": "Student ID", + "grade": "Grade", + "department": "Department" + }, + "buttons": { + "previous": "Edit", + "submit": "Submit", + "next": "Next" + }, + "errors": { + "emailTaken": "This email address is already registered.", + "emailDefault": "There is an issue with the email address.", + "passwordShort": "Password must be at least 6 characters.", + "passwordDefault": "There is an issue with the password.", + "passwordConfirmMismatch": "Passwords do not match.", + "passwordConfirmDefault": "There is an issue with the password confirmation.", + "telInvalid": "There is an issue with the phone number.", + "studentIdInvalid": "There is an issue with the student ID.", + "gradeInvalid": "There is an issue with the grade selection.", + "departmentInvalid": "There is an issue with the department selection.", + "userDetailsDefault": "There is an issue with the user details.", + "requestError": "A network error has occurred.", + "requestFailed": "The request failed. Please try again later." + }, + "toasts": { + "registrationSuccess": "Registration completed.", + "autoLogin": "Signing you in automatically. Please wait.", + "loginSuccess": "Signed in successfully.", + "loginFailed": "Failed to sign in.", + "retryLogin": "Please try signing in again." + } + }, + "footer": { + "copyright": "Copyright © {{year}} NUTMEG. All Rights Reserved." + } +} diff --git a/user/public/locales/ja/common.json b/user/public/locales/ja/common.json new file mode 100644 index 000000000..d10e1afcc --- /dev/null +++ b/user/public/locales/ja/common.json @@ -0,0 +1,100 @@ +{ + "languageSwitcher": { + "label": "言語切り替え", + "japanese": "日本語", + "english": "English" + }, + "news": { + "title": "お知らせ", + "none": "お知らせはありません。", + "loading": "読み込み中...", + "error": "エラーが発生しました" + }, + "welcomeBox": { + "register": "新規登録", + "registerDescription": "初めての方はこちら", + "login": "ログイン", + "loginDescription": "すでにアカウントをお持ちの方はこちら" + }, + "loginModal": { + "emailLabel": "メールアドレス", + "emailNote": "例:s123456@stn.nagaokaut.ac.jp", + "passwordLabel": "パスワード", + "submit": "ログイン", + "submitting": "ログイン中...", + "errors": { + "invalidCredentials": "emailかpasswordが違います" + }, + "toasts": { + "loginFailed": "ログインに失敗しました", + "loginSuccess": "ログインに成功しました" + } + }, + "registerCarousel": { + "steps": { + "email": "メールアドレス", + "representative": "代表者情報", + "confirm": "確認" + }, + "placeholders": { + "select": "選択してください" + }, + "labels": { + "email": "メールアドレス", + "password": "パスワード", + "passwordConfirm": "パスワード(確認用)", + "name": "名前", + "tel": "電話番号", + "studentId": "学籍番号", + "grade": "学年", + "department": "学科" + }, + "notes": { + "email": "例:s123456@stn.nagaokaut.ac.jp", + "password": "英数字8文字以上", + "passwordConfirm": "英数字8文字以上", + "name": "例:長岡 太郎", + "tel": "例:09012345678", + "studentId": "例:12345678" + }, + "review": { + "email": "メールアドレス", + "password": "パスワード", + "name": "名前", + "tel": "電話番号", + "studentId": "学籍番号", + "grade": "学年", + "department": "学科" + }, + "buttons": { + "previous": "修正", + "submit": "登録", + "next": "次へ" + }, + "errors": { + "emailTaken": "このメールアドレスは既に登録されています。", + "emailDefault": "メールアドレスに誤りがあります。", + "passwordShort": "パスワードは6文字以上である必要があります。", + "passwordDefault": "パスワードに誤りがあります。", + "passwordConfirmMismatch": "パスワードが一致しません。", + "passwordConfirmDefault": "パスワード確認に誤りがあります。", + "telInvalid": "電話番号に誤りがあります。", + "studentIdInvalid": "学籍番号に誤りがあります。", + "gradeInvalid": "学年に誤りがあります。", + "departmentInvalid": "学科に誤りがあります。", + "userDetailsDefault": "ユーザー詳細情報に誤りがあります。", + "requestError": "通信エラーが発生しました。", + "requestFailed": "通信に失敗しました。時間をおいて再度お試しください。" + }, + "toasts": { + "registrationSuccess": "登録が完了しました。", + "autoLogin": "自動でログインします。そのままお待ちください。", + "loginSuccess": "ログインしました。", + "loginFailed": "ログインに失敗しました。", + "retryLogin": "再度ログインしてください。" + } + }, + "footer": { + "copyright": "Copyright © {{year}} NUTMEG. All Rights Reserved." + } +} From db11018f5dea726bdf1298a0945161105c546b93 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Tue, 30 Dec 2025 03:44:07 +0900 Subject: [PATCH 02/57] =?UTF-8?q?[feat]=20i18next=E3=82=92=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E3=81=97=E3=81=9F=E5=A4=9A=E8=A8=80=E8=AA=9E=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C=E3=81=AE=E8=A8=AD=E5=AE=9A=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81next.conf?= =?UTF-8?q?ig.ts=E3=81=AB=E7=B5=B1=E5=90=88=E3=80=82=E5=BF=85=E8=A6=81?= =?UTF-8?q?=E3=81=AA=E4=BE=9D=E5=AD=98=E9=96=A2=E4=BF=82=E3=82=92package.j?= =?UTF-8?q?son=E3=81=AB=E8=BF=BD=E5=8A=A0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/next-i18next.config.js | 11 +++ user/next.config.ts | 3 + user/package.json | 4 + user/pnpm-lock.yaml | 147 +++++++++++++++++++++++++++++++++++- 4 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 user/next-i18next.config.js diff --git a/user/next-i18next.config.js b/user/next-i18next.config.js new file mode 100644 index 000000000..2993550c7 --- /dev/null +++ b/user/next-i18next.config.js @@ -0,0 +1,11 @@ +const path = require('path'); + +/** @type {import('next-i18next').UserConfig} */ +module.exports = { + i18n: { + defaultLocale: 'ja', + locales: ['ja', 'en'], + }, + localePath: path.resolve('./public/locales'), + reloadOnPrerender: process.env.NODE_ENV === 'development', +}; diff --git a/user/next.config.ts b/user/next.config.ts index f4b9fdeb2..3ae7b0279 100644 --- a/user/next.config.ts +++ b/user/next.config.ts @@ -1,6 +1,8 @@ // next.config.ts import type { NextConfig } from 'next'; import type { RuleSetRule } from 'webpack'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { i18n } = require('./next-i18next.config'); const apiConfig: { [key: string]: { @@ -34,6 +36,7 @@ const nextConfig: NextConfig = { SSR_API_URL, NEXT_PUBLIC_API_URL, }, + i18n, experimental: { turbo: { rules: { diff --git a/user/package.json b/user/package.json index 56f339b13..82de29863 100644 --- a/user/package.json +++ b/user/package.json @@ -28,12 +28,15 @@ "embla-carousel": "^8.6.0", "embla-carousel-react": "^8.5.2", "framer-motion": "^12.4.2", + "i18next": "^25.7.3", "next": "15.0.3", "next-auth": "^4.24.11", + "next-i18next": "^15.4.3", "process": "^0.11.10", "react": "19.0.0-rc-66855b96-20241106", "react-dom": "19.0.0-rc-66855b96-20241106", "react-hook-form": "^7.54.2", + "react-i18next": "^16.5.0", "react-icons": "^5.4.0", "react-textfitfix": "^1.1.0", "react-toastify": "^11.0.5", @@ -58,6 +61,7 @@ "@storybook/react": "^8.5.0", "@storybook/test": "^8.5.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "@types/i18next": "^13.0.0", "@types/node": "^20.17.24", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/user/pnpm-lock.yaml b/user/pnpm-lock.yaml index 2b8480200..16653078b 100644 --- a/user/pnpm-lock.yaml +++ b/user/pnpm-lock.yaml @@ -41,12 +41,18 @@ importers: framer-motion: specifier: ^12.4.2 version: 12.6.2(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + i18next: + specifier: ^25.7.3 + version: 25.7.3(typescript@5.8.2) next: specifier: 15.0.3 version: 15.0.3(@babel/core@7.26.10)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) next-auth: specifier: ^4.24.11 version: 4.24.11(next@15.0.3(@babel/core@7.26.10)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + next-i18next: + specifier: ^15.4.3 + version: 15.4.3(@types/react@18.3.20)(i18next@25.7.3(typescript@5.8.2))(next@15.0.3(@babel/core@7.26.10)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react-i18next@16.5.0(i18next@25.7.3(typescript@5.8.2))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.8.2))(react@19.0.0-rc-66855b96-20241106) process: specifier: ^0.11.10 version: 0.11.10 @@ -59,6 +65,9 @@ importers: react-hook-form: specifier: ^7.54.2 version: 7.55.0(react@19.0.0-rc-66855b96-20241106) + react-i18next: + specifier: ^16.5.0 + version: 16.5.0(i18next@25.7.3(typescript@5.8.2))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.8.2) react-icons: specifier: ^5.4.0 version: 5.5.0(react@19.0.0-rc-66855b96-20241106) @@ -88,7 +97,7 @@ importers: version: 3.24.2 zustand: specifier: ^5.0.3 - version: 5.0.3(@types/react@18.3.20)(react@19.0.0-rc-66855b96-20241106)(use-sync-external-store@1.5.0(react@19.0.0-rc-66855b96-20241106)) + version: 5.0.3(@types/react@18.3.20)(react@19.0.0-rc-66855b96-20241106)(use-sync-external-store@1.6.0(react@19.0.0-rc-66855b96-20241106)) devDependencies: '@chromatic-com/storybook': specifier: ^3.2.4 @@ -126,6 +135,9 @@ importers: '@trivago/prettier-plugin-sort-imports': specifier: ^5.2.2 version: 5.2.2(prettier@3.5.3) + '@types/i18next': + specifier: ^13.0.0 + version: 13.0.0(typescript@5.8.2) '@types/node': specifier: ^20.17.24 version: 20.17.28 @@ -743,6 +755,10 @@ packages: resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.0': resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} engines: {node: '>=6.9.0'} @@ -1663,9 +1679,18 @@ packages: '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/hoist-non-react-statics@3.3.7': + resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} + peerDependencies: + '@types/react': '*' + '@types/html-minifier-terser@6.1.0': resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} + '@types/i18next@13.0.0': + resolution: {integrity: sha512-gp/SIShAuf4WOqi8ey0nuI7qfWaVpMNCcs/xLygrh/QTQIXmlDC1E0TtVejweNW+7SGDY7g0lyxyKZIJuCKIJw==} + deprecated: This is a stub types definition. i18next provides its own type definitions, so you do not need this installed. + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -2453,6 +2478,9 @@ packages: core-js-pure@3.41.0: resolution: {integrity: sha512-71Gzp96T9YPk63aUvE5Q5qP+DryB4ZloUZPSOebGM88VNw8VNfvdA7z6kGA8iGOTEzAomsRidp4jXSmUIJsL+Q==} + core-js@3.47.0: + resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3299,6 +3327,9 @@ packages: hmac-drbg@1.0.1: resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + html-entities@2.5.5: resolution: {integrity: sha512-24CG9o869vSa86BGCf7x65slrAztzFTU5VBQzEIwqjhKuB4zCC7xlH/7NCcZ1EN5MdmGx9lUqugfutuT6J+jKQ==} @@ -3307,6 +3338,9 @@ packages: engines: {node: '>=12'} hasBin: true + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + html-webpack-plugin@5.6.3: resolution: {integrity: sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==} engines: {node: '>=10.13.0'} @@ -3325,6 +3359,17 @@ packages: https-browserify@1.0.0: resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} + i18next-fs-backend@2.6.1: + resolution: {integrity: sha512-eYWTX7QT7kJ0sZyCPK6x1q+R63zvNKv2D6UdbMf15A8vNb2ZLyw4NNNZxPFwXlIv/U+oUtg8SakW6ZgJZcoqHQ==} + + i18next@25.7.3: + resolution: {integrity: sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==} + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -3968,6 +4013,15 @@ packages: nodemailer: optional: true + next-i18next@15.4.3: + resolution: {integrity: sha512-ZRmiz72o1Jvh2ZghCUQX1Ua5F/f2W1/Ila/L1ZeKVuSWiH7J4zfUedfDxNBEhj9lajREC7aoJuPXMFtKi2bdIg==} + engines: {node: '>=14'} + peerDependencies: + i18next: '>= 23.7.13' + next: '>= 12.0.0' + react: '>= 17.0.2' + react-i18next: '>= 13.5.0' + next@15.0.3: resolution: {integrity: sha512-ontCbCRKJUIoivAdGB34yCaOcPgYXr9AAkV/IwqFfWWTXEPUgLYkSkqBhIk9KK7gGmgjc64B+RdoeIDM13Irnw==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -4453,6 +4507,22 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-i18next@16.5.0: + resolution: {integrity: sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw==} + peerDependencies: + i18next: '>= 25.6.2' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + react-icons@5.5.0: resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} peerDependencies: @@ -5184,6 +5254,11 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5219,6 +5294,10 @@ packages: vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + watchpack@2.4.2: resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} engines: {node: '>=10.13.0'} @@ -6117,6 +6196,8 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 + '@babel/runtime@7.28.4': {} + '@babel/template@7.27.0': dependencies: '@babel/code-frame': 7.26.2 @@ -7134,8 +7215,19 @@ snapshots: '@types/estree@1.0.7': {} + '@types/hoist-non-react-statics@3.3.7(@types/react@18.3.20)': + dependencies: + '@types/react': 18.3.20 + hoist-non-react-statics: 3.3.2 + '@types/html-minifier-terser@6.1.0': {} + '@types/i18next@13.0.0(typescript@5.8.2)': + dependencies: + i18next: 25.7.3(typescript@5.8.2) + transitivePeerDependencies: + - typescript + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -8007,6 +8099,8 @@ snapshots: core-js-pure@3.41.0: {} + core-js@3.47.0: {} + core-util-is@1.0.3: {} cosmiconfig@7.1.0: @@ -9055,6 +9149,10 @@ snapshots: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + html-entities@2.5.5: {} html-minifier-terser@6.1.0: @@ -9067,6 +9165,10 @@ snapshots: relateurl: 0.2.7 terser: 5.39.0 + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + html-webpack-plugin@5.6.3(webpack@5.98.0(esbuild@0.25.1)): dependencies: '@types/html-minifier-terser': 6.1.0 @@ -9086,6 +9188,14 @@ snapshots: https-browserify@1.0.0: {} + i18next-fs-backend@2.6.1: {} + + i18next@25.7.3(typescript@5.8.2): + dependencies: + '@babel/runtime': 7.28.4 + optionalDependencies: + typescript: 5.8.2 + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -9758,6 +9868,20 @@ snapshots: react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) uuid: 8.3.2 + next-i18next@15.4.3(@types/react@18.3.20)(i18next@25.7.3(typescript@5.8.2))(next@15.0.3(@babel/core@7.26.10)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react-i18next@16.5.0(i18next@25.7.3(typescript@5.8.2))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.8.2))(react@19.0.0-rc-66855b96-20241106): + dependencies: + '@babel/runtime': 7.27.0 + '@types/hoist-non-react-statics': 3.3.7(@types/react@18.3.20) + core-js: 3.47.0 + hoist-non-react-statics: 3.3.2 + i18next: 25.7.3(typescript@5.8.2) + i18next-fs-backend: 2.6.1 + next: 15.0.3(@babel/core@7.26.10)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-i18next: 16.5.0(i18next@25.7.3(typescript@5.8.2))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.8.2) + transitivePeerDependencies: + - '@types/react' + next@15.0.3(@babel/core@7.26.10)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): dependencies: '@next/env': 15.0.3 @@ -10284,6 +10408,17 @@ snapshots: dependencies: react: 19.0.0-rc-66855b96-20241106 + react-i18next@16.5.0(i18next@25.7.3(typescript@5.8.2))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.8.2): + dependencies: + '@babel/runtime': 7.28.4 + html-parse-stringify: 3.0.1 + i18next: 25.7.3(typescript@5.8.2) + react: 19.0.0-rc-66855b96-20241106 + use-sync-external-store: 1.6.0(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + typescript: 5.8.2 + react-icons@5.5.0(react@19.0.0-rc-66855b96-20241106): dependencies: react: 19.0.0-rc-66855b96-20241106 @@ -11176,6 +11311,10 @@ snapshots: dependencies: react: 19.0.0-rc-66855b96-20241106 + use-sync-external-store@1.6.0(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + util-deprecate@1.0.2: {} util@0.12.5: @@ -11217,6 +11356,8 @@ snapshots: vm-browserify@1.1.2: {} + void-elements@3.1.0: {} + watchpack@2.4.2: dependencies: glob-to-regexp: 0.4.1 @@ -11393,8 +11534,8 @@ snapshots: zod@3.24.2: {} - zustand@5.0.3(@types/react@18.3.20)(react@19.0.0-rc-66855b96-20241106)(use-sync-external-store@1.5.0(react@19.0.0-rc-66855b96-20241106)): + zustand@5.0.3(@types/react@18.3.20)(react@19.0.0-rc-66855b96-20241106)(use-sync-external-store@1.6.0(react@19.0.0-rc-66855b96-20241106)): optionalDependencies: '@types/react': 18.3.20 react: 19.0.0-rc-66855b96-20241106 - use-sync-external-store: 1.5.0(react@19.0.0-rc-66855b96-20241106) + use-sync-external-store: 1.6.0(react@19.0.0-rc-66855b96-20241106) From 2359a8611e7d30571d1e0e28a0d21e33f82cd897 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Tue, 30 Dec 2025 03:44:46 +0900 Subject: [PATCH 03/57] =?UTF-8?q?[feat]=20i18next=E3=82=92=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E3=81=97=E3=81=9F=E5=A4=9A=E8=A8=80=E8=AA=9E=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C=E3=81=AE=E3=81=9F=E3=82=81=E3=80=81getStaticProps?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81=E5=90=84=E3=83=9A?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=81=A7=E3=81=AE=E3=83=AD=E3=83=BC=E3=82=AB?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=82=BA=E3=82=92=E5=AE=9F=E8=A3=85=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/pages/home/index.tsx | 8 ++++++++ user/src/pages/index.tsx | 8 ++++++++ 2 files changed, 16 insertions(+) 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'])), + }, +}); From aa0db411570e050441720289dada905c74141425 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Tue, 30 Dec 2025 03:44:56 +0900 Subject: [PATCH 04/57] =?UTF-8?q?[feat]=20i18next=E3=82=92=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E3=81=97=E3=81=9F=E5=A4=9A=E8=A8=80=E8=AA=9E=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C=E3=81=AE=E3=81=9F=E3=82=81=E3=80=81App=E3=82=B3?= =?UTF-8?q?=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=82=92?= =?UTF-8?q?=E3=83=A9=E3=83=83=E3=83=97=E3=81=97=E3=80=81=E7=BF=BB=E8=A8=B3?= =?UTF-8?q?=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/pages/_app.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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); From 168c70ceceaa19b732ee27cc60300705126f88c1 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Tue, 30 Dec 2025 03:45:14 +0900 Subject: [PATCH 05/57] =?UTF-8?q?[feat]=20Footer=E3=82=B3=E3=83=B3?= =?UTF-8?q?=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=ABi18next?= =?UTF-8?q?=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=97=E3=81=9F=E7=BF=BB=E8=A8=B3?= =?UTF-8?q?=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81?= =?UTF-8?q?=E8=91=97=E4=BD=9C=E6=A8=A9=E8=A1=A8=E7=A4=BA=E3=82=92=E3=83=AD?= =?UTF-8?q?=E3=83=BC=E3=82=AB=E3=83=A9=E3=82=A4=E3=82=BA=E3=80=82Header?= =?UTF-8?q?=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=81=AB=E8=A8=80=E8=AA=9E=E5=88=87=E3=82=8A=E6=9B=BF=E3=81=88?= =?UTF-8?q?=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/components/Footer/Footer.tsx | 4 +++- user/src/components/Header/Header.tsx | 20 ++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/user/src/components/Footer/Footer.tsx b/user/src/components/Footer/Footer.tsx index 9e0585a56..813ce1daa 100644 --- a/user/src/components/Footer/Footer.tsx +++ b/user/src/components/Footer/Footer.tsx @@ -1,11 +1,13 @@ import { FC } from 'react'; +import { useTranslation } from 'next-i18next'; const Footer: FC = () => { const currentYear = new Date().getFullYear(); + const { t } = useTranslation('common'); return ( ); }; diff --git a/user/src/components/Header/Header.tsx b/user/src/components/Header/Header.tsx index ddf4135d3..77ae7113c 100755 --- a/user/src/components/Header/Header.tsx +++ b/user/src/components/Header/Header.tsx @@ -1,6 +1,7 @@ import { FC, useState } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import LanguageSwitcher from '@/components/LanguageSwitcher'; import CorporateIcon from '../../../public/corporate_logo.svg'; import ProfileIcon from '../../../public/profile_icon.svg'; import UserModal from '../UserModal'; @@ -20,14 +21,17 @@ const Header: FC = () => { - {showUserModal && ( - <> - - setIsOpen(false)} /> - - )} +
+ + {showUserModal && ( + <> + + setIsOpen(false)} /> + + )} +
); }; From 23a0632d0ae59a6f8c48746d3e14a5613074f6d6 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Tue, 30 Dec 2025 03:45:21 +0900 Subject: [PATCH 06/57] =?UTF-8?q?[feat]=20=E8=A8=80=E8=AA=9E=E5=88=87?= =?UTF-8?q?=E3=82=8A=E6=9B=BF=E3=81=88=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC?= =?UTF-8?q?=E3=83=8D=E3=83=B3=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97?= =?UTF-8?q?=E3=80=81=E8=8B=B1=E8=AA=9E=E3=81=A8=E6=97=A5=E6=9C=AC=E8=AA=9E?= =?UTF-8?q?=E3=81=AE=E5=88=87=E3=82=8A=E6=9B=BF=E3=81=88=E6=A9=9F=E8=83=BD?= =?UTF-8?q?=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LanguageSwitcher/LanguageSwitcher.tsx | 40 +++++++++++++++++++ user/src/components/LanguageSwitcher/index.ts | 1 + 2 files changed, 41 insertions(+) create mode 100644 user/src/components/LanguageSwitcher/LanguageSwitcher.tsx create mode 100644 user/src/components/LanguageSwitcher/index.ts diff --git a/user/src/components/LanguageSwitcher/LanguageSwitcher.tsx b/user/src/components/LanguageSwitcher/LanguageSwitcher.tsx new file mode 100644 index 000000000..d4ff372e8 --- /dev/null +++ b/user/src/components/LanguageSwitcher/LanguageSwitcher.tsx @@ -0,0 +1,40 @@ +import { useRouter } from 'next/router'; +import { useTranslation } from 'next-i18next'; + +const locales = ['ja', 'en'] as const; +type Locale = (typeof locales)[number]; + +const LanguageSwitcher = () => { + const router = useRouter(); + const { t } = useTranslation('common'); + const currentLocale = (router.locale as Locale) ?? 'ja'; + + const switchLocale = (locale: Locale) => { + router.push(router.asPath, router.asPath, { locale }); + }; + + return ( +
+ {locales.map((locale) => ( + + ))} +
+ ); +}; + +export default LanguageSwitcher; diff --git a/user/src/components/LanguageSwitcher/index.ts b/user/src/components/LanguageSwitcher/index.ts new file mode 100644 index 000000000..31505c03b --- /dev/null +++ b/user/src/components/LanguageSwitcher/index.ts @@ -0,0 +1 @@ +export { default } from './LanguageSwitcher'; From fabde4f181ff13296110b05f7d78390f8d3f0d28 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Tue, 30 Dec 2025 03:45:35 +0900 Subject: [PATCH 07/57] =?UTF-8?q?[feat]=20=E3=83=AD=E3=82=B0=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=83=A2=E3=83=BC=E3=83=80=E3=83=AB=E3=81=ABi18next?= =?UTF-8?q?=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=97=E3=81=9F=E7=BF=BB=E8=A8=B3?= =?UTF-8?q?=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=82=84=E3=83=9C=E3=82=BF=E3=83=B3=E3=83=A9=E3=83=99?= =?UTF-8?q?=E3=83=AB=E3=82=92=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=82=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/components/LoginModal/LoginModal.tsx | 10 ++++++---- user/src/components/LoginModal/hooks.ts | 8 +++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/user/src/components/LoginModal/LoginModal.tsx b/user/src/components/LoginModal/LoginModal.tsx index f6660ca9f..b6709d2b3 100755 --- a/user/src/components/LoginModal/LoginModal.tsx +++ b/user/src/components/LoginModal/LoginModal.tsx @@ -1,4 +1,5 @@ import { FC } from 'react'; +import { useTranslation } from 'next-i18next'; import TextBox from '@/components/Form/TextBox'; import Button from '../Button'; import Modal from '../Modal'; @@ -12,6 +13,7 @@ type LoginModalProps = { const LoginModal: FC = ({ isOpen, onClose }) => { const { handleSignInSubmit, setValue, errors, email, password, isLoggingIn } = useLoginModalHooks(); + const { t } = useTranslation('common'); return ( @@ -21,16 +23,16 @@ const LoginModal: FC = ({ isOpen, onClose }) => { >
setValue('email', value)} error={errors.email?.message} required - note="例:s123456@stn.nagaokaut.ac.jp" + note={t('loginModal.emailNote')} /> setValue('password', value)} @@ -44,7 +46,7 @@ const LoginModal: FC = ({ isOpen, onClose }) => { type="submit" isDisable={isLoggingIn} > - {isLoggingIn ? 'ログイン中...' : 'ログイン'} + {isLoggingIn ? t('loginModal.submitting') : t('loginModal.submit')}
diff --git a/user/src/components/LoginModal/hooks.ts b/user/src/components/LoginModal/hooks.ts index 48970c43f..c7b09d01e 100644 --- a/user/src/components/LoginModal/hooks.ts +++ b/user/src/components/LoginModal/hooks.ts @@ -2,6 +2,7 @@ import { useState } from 'react'; import { useRouter } from 'next/router'; import { zodResolver } from '@hookform/resolvers/zod'; import { signIn } from 'next-auth/react'; +import { useTranslation } from 'next-i18next'; import { useForm } from 'react-hook-form'; import { toast } from 'react-toastify'; import { LoginModalSchema, loginModalSchema } from './schema'; @@ -9,6 +10,7 @@ import { LoginModalSchema, loginModalSchema } from './schema'; export const useLoginModalHooks = () => { const [isLoggingIn, setIsLoggingIn] = useState(false); const router = useRouter(); + const { t } = useTranslation('common'); // react-hook-formを初期化 const { @@ -43,14 +45,14 @@ export const useLoginModalHooks = () => { if (res?.error) { setError('password', { type: 'login', - message: 'emailかpasswordが違います', + message: t('loginModal.errors.invalidCredentials'), }); - toast.error('ログインに失敗しました'); + toast.error(t('loginModal.toasts.loginFailed')); setIsLoggingIn(false); return; } // ログイン成功 - toast.success('ログインに成功しました'); + toast.success(t('loginModal.toasts.loginSuccess')); router.push('/home'); }) .catch((err) => { From 2ffeea3f2790628e42adf693d79990d93ccf4c93 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Tue, 30 Dec 2025 03:45:44 +0900 Subject: [PATCH 08/57] =?UTF-8?q?[feat]=20NewsList=E3=82=B3=E3=83=B3?= =?UTF-8?q?=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=ABi18next?= =?UTF-8?q?=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=97=E3=81=9F=E7=BF=BB=E8=A8=B3?= =?UTF-8?q?=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81?= =?UTF-8?q?=E3=83=8B=E3=83=A5=E3=83=BC=E3=82=B9=E3=82=BF=E3=82=A4=E3=83=88?= =?UTF-8?q?=E3=83=AB=E3=82=84=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E3=82=92=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=A9=E3=82=A4=E3=82=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/components/NewsList/NewsList.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/user/src/components/NewsList/NewsList.tsx b/user/src/components/NewsList/NewsList.tsx index 0f4a9f04c..c64bb59f5 100755 --- a/user/src/components/NewsList/NewsList.tsx +++ b/user/src/components/NewsList/NewsList.tsx @@ -1,6 +1,7 @@ import { FC } from 'react'; import { useGetNews } from '@/api/newsApi'; import { format } from 'date-fns'; +import { useTranslation } from 'next-i18next'; import FormContainer from '@/components/FormContainer'; type NewsListProps = { @@ -9,6 +10,7 @@ type NewsListProps = { const NewsList: FC = () => { const { news, error, isLoading } = useGetNews(); + const { t } = useTranslation('common'); const sortedNews = (news || []).slice().sort((a, b) => a.id - b.id); @@ -19,7 +21,7 @@ const NewsList: FC = () => { }); const newsList = sortedNews.map((item, index) => { - const date = formattedDates[index] ?? 'お知らせはありません。'; + const date = formattedDates[index] ?? t('news.none'); return (
@@ -35,13 +37,13 @@ const NewsList: FC = () => {
-
お知らせ
+
{t('news.title')}
{isLoading ? ( -
読み込み中...
+
{t('news.loading')}
) : error ? ( -
エラーが発生しました
+
{t('news.error')}
) : ( newsList )} From 370358d02a5744a94ef9ed3ca2057909f4083797 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Tue, 30 Dec 2025 03:45:55 +0900 Subject: [PATCH 09/57] =?UTF-8?q?[feat]=20RegisterCarousel=E3=82=B3?= =?UTF-8?q?=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=AB?= =?UTF-8?q?i18next=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=97=E3=81=9F=E7=BF=BB?= =?UTF-8?q?=E8=A8=B3=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97?= =?UTF-8?q?=E3=80=81=E3=83=95=E3=82=A9=E3=83=BC=E3=83=A0=E3=83=A9=E3=83=99?= =?UTF-8?q?=E3=83=AB=E3=82=84=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=A1=E3=83=83?= =?UTF-8?q?=E3=82=BB=E3=83=BC=E3=82=B8=E3=82=92=E3=83=AD=E3=83=BC=E3=82=AB?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=82=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RegisterCarousel/RegisterCarousel.tsx | 75 ++++++++++++------- .../RegisterCarousel/useRegistration.ts | 62 ++++++++------- 2 files changed, 79 insertions(+), 58 deletions(-) diff --git a/user/src/components/RegisterCarousel/RegisterCarousel.tsx b/user/src/components/RegisterCarousel/RegisterCarousel.tsx index 3e1bcb8d5..57bfa96fb 100755 --- a/user/src/components/RegisterCarousel/RegisterCarousel.tsx +++ b/user/src/components/RegisterCarousel/RegisterCarousel.tsx @@ -1,5 +1,6 @@ import { FC, useRef } from 'react'; import { DepartmentList, GradeList } from '@/utils/list'; +import { useTranslation } from 'next-i18next'; import Button from '@/components/Button'; import Selector from '@/components/Form/Selector'; import TextBox from '@/components/Form/TextBox'; @@ -19,6 +20,8 @@ type FormStepProps = { }; const FormStep: FC = ({ step }) => { + const { t } = useTranslation('common'); + return (
@@ -59,13 +62,19 @@ const FormStep: FC = ({ step }) => {
-
メールアドレス
+
+ {t('registerCarousel.steps.email')} +
-
代表者情報
+
+ {t('registerCarousel.steps.representative')} +
-
確認
+
+ {t('registerCarousel.steps.confirm')} +
@@ -73,6 +82,7 @@ const FormStep: FC = ({ step }) => { }; const Carousel: FC = ({ isOpen, onClose }) => { + const { t } = useTranslation('common'); // カルーセル関連のフック const { stepIndex, @@ -100,10 +110,11 @@ const Carousel: FC = ({ isOpen, onClose }) => { handleSubmit ); + const selectPlaceholder = t('registerCarousel.placeholders.select'); // 選択肢のオプション - const gradeOptions = [{ id: 0, name: '選択してください' }, ...GradeList]; + const gradeOptions = [{ id: 0, name: selectPlaceholder }, ...GradeList]; const departmentOptions = [ - { id: 0, name: '選択してください' }, + { id: 0, name: selectPlaceholder }, ...DepartmentList, ]; @@ -160,30 +171,30 @@ const Carousel: FC = ({ isOpen, onClose }) => {
setValue('mail', value)} onBlur={() => trigger('mail')} /> setValue('password', value)} onBlur={() => trigger('password')} /> @@ -196,34 +207,34 @@ const Carousel: FC = ({ isOpen, onClose }) => {
setValue('name', value)} onBlur={() => trigger('name')} /> setValue('tel', value)} onBlur={() => trigger('tel')} /> setValue('studentId', value)} onBlur={() => trigger('studentId')} /> setValue('gradeId', Number(value)) @@ -233,7 +244,7 @@ const Carousel: FC = ({ isOpen, onClose }) => { error={errors.gradeId?.message} /> setValue('departmentId', Number(value)) @@ -249,7 +260,7 @@ const Carousel: FC = ({ isOpen, onClose }) => {
- メールアドレス + {t('registerCarousel.review.email')}
@@ -261,7 +272,7 @@ const Carousel: FC = ({ isOpen, onClose }) => {
- パスワード + {t('registerCarousel.review.password')}
@@ -272,7 +283,9 @@ const Carousel: FC = ({ isOpen, onClose }) => {
-
名前
+
+ {t('registerCarousel.review.name')} +
@@ -283,7 +296,7 @@ const Carousel: FC = ({ isOpen, onClose }) => {
- 電話番号 + {t('registerCarousel.review.tel')}
@@ -295,7 +308,7 @@ const Carousel: FC = ({ isOpen, onClose }) => {
- 学籍番号 + {t('registerCarousel.review.studentId')}
@@ -306,7 +319,9 @@ const Carousel: FC = ({ isOpen, onClose }) => {
-
学年
+
+ {t('registerCarousel.review.grade')} +
@@ -319,7 +334,9 @@ const Carousel: FC = ({ isOpen, onClose }) => {
-
学科
+
+ {t('registerCarousel.review.department')} +
@@ -357,7 +374,7 @@ const Carousel: FC = ({ isOpen, onClose }) => { icon="lessThan" isDisable={isLoading} > - 修正 + {t('registerCarousel.buttons.previous')} )} {stepIndex === 2 ? ( @@ -368,7 +385,7 @@ const Carousel: FC = ({ isOpen, onClose }) => { onClick={handleRegisterClick} isDisable={isLoading} > - 登録 + {t('registerCarousel.buttons.submit')} ) : ( )}
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); // 最終的にローディング状態を終了 } From 40ffa47417c58fec58cfdde82ae8ac520f3fe477 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Tue, 30 Dec 2025 03:46:03 +0900 Subject: [PATCH 10/57] =?UTF-8?q?[feat]=20WelcomeBox=E3=82=B3=E3=83=B3?= =?UTF-8?q?=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=ABi18next?= =?UTF-8?q?=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=97=E3=81=9F=E7=BF=BB=E8=A8=B3?= =?UTF-8?q?=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81?= =?UTF-8?q?=E3=83=9C=E3=82=BF=E3=83=B3=E3=83=A9=E3=83=99=E3=83=AB=E3=82=84?= =?UTF-8?q?=E8=AA=AC=E6=98=8E=E6=96=87=E3=82=92=E3=83=AD=E3=83=BC=E3=82=AB?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=82=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/components/WelcomeBox/WelcomeBox.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/user/src/components/WelcomeBox/WelcomeBox.tsx b/user/src/components/WelcomeBox/WelcomeBox.tsx index 5775d8a36..e3d869e12 100755 --- a/user/src/components/WelcomeBox/WelcomeBox.tsx +++ b/user/src/components/WelcomeBox/WelcomeBox.tsx @@ -1,4 +1,5 @@ import { FC } from 'react'; +import { useTranslation } from 'next-i18next'; import Button from '../Button'; type WelcomeBoxProps = { @@ -10,6 +11,8 @@ const WelcomeBox: FC = ({ handleLoginClick, handleRegisterClick, }) => { + const { t } = useTranslation('common'); + return (
@@ -19,18 +22,18 @@ const WelcomeBox: FC = ({ color="main" onClick={handleRegisterClick} > - 新規登録 + {t('welcomeBox.register')}

- 初めての方はこちら + {t('welcomeBox.registerDescription')}

- すでにアカウントをお持ちの方はこちら + {t('welcomeBox.loginDescription')}

From b88864556b06127df5fbb838660351a066e14305 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Tue, 30 Dec 2025 03:46:14 +0900 Subject: [PATCH 11/57] =?UTF-8?q?[feat]=20=E6=96=B0=E3=81=97=E3=81=84?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=A9=E3=82=A4=E3=82=BA=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=80=8C.cursorru?= =?UTF-8?q?les=E3=80=8D=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81?= =?UTF-8?q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=A7=E3=81=AE=E5=87=BA=E5=8A=9B?= =?UTF-8?q?=E3=82=92=E6=8C=87=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursorrules | 1 + 1 file changed, 1 insertion(+) create mode 100644 .cursorrules diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 000000000..49282b29d --- /dev/null +++ b/.cursorrules @@ -0,0 +1 @@ +すべて日本語で出力してください From 26fa7582edfdf8369ff8c5974689bd0aeb874a91 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Thu, 1 Jan 2026 12:52:35 +0900 Subject: [PATCH 12/57] =?UTF-8?q?[feat]=20=E8=8B=B1=E8=AA=9E=E3=81=A8?= =?UTF-8?q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AE=E3=83=AD=E3=83=BC=E3=82=AB?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=82=BA=E8=A8=AD=E5=AE=9A=E3=82=92=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E3=81=97=E3=80=81=E4=B8=80=E8=88=AC=E7=9A=84=E3=81=AA?= =?UTF-8?q?=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=80=81=E3=83=95?= =?UTF-8?q?=E3=82=A9=E3=83=BC=E3=83=A0=E3=80=81=E3=82=B9=E3=83=86=E3=83=BC?= =?UTF-8?q?=E3=82=BF=E3=82=B9=E3=80=81=E3=83=AA=E3=82=B9=E3=83=88=E3=80=81?= =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E7=B7=A8=E9=9B=86=E3=83=A2?= =?UTF-8?q?=E3=83=BC=E3=83=80=E3=83=AB=E3=80=81=E7=94=B3=E8=AB=8B=E9=96=A2?= =?UTF-8?q?=E9=80=A3=E3=81=AE=E7=BF=BB=E8=A8=B3=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/public/locales/en/common.json | 792 +++++++++++++++++++++++++++++ user/public/locales/ja/common.json | 792 +++++++++++++++++++++++++++++ 2 files changed, 1584 insertions(+) diff --git a/user/public/locales/en/common.json b/user/public/locales/en/common.json index 72c2d500e..dfd89d033 100644 --- a/user/public/locales/en/common.json +++ b/user/public/locales/en/common.json @@ -4,6 +4,62 @@ "japanese": "Japanese", "english": "English" }, + "general": { + "loading": "Loading...", + "errors": { + "fetch": "Failed to fetch data." + } + }, + "form": { + "required": "Required", + "optional": "Optional", + "actions": { + "register": "Register", + "edit": "Edit", + "cancel": "Cancel", + "delete": "Delete", + "close": "Close", + "submit": "Submit", + "save": "Save", + "add": "Add", + "back": "Back", + "next": "Next", + "previous": "Previous", + "confirm": "Confirm", + "open": "Open", + "upload": "Upload" + }, + "messages": { + "nonEditable": "* Cannot be edited", + "registerSuccess": "Saved successfully.", + "registerFailed": "Failed to save.", + "updateSuccess": "Updated successfully.", + "updateFailed": "Failed to update." + }, + "validation": { + "required": "This field is required", + "duplicateChoice": "You selected the same choice more than once.", + "remarkRequired": "Please enter the location in the remarks.", + "inputError": "Please fix the highlighted errors.", + "select": "Please select an option" + } + }, + "status": { + "reception": { + "open": "Open", + "deadline": "Closing soon", + "closed": "Closed" + }, + "registration": { + "registered": "Registered", + "unregistered": "Not registered" + }, + "progress": { + "notRequired": "Not required", + "completed": "Complete", + "pending": "Incomplete" + } + }, "news": { "title": "News", "none": "No news available.", @@ -94,6 +150,742 @@ "retryLogin": "Please try signing in again." } }, + "lists": { + "grade": { + "select": "Select", + "B1": "B1 (1st year undergraduate)", + "B2": "B2 (2nd year undergraduate)", + "B3": "B3 (3rd year undergraduate)", + "B4": "B4 (4th year undergraduate)", + "M1": "M1 (1st year master's)", + "M2": "M2 (2nd year master's)", + "D1": "D1 (1st year doctoral)", + "D2": "D2 (2nd year doctoral)", + "D3": "D3 (3rd year doctoral)", + "GD1": "GD1 (1st year innovation program)", + "GD2": "GD2 (2nd year innovation program)", + "GD3": "GD3 (3rd year innovation program)", + "GD4": "GD4 (4th year innovation program)", + "GD5": "GD5 (5th year innovation program)", + "faculty": "Faculty / Staff", + "other": "Other" + }, + "department": { + "select": "Select", + "1": "Mechanical Engineering (Undergraduate Program)", + "2": "Electrical, Electronics and Information Engineering (Undergraduate Program)", + "3": "Materials Science and Biotechnology (Undergraduate Programs)", + "4": "Civil and Environmental Engineering (Undergraduate Program)", + "5": "Information and Management Systems Engineering (Undergraduate Program)", + "6": "Mechanical Engineering (Graduate Program)", + "7": "Electrical, Electronics and Information Engineering (Graduate Program)", + "8": "Materials Science and Biotechnology (Graduate Programs)", + "9": "Civil and Environmental Engineering (Graduate Program)", + "10": "Information and Management Systems Engineering (Graduate Program)", + "11": "Integrated Quantum and Nuclear Engineering", + "12": "System Safety Engineering", + "13": "GIGAKU Innovation Group", + "14": "Information Science and Control Engineering", + "15": "Materials Science", + "16": "Energy and Environmental Engineering", + "17": "Integrated Bioscience and Technology", + "18": "Other" + } + }, + "userEditModal": { + "labels": { + "name": "Name", + "email": "Email address", + "tel": "Phone number", + "studentId": "Student ID", + "grade": "Grade", + "department": "Department" + }, + "notes": { + "name": "e.g., Taro Nagaoka", + "email": "e.g., s123456@stn.nagaokaut.ac.jp", + "tel": "e.g., 09012345678", + "studentId": "e.g., 12345678" + }, + "messages": { + "emailChangeConfirm": "If you change your email address, you must sign in again with the new address. Your password will remain the same." + }, + "toasts": { + "cancelled": "Update was cancelled.", + "updateSuccess": "User information updated.", + "emailChanged": "Email updated. Please sign in again." + }, + "errors": { + "duplicateEmail": "This email address is already in use.", + "updateFailed": "Failed to update." + }, + "validation": { + "name": "Name is required.", + "studentId": "Enter an 8-digit student ID.", + "tel": "Enter a valid phone number (e.g., 09012345678).", + "telMin": "Phone number is too short.", + "telMax": "Phone number is too long.", + "email": "Enter a valid email address.", + "department": "Select a department.", + "grade": "Select a grade." + } + }, + "applications": { + "group": { + "title": "Group Registration", + "loading": "Loading...", + "errors": { + "fetch": "Failed to fetch data." + }, + "fields": { + "name": "Group name", + "projectName": "Project name", + "isInternational": "Is this an international group?", + "isExternal": "Is this an off-campus group?", + "groupCategory": "Participation type", + "activity": "Activity details" + }, + "notes": { + "name": "e.g., NUT Festival Executive Committee", + "projectName": "e.g., Gidai Rangers", + "international": "Please confirm your registration status.", + "external": "Please confirm whether outside organizations are allowed.", + "groupCategory": "Select the format that best fits your activity.", + "activity": "e.g., Food sales, stage performance" + }, + "boolean": { + "yes": "Yes", + "no": "No" + }, + "options": { + "international": { + "no": "No, we are not an international (international-student) group.", + "yes": "Yes, we are an international (international-student) group." + }, + "external": { + "no": "No, we are an on-campus group.", + "yes": "Yes, we are an off-campus group." + } + } + }, + "venue": { + "title": "Venue Application", + "loading": "Loading...", + "fields": { + "firstChoice": "First choice", + "secondChoice": "Second choice", + "thirdChoice": "Third choice", + "remark": "Remarks" + } + }, + "rentItems": { + "title": "Equipment rental application", + "loading": "Loading data...", + "errors": { + "fetchTitle": "Error:", + "fetchDescription": "Failed to load data. Please reload the page." + }, + "radio": { + "question": "Will you submit a rental request?", + "options": { + "yes": "Yes", + "no": "No" + } + }, + "summary": { + "noApplication": { + "label": "Rental request not required (saved)", + "description": "We will not borrow any equipment from the school." + }, + "count": "{{value}} items" + }, + "location": { + "displayLabel": "First choice:", + "radioQuestion": "Which venue type is your first choice?", + "options": { + "indoor": "Indoor", + "outdoor": "Outdoor" + }, + "notes": { + "preApplication": "Please submit your venue application first.", + "foodOnlyOutdoor": "Food sales groups can only operate outdoors." + } + }, + "fields": { + "section": "Item {{index}}", + "item": "Item name", + "count": "Quantity" + }, + "notes": { + "minRequest": "Request only the minimum quantity you need.", + "contactLimit": "If you need 20 or more units, please email us.", + "contactEmail": "nutfes.soumu@gmail.com" + }, + "buttons": { + "addItem": "Add item" + }, + "messages": { + "registerNoItemsFailed": "Failed to save the response that no rental is needed.", + "deleteExistingError": "An error occurred while deleting existing rental data.", + "deleteExistingFailed": "Failed to delete the existing rental request.", + "registerNoItemsSuccess": "Saved that no rental request is needed.", + "unexpectedError": "An unexpected error occurred.", + "unexpectedErrorWithDetail": "An unexpected error occurred: {{message}}", + "unexpectedRetry": "An unexpected error occurred. Please try again.", + "submitError": "An error occurred while submitting. Please try again.", + "submitFailed": "Failed to submit the rental request.", + "updateSuccess": "Rental request updated.", + "createSuccess": "Rental request submitted.", + "editStartFailed": "Failed to start editing." + }, + "validation": { + "selectItem": "Please select an item.", + "minCount": "Enter at least one.", + "selectLocation": "Please select a venue type.", + "addOneItem": "Add at least one item.", + "fillAllFields": "Complete all item fields.", + "noDuplicates": "You cannot add the same item more than once.", + "tentLimit": "You can request at most one tent.", + "partitionDisplayExclusive": "You can request either partitions or display boards, not both.", + "longTableLimit": "You can request at most one long table.", + "tableOutdoorLimit": "Outdoor groups can request up to 20 tables.", + "chairOutdoorLimit": "Outdoor groups can request up to 20 chairs." + } + }, + "purchaseLists": { + "title": "Purchase application", + "loading": "Loading...", + "errors": { + "fetch": "Failed to fetch data." + }, + "deadline": { + "title": "Submission deadline has passed", + "description": "The purchase application deadline is over, so you can no longer submit a new request." + }, + "summary": { + "labels": { + "foodProduct": "Product name", + "items": "Ingredients / materials", + "type": "Product type", + "shop": "Shop", + "date": "Purchase date", + "remark": "Remarks", + "url": "URL" + } + }, + "radio": { + "label": "Product type", + "options": { + "fresh": "Fresh food", + "processed": "Processed food" + } + }, + "fields": { + "section": "Item {{index}}", + "foodProduct": "Product name", + "items": "Ingredients/materials used for the selected dish", + "type": "Product type", + "shop": "Shop", + "purchaseDate": "Purchase date", + "url": "URL", + "remark": "Remarks" + }, + "notes": { + "foodProduct": "Selectable after you register your food product application.", + "shop": "If you choose online order, a URL is required.", + "purchaseDate": "Example: 2025/03/14", + "url": "Enter the URL of the e-commerce site you used.", + "remarkOther": "Enter the shop name, address, phone number, and opening hours.", + "remarkDefault": "Add any other details if needed." + }, + "buttons": { + "addItem": "Add purchase item" + }, + "messages": { + "itemDeleteSuccess": "Deleted the purchase item.", + "itemDeleteFailed": "Failed to delete the item.", + "bulkCreateSuccess": "Submitted multiple purchase applications.", + "updateSuccess": "Purchase application updated.", + "createSuccess": "Purchase application submitted.", + "submitFailed": "Failed to submit." + }, + "validation": { + "foodProduct": "Select a product.", + "shop": "Select a shop.", + "items": "Enter the ingredients or materials.", + "purchaseDate": "Enter the purchase date.", + "invalidDate": "Enter a valid date.", + "invalidUrl": "Enter a valid URL.", + "urlRequired": "Enter a URL for online orders.", + "remarkRequired": "For “Other” shops, enter the name, address, phone number, and opening hours.", + "minItems": "Register at least one purchase item." + } + }, + "foodProduct": { + "title": "Food product application", + "loading": "Loading...", + "errors": { + "fetch": "Failed to fetch data." + }, + "deadline": { + "title": "Submission deadline has passed", + "description": "You can no longer submit new products because the deadline has passed." + }, + "view": { + "summaryLabel": "Product list", + "registered": "{{count}} items registered", + "none": "Not registered", + "empty": "No products have been registered.", + "addButton": "Add product" + }, + "summary": { + "labels": { + "name": "Product name", + "alcohol": "Is it alcoholic?", + "cooking": "Cooking required", + "day1": "Day 1 quantity", + "day2": "Day 2 quantity" + } + }, + "radio": { + "alcohol": { + "label": "Is this an alcoholic beverage?", + "note": "Selecting “Yes” automatically sets cooking to “required.”", + "options": { + "yes": "Yes", + "no": "No" + } + }, + "cooking": { + "label": "Cooking required", + "options": { + "yes": "Yes (e.g., alcohol, heated dishes)", + "no": "No (e.g., soft drinks)" + } + } + }, + "fields": { + "name": "Product name", + "day1": "Planned quantity (Day 1)", + "day2": "Planned quantity (Day 2)" + }, + "notes": { + "processing": "Processing...", + "quantity": "Use half-width numbers." + }, + "buttons": { + "add": "Add product" + }, + "messages": { + "updateSuccess": "Updated the product list.", + "updateFailed": "Failed to update the product list.", + "updateFailedDetail": "Failed to update the product list: {{message}}", + "createSuccess": "Submitted the product list.", + "createFailed": "Failed to submit the product list.", + "createFailedDetail": "Failed to submit the product list: {{message}}", + "deleteSuccess": "Deleted “{{name}}”.", + "deleteFailed": "Failed to delete the product.", + "deleteFailedDetail": "Failed to delete the product: {{message}}", + "deleteNotFound": "Could not find the product to delete.", + "authRequired": "Authentication is required. Please sign in." + }, + "validation": { + "name": "Enter a product name.", + "isAlcohol": "Select whether it is alcoholic.", + "isCooking": "Select whether cooking is required.", + "day1": "Enter the planned quantity for Day 1.", + "day2": "Enter the planned quantity for Day 2.", + "number": "Use half-width numbers.", + "minValue": "Enter a value of 1 or more.", + "alcoholRequiresCooking": "Alcohol must be marked as requiring cooking.", + "minProducts": "Register at least one product." + } + }, + "power": { + "title": "Power application", + "radio": { + "question": "Will you submit a power application?", + "options": { + "yes": "Yes", + "no": "No" + } + }, + "summary": { + "noApplication": { + "label": "Power application not required (saved)", + "description": "We will not use any devices that require power." + }, + "fields": { + "productName": "Product name", + "manufacturer": "Manufacturer", + "model": "Model number", + "url": "Product URL", + "maxPower": "Power consumption" + }, + "powerValue": "{{value}} W" + }, + "form": { + "fields": { + "productName": "Device name", + "manufacturer": "Device manufacturer", + "model": "Model number", + "url": "Product URL", + "maxPower": "Power (W)" + }, + "notes": { + "url": "Enter the URL of the product introduction page.", + "totalPower": "Keep the total consumption of all devices within {{limit}} W.", + "emailWarning": "If you need more than {{limit}} W, contact us at the email below.", + "contactEmail": "nutfes.soumu@gmail.com" + }, + "addDevice": "Add device", + "totalPowerWarning": "Total power exceeds {{limit}} W (current: {{value}} W)." + }, + "errors": { + "submitTitle": "Error:" + }, + "messages": { + "partialDeleteWarning": "Some devices could not be deleted, but the process will continue.", + "registerNegativeSuccess": "Saved your response that no power application is needed.", + "registerNegativeFailed": "Failed to save your response. Please try again.", + "processError": "Failed to process the application. Please try again.", + "missingGroup": "Could not retrieve the group ID.", + "unregisteredDeleteWarning": "There was an issue removing the previous response, but the process will continue.", + "updateSuccess": "Power application updated.", + "createSuccess": "Power application submitted.", + "submitFailed": "Failed to submit the application. Please try again.", + "submitUnexpectedError": "An error occurred while submitting the application.", + "deviceDeleteSuccess": "Deleted the device.", + "deviceDeleteFailed": "Failed to delete the device. Please try again.", + "deviceDeleteError": "An error occurred while deleting the device." + }, + "validation": { + "productNameRequired": "Enter a product name.", + "manufacturerRequired": "Enter the manufacturer.", + "modelRequired": "Enter the model number.", + "invalidUrl": "Enter a valid URL.", + "invalidNumber": "Enter a number.", + "minPower": "Enter at least 1 W.", + "maxPower": "Enter 1500 W or less.", + "minDevices": "Register at least one device.", + "totalPowerLimit": "Keep the total power consumption within 1500 W." + } + }, + "stage": { + "title": "Stage application", + "loading": "Loading stage data...", + "fields": { + "date": "Event date", + "sunnyFirst": "Sunny: first choice", + "sunnySecond": "Sunny: second choice", + "rainyFirst": "Rainy: first choice", + "rainySecond": "Rainy: second choice", + "prepTime": "Preparation time", + "performTime": "Performance time", + "cleanupTime": "Cleanup time" + }, + "notes": { + "select": "Please select", + "unit": " (Unit: min)", + "prepTime": "Enter the minutes required to prepare on stage.", + "performTime": "The total of preparation, performance, and cleanup must be within 120 minutes.", + "cleanupTime": "Enter the minutes required to clean up on stage." + }, + "minutes": "{{value}} min", + "messages": { + "missingGroup": "Group ID was not found.", + "submitError": "An error occurred while submitting. Please try again.", + "unexpectedError": "An unexpected error occurred. Please try again.", + "updateSuccess": "Stage preferences updated.", + "createSuccess": "Stage preferences submitted." + }, + "errors": { + "fetchTitle": "Error:", + "fetchDescription": "Failed to fetch data. Please reload the page.", + "submitTitle": "Submission error:" + }, + "validation": { + "sunnyFirst": "Select your first sunny-day preference.", + "sunnySecond": "Select your second sunny-day preference.", + "rainyFirst": "Select your first rainy-day preference.", + "rainySecond": "Select your second rainy-day preference.", + "prepTimeRequired": "Enter a preparation time.", + "performTimeRequired": "Enter a performance time.", + "cleanupTimeRequired": "Enter a cleanup time.", + "prepTimeInvalid": "Enter a valid preparation time.", + "performTimeInvalid": "Enter a valid performance time.", + "cleanupTimeInvalid": "Enter a valid cleanup time.", + "totalTime": "The total of preparation, performance, and cleanup must be 120 minutes or less.", + "sunnyChoiceDuplicate": "Choose a different stage than your first sunny-day choice.", + "rainyChoiceDuplicate": "Choose a different stage than your first rainy-day choice." + } + }, + "stageOptions": { + "title": "Stage option application", + "fields": { + "ownEquipment": "Will you bring electrical equipment?", + "bgm": "Will you bring equipment that connects to the speakers?", + "cameraPermission": "Do you allow the committee to film your performance?", + "loudSound": "Will you produce loud sounds?" + }, + "notes": { + "select": "Please select" + }, + "options": { + "yes": "Yes", + "no": "No" + }, + "messages": { + "submitSuccess": "Submitted successfully.", + "submitFailed": "Submission failed. Please try again later." + } + }, + "publicRelations": { + "title": "Public relations submission", + "fields": { + "text": "PR text (used on the website, pamphlet, and announcements)", + "announce": "Would you like to make an announcement?", + "image": "PR image" + }, + "notes": { + "text": "Japanese: 0–50 characters / English: 0–25 words", + "upload": [ + "File types: png, jpeg", + "File size: under 10 MB", + "Image shape: square (ideally a photo of your food/product)" + ], + "existingImage": "If you don’t upload a new image, the current one will be kept." + }, + "uploadStatus": "Uploaded: {{fileName}}", + "options": { + "announce": { + "yes": "Yes", + "no": "No" + } + }, + "validation": { + "imageRequired": "Please upload an image.", + "imageSquare": "Please upload a square image.", + "imageLoadFailed": "Failed to load the selected image.", + "sizeLimit": "Image files must be under 10 MB.", + "format": "Only png or jpeg files are supported.", + "jpLimit": "Japanese text must be 50 characters or less.", + "enLimit": "English text must be 25 words or fewer." + }, + "messages": { + "imgurMissing": "Imgur Client ID is not configured. Check your environment variables.", + "imgurUploadFailed": "Failed to upload the image.", + "submitSuccess": "Submitted successfully.", + "submitFailed": "Submission failed. Please try again later." + }, + "state": { + "notSet": "Not set", + "missingText": "(No PR text submitted)" + } + }, + "viceRepresentative": { + "title": "Vice Representative Application", + "note": "If you are participating alone, you do not need to submit this application.", + "summary": { + "individual": { + "label": "No vice representative required (saved)", + "description": "You are participating alone." + } + }, + "fields": { + "isIndividual": "Are you participating alone?", + "name": "Name", + "studentId": "Student ID", + "gradeId": "Grade", + "departmentId": "Department", + "email": "Email address", + "tel": "Phone number" + }, + "notes": { + "name": "e.g., Taro Nagaoka", + "studentId": "8-digit half-width numbers only (e.g., 12345678)", + "email": "e.g., 123456@stn.nagaokaut.ac.jp", + "tel": "e.g., 09012345678 (no hyphen)" + }, + "radio": { + "options": { + "individual": "Yes (participating alone)", + "group": "No (participating as a group)" + } + }, + "messages": { + "submitSuccess": "Submitted successfully.", + "submitFailed": "Failed to submit." + }, + "validation": { + "name": "Enter the vice representative's name.", + "studentIdInteger": "Enter numbers only.", + "studentIdLength": "Student ID must be 8 digits.", + "gradeId": "Select a grade.", + "departmentId": "Select a department.", + "email": "Enter an email address.", + "emailFormat": "Enter a valid email address.", + "tel": "Enter a 10 or 11 digit phone number starting with 0." + } + }, + "employees": { + "title": "Employee Application", + "deadline": { + "title": "The submission deadline has passed", + "description": "The deadline for the employee application has passed, so you can no longer submit a new application." + }, + "summary": { + "noApplication": { + "label": "No employee application required (saved)", + "description": "Only the representative and vice representative will participate." + }, + "headers": { + "name": "Employee name", + "studentId": "Student ID" + } + }, + "radio": { + "label": "Will you register members other than the representative and vice representative?", + "options": { + "yes": "Yes", + "no": "No" + } + }, + "form": { + "labels": { + "name": "Employee name", + "studentId": "Student ID" + }, + "notes": { + "name": "e.g., Hanako NUT", + "studentId": "e.g., 12345678" + } + }, + "buttons": { + "addEmployee": "Add employee" + }, + "messages": { + "applicationSuccess": "Employee information saved.", + "applicationFailed": "Failed to save the employee information.", + "noApplicationSuccess": "Saved that no employee registration is required.", + "noApplicationFailed": "Failed to save the response that no employee registration is required.", + "deleteSuccess": "Employee deleted.", + "deleteFailed": "Failed to delete the employee.", + "registerUnregisteredFailed": "Failed to save.", + "deleteUnregisteredFailed": "Failed to delete." + }, + "validation": { + "name": "Employee name is required.", + "studentId": "Enter an 8-digit student ID." + } + }, + "cookingProcessOrder": { + "title": "Cooking Process Application", + "warning": "Please submit your food product application first.", + "summary": { + "labels": { + "foodProduct": "Product name", + "preOpen": "Kitchen use (before opening)", + "duringOpen": "Kitchen use (during business)", + "description": "Cooking details" + }, + "status": { + "use": "Will use", + "notUse": "Will not use", + "notRegistered": "Not submitted" + } + }, + "fields": { + "kitchenUsage": "Kitchen usage", + "preOpen": "(Before opening)", + "duringOpen": "(During business)", + "tent": "Cooking details", + "confirm": "Confirmation items" + }, + "placeholders": { + "tent": "Example:\n1. Measure 15 g of coffee beans\n2. Place in filter\n3. Heat\n4. Plate and serve" + }, + "notes": { + "confirm": "Check every confirmation item." + }, + "options": { + "kitchenUsage": { + "use": "Will use", + "notUse": "Will not use" + } + }, + "checkbox": { + "options": [ + "I described the hygiene control process in as much detail as possible.", + "I confirmed whether the final product is heated before serving.", + "I also submitted the cooking process for alcoholic beverages." + ] + }, + "buttons": { + "edit": "Edit" + }, + "messages": { + "updateSuccess": "Cooking processes updated.", + "updateFailed": "Failed to update the cooking processes." + }, + "validation": { + "tentRequired": "Enter the cooking details.", + "confirmAll": "Check all confirmation items." + } + }, + "venueMap": { + "title": "Venue Layout", + "fields": { + "picture": "Venue layout image", + "checklist": "Layout checklist" + }, + "summary": { + "notSet": "Not set" + }, + "notes": { + "required": "*Required", + "existing": "If you do not upload a new image, the current image will be used.", + "currentImage": "Current image: {{name}}", + "unknownFile": "Unknown file name" + }, + "upload": { + "note": [ + "Show the placement of tables, chairs, and equipment.", + "File types: png, jpeg", + "File size: 20 MB max" + ], + "uploaded": "Uploaded: {{fileName}}" + }, + "checklist": { + "note": "Check every item.", + "options": { + "trashPosition": "I included the location of the trash bins.", + "foodStorage": "I included where ingredients will be stored.", + "allItemsListed": "I added every requested item to the layout.", + "fireHazardousMaterials": "I specified where fire or electrical appliances will be used.", + "partitionPlacement": "I confirmed that partitions/boards stay outside the kitchen area and are placed on the side of the tent." + } + }, + "messages": { + "imgurMissing": "Imgur Client ID is not configured. Check your environment variables.", + "imgurUploadFailed": "Failed to upload the image.", + "submitSuccess": "Submitted successfully.", + "submitFailed": "Submission failed. Please try again later." + }, + "buttons": { + "submitting": "Submitting..." + }, + "validation": { + "imageRequired": "Upload the venue layout image.", + "fileSize": "File size must be under 20 MB.", + "fileType": "Only png or jpeg files are supported.", + "checklist": "Confirm every item." + } + } + }, + "auth": { + "logout": "Log out" + }, "footer": { "copyright": "Copyright © {{year}} NUTMEG. All Rights Reserved." } diff --git a/user/public/locales/ja/common.json b/user/public/locales/ja/common.json index d10e1afcc..f6ec27b2e 100644 --- a/user/public/locales/ja/common.json +++ b/user/public/locales/ja/common.json @@ -4,6 +4,62 @@ "japanese": "日本語", "english": "English" }, + "general": { + "loading": "読み込み中...", + "errors": { + "fetch": "データの取得に失敗しました。" + } + }, + "form": { + "required": "必須", + "optional": "任意", + "actions": { + "register": "登録", + "edit": "修正", + "cancel": "キャンセル", + "delete": "削除", + "close": "閉じる", + "submit": "送信", + "save": "保存", + "add": "追加", + "back": "戻る", + "next": "次へ", + "previous": "前へ", + "confirm": "確認", + "open": "開く", + "upload": "アップロード" + }, + "messages": { + "nonEditable": "※変更できません", + "registerSuccess": "登録しました。", + "registerFailed": "登録に失敗しました。", + "updateSuccess": "更新しました。", + "updateFailed": "更新に失敗しました。" + }, + "validation": { + "required": "入力してください", + "duplicateChoice": "希望が重複しています", + "remarkRequired": "備考に場所を入力してください", + "inputError": "入力エラーがあります。", + "select": "選択してください" + } + }, + "status": { + "reception": { + "open": "受付中", + "deadline": "締切間近", + "closed": "受付終了" + }, + "registration": { + "registered": "登録済", + "unregistered": "未登録" + }, + "progress": { + "notRequired": "不要", + "completed": "完了", + "pending": "未完了" + } + }, "news": { "title": "お知らせ", "none": "お知らせはありません。", @@ -94,6 +150,742 @@ "retryLogin": "再度ログインしてください。" } }, + "lists": { + "grade": { + "select": "選択してください", + "B1": "B1(学部1年)", + "B2": "B2(学部2年)", + "B3": "B3(学部3年)", + "B4": "B4(学部4年)", + "M1": "M1(修士1年)", + "M2": "M2(修士2年)", + "D1": "D1(博士1年)", + "D2": "D2(博士2年)", + "D3": "D3(博士3年)", + "GD1": "GD1(イノベ1年)", + "GD2": "GD2(イノベ2年)", + "GD3": "GD3(イノベ3年)", + "GD4": "GD4(イノベ4年)", + "GD5": "GD5(イノベ5年)", + "faculty": "教員", + "other": "その他" + }, + "department": { + "select": "選択してください", + "1": "機械工学分野 / 機械創造工学課程", + "2": "電気電子情報工学分野 / 電気電子情報工学課程", + "3": "物質生物工学分野 / 物質材料工学課程 / 生物機能工学課程", + "4": "環境社会基盤工学分野 / 環境社会基盤工学課程", + "5": "情報・経営システム工学分野 / 情報・経営システム工学課程", + "6": "機械工学分野 / 機械創造工学専攻", + "7": "電気電子情報工学分野 / 電気電子情報工学専攻", + "8": "物質生物工学分野 / 物質材料工学専攻 / 生物機能工学専攻", + "9": "環境社会基盤工学分野 / 環境社会基盤工学専攻", + "10": "情報・経営システム工学分野 / 情報・経営システム工学専攻", + "11": "量子・原子力統合工学分野 / 原子力システム安全工学専攻", + "12": "システム安全工学専攻", + "13": "技術科学イノベーション専攻", + "14": "情報・制御工学分野 / 情報・制御工学専攻", + "15": "材料工学分野 / 材料工学専攻", + "16": "エネルギー工学分野 / エネルギー・環境工学専攻", + "17": "社会環境・生物機能工学分野 / 生物統合工学専攻", + "18": "その他" + } + }, + "userEditModal": { + "labels": { + "name": "名前", + "email": "メールアドレス", + "tel": "電話番号", + "studentId": "学籍番号", + "grade": "学年", + "department": "学科" + }, + "notes": { + "name": "例:長岡 太郎", + "email": "例:s123456@stn.nagaokaut.ac.jp", + "tel": "例:09012345678", + "studentId": "例:12345678" + }, + "messages": { + "emailChangeConfirm": "メールアドレスを変更する場合は、変更後のメールアドレスで再度ログインする必要があります。パスワードは以前のものと同じです。" + }, + "toasts": { + "cancelled": "変更はキャンセルされました。", + "updateSuccess": "ユーザー情報を登録しました。", + "emailChanged": "メールアドレスを変更しました。再度ログインしてください。" + }, + "errors": { + "duplicateEmail": "このメールアドレスはすでに使われています。", + "updateFailed": "更新に失敗しました。" + }, + "validation": { + "name": "名前は必須です", + "studentId": "8桁の学籍番号を入力してください", + "tel": "有効な電話番号を入力してください(例: 09012345678)", + "telMin": "電話番号が短すぎます", + "telMax": "電話番号が長すぎます", + "email": "有効なメールアドレスを入力してください", + "department": "学科を選択してください", + "grade": "学年を選択してください" + } + }, + "applications": { + "group": { + "title": "団体申請", + "loading": "読み込み中...", + "errors": { + "fetch": "データの取得に失敗しました。" + }, + "fields": { + "name": "団体名", + "projectName": "企画名", + "isInternational": "国際団体ですか?", + "isExternal": "学外団体ですか?", + "groupCategory": "参加形式", + "activity": "企画内容" + }, + "notes": { + "name": "例:技大祭実行委員会", + "projectName": "例:ギダイジャー", + "international": "注意書き", + "external": "注意書き", + "groupCategory": "注意書き", + "activity": "〇〇の販売、〇〇のパフォーマンスなど" + }, + "boolean": { + "yes": "はい", + "no": "いいえ" + }, + "options": { + "international": { + "no": "いいえ、国際団体(留学生団体)ではありません。", + "yes": "はい、国際団体(留学生団体)です。" + }, + "external": { + "no": "いいえ、学内の団体です。", + "yes": "はい、学外の団体です。" + } + } + }, + "venue": { + "title": "会場申請", + "loading": "読み込み中...", + "fields": { + "firstChoice": "第一希望", + "secondChoice": "第二希望", + "thirdChoice": "第三希望", + "remark": "備考" + } + }, + "rentItems": { + "title": "物品申請", + "loading": "データを読み込み中です...", + "errors": { + "fetchTitle": "エラー:", + "fetchDescription": "データの取得に失敗しました。ページを再読込してください。" + }, + "radio": { + "question": "物品申請を行いますか?", + "options": { + "yes": "はい", + "no": "いいえ" + } + }, + "summary": { + "noApplication": { + "label": "物品申請は不要(登録済み)", + "description": "学校から借用する備品はありません。" + }, + "count": "{{value}} 個" + }, + "location": { + "displayLabel": "第一希望:", + "radioQuestion": "会場申請の第一希望はどちらですか?", + "options": { + "indoor": "屋内", + "outdoor": "屋外" + }, + "notes": { + "preApplication": "会場申請を先に申請してください。", + "foodOnlyOutdoor": "※食品販売団体は屋外での出店のみとなります" + } + }, + "fields": { + "section": "物品 {{index}}", + "item": "物品名", + "count": "個数" + }, + "notes": { + "minRequest": "※必要最低限の数だけ申請してください", + "contactLimit": "使用する個数が20個以上の場合はメールをお送りください", + "contactEmail": "nutfes.soumu@gmail.com" + }, + "buttons": { + "addItem": "物品の追加" + }, + "messages": { + "registerNoItemsFailed": "物品申請の登録に失敗しました", + "deleteExistingError": "既存の申請データ削除中にエラーが発生しました", + "deleteExistingFailed": "既存の物品申請の削除に失敗しました", + "registerNoItemsSuccess": "物品申請を行わない設定を登録しました", + "unexpectedError": "予期せぬエラーが発生しました", + "unexpectedErrorWithDetail": "予期せぬエラーが発生しました: {{message}}", + "unexpectedRetry": "予期せぬエラーが発生しました。もう一度お試しください。", + "submitError": "送信中にエラーが発生しました。もう一度お試しください。", + "submitFailed": "物品申請の送信に失敗しました", + "updateSuccess": "物品申請を更新しました", + "createSuccess": "物品申請を登録しました", + "editStartFailed": "編集モードの開始に失敗しました" + }, + "validation": { + "selectItem": "物品を選択してください", + "minCount": "1つ以上選択してください", + "selectLocation": "会場タイプを選択してください", + "addOneItem": "少なくとも1つの物品を追加してください", + "fillAllFields": "すべての物品情報を正しく入力してください", + "noDuplicates": "同じ物品を複数回追加することはできません", + "tentLimit": "テントは1個までしか申請できません", + "partitionDisplayExclusive": "パーテーションと掲示板はどちらか一方のみ申請できます", + "longTableLimit": "長机は1個までしか申請できません", + "tableOutdoorLimit": "屋外団体は机を20個までしか申請できません", + "chairOutdoorLimit": "屋外団体は椅子を20個までしか申請できません" + } + }, + "purchaseLists": { + "title": "購入品申請", + "loading": "読み込み中...", + "errors": { + "fetch": "データの取得に失敗しました。" + }, + "deadline": { + "title": "申請期限が過ぎています", + "description": "購入品申請の締切期限が過ぎているため、新規申請はできません。" + }, + "summary": { + "labels": { + "foodProduct": "販売品名", + "items": "食材・材料", + "type": "商品の種類", + "shop": "購入場所", + "date": "購入日", + "remark": "備考", + "url": "URL" + } + }, + "radio": { + "label": "商品の種類", + "options": { + "fresh": "生鮮品", + "processed": "加工品" + } + }, + "fields": { + "section": "購入品 {{index}}", + "foodProduct": "販売品名", + "items": "選択した料理に使用した食材・使用する材料", + "type": "商品の種類", + "shop": "購入場所", + "purchaseDate": "購入日", + "url": "URL", + "remark": "備考" + }, + "notes": { + "foodProduct": "販売品申請登録後に選択可能", + "shop": "ネット注文選択時はURL入力が必要です", + "purchaseDate": "例:2025/03/14", + "url": "購入したECサイトのURLなど", + "remarkOther": "店名・住所・電話番号・営業時間を入力してください", + "remarkDefault": "その他補足事項があれば入力してください" + }, + "buttons": { + "addItem": "購入品を追加" + }, + "messages": { + "itemDeleteSuccess": "購入品が削除されました", + "itemDeleteFailed": "削除に失敗しました。", + "bulkCreateSuccess": "複数の購入品申請が登録されました", + "updateSuccess": "購入品申請が更新されました", + "createSuccess": "購入品申請が登録されました", + "submitFailed": "登録に失敗しました" + }, + "validation": { + "foodProduct": "販売品名を選択してください", + "shop": "購入場所を選択してください", + "items": "食材・材料を入力してください", + "purchaseDate": "購入日を入力してください", + "invalidDate": "日付を入力してください", + "invalidUrl": "有効なURLを入力してください", + "urlRequired": "ネット注文の場合はURLを入力してください", + "remarkRequired": "「その他」の場合は、店名・住所・電話番号・営業時間を記入してください", + "minItems": "少なくとも1つの購入品を登録してください" + } + }, + "foodProduct": { + "title": "販売品申請", + "loading": "読み込み中...", + "errors": { + "fetch": "データの取得に失敗しました。" + }, + "deadline": { + "title": "申請期限が過ぎています", + "description": "販売品申請の締切期限が過ぎているため、新規申請はできません。" + }, + "view": { + "summaryLabel": "販売品一覧", + "registered": "{{count}}品目登録済み", + "none": "未登録", + "empty": "販売品が登録されていません", + "addButton": "販売品を追加" + }, + "summary": { + "labels": { + "name": "販売品名", + "alcohol": "酒類ですか?", + "cooking": "調理の有無", + "day1": "1日目の販売予定数", + "day2": "2日目の販売予定数" + } + }, + "radio": { + "alcohol": { + "label": "酒類ですか?", + "note": "「はい」を選択すると、自動的に「調理あり」になります。", + "options": { + "yes": "はい", + "no": "いいえ" + } + }, + "cooking": { + "label": "調理の有無", + "options": { + "yes": "有り (例:酒類、加熱調理をするものなど)", + "no": "無し (例:ソフトドリンク)" + } + } + }, + "fields": { + "name": "販売品名", + "day1": "1日目販売予定数", + "day2": "2日目販売予定数" + }, + "notes": { + "processing": "処理中...", + "quantity": "半角数字" + }, + "buttons": { + "add": "販売品の追加" + }, + "messages": { + "updateSuccess": "販売品を更新しました。", + "updateFailed": "販売品の更新に失敗しました。", + "updateFailedDetail": "販売品の更新に失敗しました: {{message}}", + "createSuccess": "販売品申請を送信しました。", + "createFailed": "販売品の登録に失敗しました。", + "createFailedDetail": "販売品の登録に失敗しました: {{message}}", + "deleteSuccess": "「{{name}}」を削除しました。", + "deleteFailed": "販売品の削除に失敗しました。", + "deleteFailedDetail": "販売品の削除に失敗しました: {{message}}", + "deleteNotFound": "削除対象の商品が見つかりませんでした。", + "authRequired": "認証が必要です。ログインしてください。" + }, + "validation": { + "name": "販売品名を入力してください", + "isAlcohol": "酒類かどうかを選択してください", + "isCooking": "調理の有無を選択してください", + "day1": "1日目の販売予定数を入力してください", + "day2": "2日目の販売予定数を入力してください", + "number": "半角数字で入力してください", + "minValue": "1以上の数値を入力してください", + "alcoholRequiresCooking": "酒類を販売する場合は調理の有無を「有り」にしてください", + "minProducts": "少なくとも1つの販売品を登録してください" + } + }, + "power": { + "title": "電力申請", + "radio": { + "question": "電力申請を行いますか?", + "options": { + "yes": "はい", + "no": "いいえ" + } + }, + "summary": { + "noApplication": { + "label": "電力申請は不要(登録済み)", + "description": "電力が必要な機器は使用しません。" + }, + "fields": { + "productName": "製品名", + "manufacturer": "メーカー名", + "model": "型番", + "url": "製品URL", + "maxPower": "消費電力" + }, + "powerValue": "{{value}}W" + }, + "form": { + "fields": { + "productName": "機器の名称", + "manufacturer": "機器のメーカー名", + "model": "型番", + "url": "製品URL", + "maxPower": "電力量 (W)" + }, + "notes": { + "url": "製品の紹介ページのサイトURLを入力してください", + "totalPower": "使用機器の消費電力の合計が{{limit}}W以内になるようにしてください", + "emailWarning": "{{limit}}Wを超える場合は以下のメールアドレスまでご連絡ください。", + "contactEmail": "nutfes.soumu@gmail.com" + }, + "addDevice": "物品の追加", + "totalPowerWarning": "合計電力が{{limit}}Wを超えています(現在: {{value}}W)" + }, + "errors": { + "submitTitle": "エラー:" + }, + "messages": { + "partialDeleteWarning": "一部の機器情報の削除に失敗しましたが、処理を続行します。", + "registerNegativeSuccess": "電力申請を行わない登録が完了しました。", + "registerNegativeFailed": "申請の登録に失敗しました。もう一度お試しください。", + "processError": "申請の処理に失敗しました。もう一度お試しください。", + "missingGroup": "グループIDが取得できませんでした。", + "unregisteredDeleteWarning": "未登録データの削除に問題がありましたが、処理を続行します。", + "updateSuccess": "電力申請情報を更新しました。", + "createSuccess": "電力申請情報を登録しました。", + "submitFailed": "申請の送信に失敗しました。もう一度お試しください。", + "submitUnexpectedError": "申請の送信中にエラーが発生しました。", + "deviceDeleteSuccess": "機器情報を削除しました。", + "deviceDeleteFailed": "機器の削除に失敗しました。もう一度お試しください。", + "deviceDeleteError": "機器の削除中にエラーが発生しました。" + }, + "validation": { + "productNameRequired": "製品名を入力してください", + "manufacturerRequired": "メーカー名を入力してください", + "modelRequired": "型番を入力してください", + "invalidUrl": "有効なURLを入力してください", + "invalidNumber": "数値を入力してください", + "minPower": "1W以上で入力してください", + "maxPower": "1500W以下で入力してください", + "minDevices": "少なくとも1つの機器を登録してください", + "totalPowerLimit": "合計消費電力は1500W以下にしてください" + } + }, + "stage": { + "title": "ステージ申請", + "loading": "データを読み込み中です...", + "fields": { + "date": "開催日", + "sunnyFirst": "晴れの場合:第1希望", + "sunnySecond": "晴れの場合:第2希望", + "rainyFirst": "雨の場合:第1希望", + "rainySecond": "雨の場合:第2希望", + "prepTime": "準備時間", + "performTime": "本番時間", + "cleanupTime": "片付け時間" + }, + "notes": { + "select": "選んでください", + "unit": "(単位:min)", + "prepTime": "ステージ上の準備にかかる時間を分単位で記入してください", + "performTime": "準備、本番、片付けの時間が120分以内になるようにしてください", + "cleanupTime": "ステージ上の片付けにかかる時間を分単位で記入してください" + }, + "minutes": "{{value}}分", + "messages": { + "missingGroup": "グループIDが見つかりません", + "submitError": "送信中にエラーが発生しました。もう一度お試しください。", + "unexpectedError": "予期せぬエラーが発生しました。もう一度お試しください。", + "updateSuccess": "ステージ希望を更新しました。", + "createSuccess": "ステージ希望を登録しました。" + }, + "errors": { + "fetchTitle": "エラー:", + "fetchDescription": "データの取得に失敗しました。ページを再読込してください。", + "submitTitle": "送信エラー:" + }, + "validation": { + "sunnyFirst": "晴れの第1希望を選択してください", + "sunnySecond": "晴れの第2希望を選択してください", + "rainyFirst": "雨の第1希望を選択してください", + "rainySecond": "雨の第2希望を選択してください", + "prepTimeRequired": "準備時間を入力してください", + "performTimeRequired": "本番時間を入力してください", + "cleanupTimeRequired": "片付け時間を入力してください", + "prepTimeInvalid": "有効な準備時間を入力してください", + "performTimeInvalid": "有効な本番時間を入力してください", + "cleanupTimeInvalid": "有効な片付け時間を入力してください", + "totalTime": "準備、本番、片付けの合計時間が120分を超えています", + "sunnyChoiceDuplicate": "第1希望と異なるステージを選んでください", + "rainyChoiceDuplicate": "第1希望と異なるステージを選んでください" + } + }, + "stageOptions": { + "title": "ステージオプション申請", + "fields": { + "ownEquipment": "電力を使用する機器を持ち込みますか", + "bgm": "スピーカーに繋ぐ機器を持ち込みますか", + "cameraPermission": "実行委員が撮影することを許可しますか", + "loudSound": "大きい音を出しますか" + }, + "notes": { + "select": "選んでください" + }, + "options": { + "yes": "はい", + "no": "いいえ" + }, + "messages": { + "submitSuccess": "送信しました", + "submitFailed": "送信に失敗しました。時間を置いて再度お試しください" + } + }, + "publicRelations": { + "title": "PR文申請", + "fields": { + "text": "PR文(HP,パンフレット,アナウンスに使用)", + "announce": "アナウンスを行いますか?", + "image": "PR画像" + }, + "notes": { + "text": "日本語の場合:0~50文字、英語の場合:0~25words", + "upload": [ + "ファイル形式:png、jpeg", + "ファイルサイズ:10MB未満", + "画像、イラストの形:正方形(できれば料理の写真)" + ], + "existingImage": "※新しい画像をアップロードしない場合、既存の画像がそのまま使用されます" + }, + "uploadStatus": "アップロード済み: {{fileName}}", + "options": { + "announce": { + "yes": "はい", + "no": "いいえ" + } + }, + "validation": { + "imageRequired": "画像をアップロードしてください", + "imageSquare": "画像は正方形にしてください", + "imageLoadFailed": "画像の読み込みに失敗しました", + "sizeLimit": "ファイルサイズは10MB未満にしてください", + "format": "ファイル形式はpngまたはjpegにしてください", + "jpLimit": "日本語は50文字以内で入力してください", + "enLimit": "英語は25単語以内で入力してください" + }, + "messages": { + "imgurMissing": "Imgur Client IDが設定されていません。環境変数を確認してください。", + "imgurUploadFailed": "画像のアップロードに失敗しました", + "submitSuccess": "送信しました", + "submitFailed": "送信に失敗しました。時間を置いて再度お試しください" + }, + "state": { + "notSet": "未設定", + "missingText": "(PR文が未入力です)" + } + }, + "viceRepresentative": { + "title": "副代表申請", + "note": "一人での参加者の場合のみ、副代表申請は不要です。", + "summary": { + "individual": { + "label": "副代表申請は不要(登録済み)", + "description": "あなたは1人での参加です" + } + }, + "fields": { + "isIndividual": "一人での参加ですか?", + "name": "名前", + "studentId": "学籍番号", + "gradeId": "課程・学年", + "departmentId": "学科・専攻", + "email": "メールアドレス", + "tel": "電話番号" + }, + "notes": { + "name": "例:長岡 太郎", + "studentId": "半角数字のみ8桁(例:12345678)", + "email": "例:123456@stn.nagaokaut.ac.jp", + "tel": "例:09012345678(ハイフンなし)" + }, + "radio": { + "options": { + "individual": "はい(個人で参加)", + "group": "いいえ(グループで参加)" + } + }, + "messages": { + "submitSuccess": "送信しました。", + "submitFailed": "送信に失敗しました。" + }, + "validation": { + "name": "名前を入力してください", + "studentIdInteger": "整数で入力してください", + "studentIdLength": "学籍番号は8桁で入力してください", + "gradeId": "課程・学年を選択してください", + "departmentId": "学科・専攻を選択してください", + "email": "メールアドレスを入力してください", + "emailFormat": "有効なメールアドレスを入力してください", + "tel": "電話番号は0から始まる10桁または11桁の半角数字のみで入力してください" + } + }, + "employees": { + "title": "従業員申請", + "deadline": { + "title": "申請期限が過ぎています", + "description": "従業員申請の締切期限が過ぎているため、新規申請はできません。" + }, + "summary": { + "noApplication": { + "label": "従業員申請は不要(登録済み)", + "description": "代表と副代表だけで活動します。" + }, + "headers": { + "name": "従業員名", + "studentId": "学籍番号" + } + }, + "radio": { + "label": "「代表」と「副代表」以外の従業員申請を行いますか?", + "options": { + "yes": "はい", + "no": "いいえ" + } + }, + "form": { + "labels": { + "name": "従業員名", + "studentId": "学籍番号" + }, + "notes": { + "name": "例:技大 花子", + "studentId": "例:12345678" + } + }, + "buttons": { + "addEmployee": "従業員の追加" + }, + "messages": { + "applicationSuccess": "従業員申請が完了しました。", + "applicationFailed": "従業員申請の登録に失敗しました。", + "noApplicationSuccess": "従業員申請を行わない登録が完了しました。", + "noApplicationFailed": "従業員申請を行わない登録に失敗しました。", + "deleteSuccess": "従業員を削除しました。", + "deleteFailed": "従業員の削除に失敗しました。", + "registerUnregisteredFailed": "登録に失敗しました。", + "deleteUnregisteredFailed": "削除に失敗しました。" + }, + "validation": { + "name": "従業員名は必須です", + "studentId": "8桁の学籍番号を入力してください" + } + }, + "cookingProcessOrder": { + "title": "調理工程申請", + "warning": "販売品申請を先に申請してください。", + "summary": { + "labels": { + "foodProduct": "販売品名", + "preOpen": "調理場の使用有無(営業前)", + "duringOpen": "調理場の使用有無(営業中)", + "description": "調理内容" + }, + "status": { + "use": "使用する", + "notUse": "使用しない", + "notRegistered": "未登録" + } + }, + "fields": { + "kitchenUsage": "調理場の使用有無", + "preOpen": "(営業前)", + "duringOpen": "(営業中)", + "tent": "調理内容", + "confirm": "調理工程確認事項" + }, + "placeholders": { + "tent": "例)\n1. コーヒー豆を15g測る\n2. 入れる\n3. 温める\n4. 皿に乗せる" + }, + "notes": { + "confirm": "確認事項にチェックを入れてください" + }, + "options": { + "kitchenUsage": { + "use": "使用する", + "notUse": "使用しない" + } + }, + "checkbox": { + "options": [ + "衛生管理の工程をできるだけ詳しく記載しました。", + "最終的に加熱して提供するか確認しました。", + "お酒の調理工程も提出しました。" + ] + }, + "buttons": { + "edit": "修正" + }, + "messages": { + "updateSuccess": "調理工程を更新しました。", + "updateFailed": "調理工程の更新に失敗しました。" + }, + "validation": { + "tentRequired": "調理内容を入力してください", + "confirmAll": "すべての確認事項にチェックを入れてください" + } + }, + "venueMap": { + "title": "模擬店平面図", + "fields": { + "picture": "模擬店平面図画像", + "checklist": "平面図確認事項" + }, + "summary": { + "notSet": "未設定" + }, + "notes": { + "required": "※必須", + "existing": "※新しい画像をアップロードしない場合、既存の画像がそのまま使用されます。", + "currentImage": "現在の画像: {{name}}", + "unknownFile": "ファイル名不明" + }, + "upload": { + "note": [ + "机、椅子、使用機器などの配置が分かるように", + "ファイル形式:png、jpeg", + "ファイルサイズ:20MB" + ], + "uploaded": "アップロード済み: {{fileName}}" + }, + "checklist": { + "note": "確認事項にチェックを入れてください", + "options": { + "trashPosition": "ゴミ箱の設置位置を記載しました。", + "foodStorage": "食材の保存場所を記載しました。", + "allItemsListed": "申請した物品をすべて平面図に記載しました。", + "fireHazardousMaterials": "火気・電化製品の使用場所を明記しました。", + "partitionPlacement": "パーテーション/掲示板が調理場内に入っておらず、テントの側面に設置してあることを確認しました。" + } + }, + "messages": { + "imgurMissing": "Imgur Client IDが設定されていません。環境変数を確認してください。", + "imgurUploadFailed": "画像のアップロードに失敗しました。", + "submitSuccess": "送信しました。", + "submitFailed": "送信に失敗しました。時間を置いて再度お試しください。" + }, + "buttons": { + "submitting": "送信中..." + }, + "validation": { + "imageRequired": "模擬店平面図画像をアップロードしてください。", + "fileSize": "ファイルサイズは20MB未満にしてください", + "fileType": "ファイル形式はpngまたはjpegにしてください", + "checklist": "すべての項目を確認してください。" + } + } + }, + "auth": { + "logout": "ログアウト" + }, "footer": { "copyright": "Copyright © {{year}} NUTMEG. All Rights Reserved." } From dd0af2de27239ebd8f0cb90f750139d1ccdd0e2f Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Thu, 1 Jan 2026 12:53:31 +0900 Subject: [PATCH 13/57] =?UTF-8?q?[feat]=20AccordionMenu=E3=82=B3=E3=83=B3?= =?UTF-8?q?=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=ABi18next?= =?UTF-8?q?=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=97=E3=81=9F=E7=BF=BB=E8=A8=B3?= =?UTF-8?q?=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81?= =?UTF-8?q?=E5=BF=85=E9=A0=88=E3=83=BB=E4=BB=BB=E6=84=8F=E3=81=AE=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA=E3=82=92=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=82=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/components/AccordionMenu/AccordionMenu.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/user/src/components/AccordionMenu/AccordionMenu.tsx b/user/src/components/AccordionMenu/AccordionMenu.tsx index 7ac1c2d6e..3a1aa77e8 100755 --- a/user/src/components/AccordionMenu/AccordionMenu.tsx +++ b/user/src/components/AccordionMenu/AccordionMenu.tsx @@ -1,4 +1,5 @@ import React, { FC, useState } from 'react'; +import { useTranslation } from 'next-i18next'; import { RiArrowDownWideLine } from 'react-icons/ri'; import { Textfit } from 'react-textfitfix'; import Status from '@/components/Status'; @@ -22,6 +23,7 @@ const AccordionMenu: FC = ({ required, note, }) => { + const { t } = useTranslation('common'); const receptionStatus = isEdit ? 'open' : 'closed'; const registerStatus = @@ -49,7 +51,7 @@ const AccordionMenu: FC = ({
- {required ? '必須' : '任意'} + {required ? t('form.required') : t('form.optional')}
From 19d165591651d4f38c9aeb8f21a488b3c7265f4d Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Thu, 1 Jan 2026 12:54:25 +0900 Subject: [PATCH 14/57] =?UTF-8?q?[feat]=20CookingProcessOrder=E3=82=B3?= =?UTF-8?q?=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=8A?= =?UTF-8?q?=E3=82=88=E3=81=B3=E9=96=A2=E9=80=A3=E3=83=95=E3=82=A9=E3=83=BC?= =?UTF-8?q?=E3=83=A0=E3=81=ABi18next=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=97?= =?UTF-8?q?=E3=81=9F=E7=BF=BB=E8=A8=B3=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E3=81=97=E3=80=81=E8=A1=A8=E7=A4=BA=E3=83=86=E3=82=AD?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=82=84=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=82=92=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=82=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CookingProcessOrder.tsx | 59 ++++++++++++++----- .../CookingProcessOrderForm.tsx | 52 ++++++++-------- .../CookingProcessOrderForm/schema.ts | 9 ++- .../Applications/CookingProcessOrder/hooks.ts | 8 ++- 4 files changed, 85 insertions(+), 43 deletions(-) diff --git a/user/src/components/Applications/CookingProcessOrder/CookingProcessOrder.tsx b/user/src/components/Applications/CookingProcessOrder/CookingProcessOrder.tsx index 885f49af8..f69529392 100644 --- a/user/src/components/Applications/CookingProcessOrder/CookingProcessOrder.tsx +++ b/user/src/components/Applications/CookingProcessOrder/CookingProcessOrder.tsx @@ -1,4 +1,5 @@ import { FC } from 'react'; +import { useTranslation } from 'next-i18next'; import { FormProvider } from 'react-hook-form'; import AccordionMenu from '@/components/AccordionMenu'; import Button from '@/components/Button'; @@ -17,6 +18,7 @@ const CookingProcessOrder: FC = ({ groupId, isDeadline, }) => { + const { t } = useTranslation('common'); const { methods, fields, @@ -30,12 +32,12 @@ const CookingProcessOrder: FC = ({ } = useCookingProcessOrder(groupId, isDeadline); if (isLoading) { - return
Loading...
; + return
{t('general.loading')}
; } return ( = ({ > {shouldShowWarning ? (

- 販売品申請を先に申請してください + {t('applications.cookingProcessOrder.warning')}

) : ( @@ -70,7 +72,9 @@ const CookingProcessOrder: FC = ({ } icon={isExist ? 'save' : 'send'} > - {isExist ? '更新' : '登録'} + {isExist + ? t('form.actions.save') + : t('form.actions.register')}
@@ -83,32 +87,57 @@ const CookingProcessOrder: FC = ({ cookingProcessOrder ? [ { - label: '販売品名', + label: t( + 'applications.cookingProcessOrder.summary.labels.foodProduct' + ), content: foodProduct.name, }, { - label: '調理場の使用有無(営業前)', + label: t( + 'applications.cookingProcessOrder.summary.labels.preOpen' + ), content: cookingProcessOrder.preOpenKitchen - ? '使用する' - : '使用しない', + ? t( + 'applications.cookingProcessOrder.summary.status.use' + ) + : t( + 'applications.cookingProcessOrder.summary.status.notUse' + ), }, { - label: '調理場の使用有無(営業中)', + label: t( + 'applications.cookingProcessOrder.summary.labels.duringOpen' + ), content: cookingProcessOrder.duringOpenKitchen - ? '使用する' - : '使用しない', + ? t( + 'applications.cookingProcessOrder.summary.status.use' + ) + : t( + 'applications.cookingProcessOrder.summary.status.notUse' + ), }, { - label: '調理内容', + label: t( + 'applications.cookingProcessOrder.summary.labels.description' + ), content: cookingProcessOrder.tent || '', }, ] : [ { - label: '販売品名', + label: t( + 'applications.cookingProcessOrder.summary.labels.foodProduct' + ), content: foodProduct.name, }, - { label: '調理工程', content: '未登録' }, + { + label: t( + 'applications.cookingProcessOrder.title' + ), + content: t( + 'applications.cookingProcessOrder.summary.status.notRegistered' + ), + }, ] } /> @@ -125,7 +154,7 @@ const CookingProcessOrder: FC = ({ icon="pencil" onClick={handleEditClick} > - 修正 + {t('form.actions.edit')}
)} diff --git a/user/src/components/Applications/CookingProcessOrder/CookingProcessOrderForm/CookingProcessOrderForm.tsx b/user/src/components/Applications/CookingProcessOrder/CookingProcessOrderForm/CookingProcessOrderForm.tsx index 2ba4e9ac7..274bbe8cd 100755 --- a/user/src/components/Applications/CookingProcessOrder/CookingProcessOrderForm/CookingProcessOrderForm.tsx +++ b/user/src/components/Applications/CookingProcessOrder/CookingProcessOrderForm/CookingProcessOrderForm.tsx @@ -1,4 +1,5 @@ import { FC } from 'react'; +import { useTranslation } from 'next-i18next'; import { useFormContext } from 'react-hook-form'; import CheckBox from '../../../Form/CheckBox'; import Radio from '../../../Form/Radio'; @@ -16,6 +17,7 @@ const CookingProcessOrderForm: FC = ({ foodProductName, }) => { const { setValue } = useFormContext(); + const { t } = useTranslation('common'); const { values, getError } = useCookingProcessOrderForm(index); // 調理場使用状況の定数 @@ -25,38 +27,42 @@ const CookingProcessOrderForm: FC = ({ } as const; const option = [ - { id: KITCHEN_USAGE.USE, name: '使用する' }, - { id: KITCHEN_USAGE.NOT_USE, name: '使用しない' }, - ]; - - const confirmCookingProcess = [ - { - id: '1', - name: '衛生管理の工程をできるだけ詳しく記載しました', - }, { - id: '2', - name: '最終的に加熱して提供するか確認しました', + id: KITCHEN_USAGE.USE, + name: t('applications.cookingProcessOrder.options.kitchenUsage.use'), }, { - id: '3', - name: 'お酒の調理工程も提出しました', + id: KITCHEN_USAGE.NOT_USE, + name: t('applications.cookingProcessOrder.options.kitchenUsage.notUse'), }, ]; + const confirmCookingProcess = ( + t('applications.cookingProcessOrder.checkbox.options', { + returnObjects: true, + }) as string[] + ).map((label, idx) => ({ + id: String(idx + 1), + name: label, + })); + return (
-
販売品名
+
+ {t('applications.cookingProcessOrder.summary.labels.foodProduct')} +
{foodProductName}
-

調理場の使用有無

-

※必須

+

+ {t('applications.cookingProcessOrder.fields.kitchenUsage')} +

+

※{t('form.required')}

= ({ error={getError('preOpenKitchen')} /> = ({ error={getError('duringOpenKitchen')} />
@@ -132,7 +137,7 @@ const GroupForm: FC = ({ type="button" onClick={toEdit} > - キャンセル + {t('form.actions.cancel')}
)} @@ -142,7 +147,7 @@ const GroupForm: FC = ({ type="submit" isDisable={createIsMutating || updateIsMutating || validateEdit()} > - {groups ? '修正' : '登録'} + {groups ? t('form.actions.edit') : t('form.actions.register')}
diff --git a/user/src/components/Applications/Group/GroupForm/hooks.ts b/user/src/components/Applications/Group/GroupForm/hooks.ts index 6fcc6070f..a4fe0dd11 100644 --- a/user/src/components/Applications/Group/GroupForm/hooks.ts +++ b/user/src/components/Applications/Group/GroupForm/hooks.ts @@ -6,6 +6,7 @@ import { } from '@/api/groupApi'; import { GROUP_CATEGORY } from '@/utils/constants'; import { zodResolver } from '@hookform/resolvers/zod'; +import { useTranslation } from 'next-i18next'; import { useForm } from 'react-hook-form'; import { toast } from 'react-toastify'; import { GroupForm, groupSchema } from './schema'; @@ -18,6 +19,7 @@ export const useGroupFormHooks = ( mutateCheckAllRegisteredGroups: () => void, mutateGroupByUserId: () => void ) => { + const { t } = useTranslation('common'); // 団体カテゴリー一覧を取得 const { handleSubmit, @@ -78,9 +80,9 @@ export const useGroupFormHooks = ( try { await update({ query: formData }); mutateGroups(); - toast.success('送信しました'); + toast.success(t('form.messages.updateSuccess')); } catch { - toast.error('送信に失敗しました。'); + toast.error(t('form.messages.updateFailed')); } // 団体申請がない場合は新規作成 } else { @@ -89,9 +91,9 @@ export const useGroupFormHooks = ( mutateGroups(); mutateCheckAllRegisteredGroups(); mutateGroupByUserId(); - toast.success('送信しました'); + toast.success(t('form.messages.registerSuccess')); } catch { - toast.error('送信に失敗しました。'); + toast.error(t('form.messages.registerFailed')); } reset(); } diff --git a/user/src/components/Applications/Group/GroupForm/schema.ts b/user/src/components/Applications/Group/GroupForm/schema.ts index f543abedc..239258dd5 100644 --- a/user/src/components/Applications/Group/GroupForm/schema.ts +++ b/user/src/components/Applications/Group/GroupForm/schema.ts @@ -3,15 +3,15 @@ import { z } from 'zod'; // バリデーションスキーマ export const groupSchema = z.object({ id: z.number().optional(), - name: z.string().min(1, '入力してください'), - projectName: z.string().min(1, '入力してください'), - activity: z.string().min(1, '入力してください'), + name: z.string().min(1, 'form.validation.required'), + projectName: z.string().min(1, 'form.validation.required'), + activity: z.string().min(1, 'form.validation.required'), userId: z.number(), - groupCategoryId: z.number().min(1, { message: '選択してください' }), + groupCategoryId: z.number().min(1, { message: 'form.validation.select' }), fesYearId: z.number(), committee: z.number(), - isInternational: z.boolean({ required_error: '選択してください' }), - isExternal: z.boolean({ required_error: '選択してください' }), + isInternational: z.boolean({ required_error: 'form.validation.select' }), + isExternal: z.boolean({ required_error: 'form.validation.select' }), }); export type GroupForm = z.infer; diff --git a/user/src/components/Applications/Group/hooks.ts b/user/src/components/Applications/Group/hooks.ts index 59003f170..cb88f4dd1 100644 --- a/user/src/components/Applications/Group/hooks.ts +++ b/user/src/components/Applications/Group/hooks.ts @@ -1,32 +1,38 @@ import { useEffect, useState } from 'react'; import { useGetGroupCategories, useGetGroups } from '@/api/groupApi'; +import { useTranslation } from 'next-i18next'; import { FormItem } from '@/components/FormList/type'; import { groupLabels } from '../label'; export const useGroupHooks = (groupId: number) => { + const { t } = useTranslation('common'); const { groups, isLoading, hasError, mutateGroups } = useGetGroups(groupId); const { groupCategories } = useGetGroupCategories(); // 団体申請のフォーム内容 const formItem: FormItem[] = [ { - label: groupLabels[0], + label: t(groupLabels[0]), content: groups?.name, }, { - label: groupLabels[1], + label: t(groupLabels[1]), content: groups?.projectName, }, { - label: groupLabels[2], - content: groups?.isInternational ? 'はい' : 'いいえ', + label: t(groupLabels[2]), + content: groups?.isInternational + ? t('applications.group.boolean.yes') + : t('applications.group.boolean.no'), }, { - label: groupLabels[3], - content: groups?.isExternal ? 'はい' : 'いいえ', + label: t(groupLabels[3]), + content: groups?.isExternal + ? t('applications.group.boolean.yes') + : t('applications.group.boolean.no'), }, { - label: groupLabels[4], + label: t(groupLabels[4]), content: groups?.groupCategoryId ? groupCategories?.find( (category) => category.id === groups.groupCategoryId @@ -34,7 +40,7 @@ export const useGroupHooks = (groupId: number) => { : '', }, { - label: groupLabels[5], + label: t(groupLabels[5]), content: groups?.activity, }, ]; From 957706a0d3f7b34b6fc48b56db223c083af82402 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Thu, 1 Jan 2026 12:55:33 +0900 Subject: [PATCH 18/57] =?UTF-8?q?[feat]=20RentItems=E3=82=B3=E3=83=B3?= =?UTF-8?q?=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=8A=E3=82=88?= =?UTF-8?q?=E3=81=B3=E9=96=A2=E9=80=A3=E3=83=95=E3=82=A9=E3=83=BC=E3=83=A0?= =?UTF-8?q?=E3=81=ABi18next=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=97=E3=81=9F?= =?UTF-8?q?=E7=BF=BB=E8=A8=B3=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=97=E3=80=81=E8=A1=A8=E7=A4=BA=E3=83=86=E3=82=AD=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=84=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E3=82=92=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=A9=E3=82=A4=E3=82=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MultiItemForms/RentItems/RentItems.tsx | 4 +- .../RentItems/RentItemsForm/RentItemsForm.tsx | 109 ++++++++++++------ .../RentItems/RentItemsForm/schema.ts | 26 +++-- .../RentItems/hooks/useRentItemsFormLogic.ts | 46 +++++--- 4 files changed, 120 insertions(+), 65 deletions(-) diff --git a/user/src/components/Applications/MultiItemForms/RentItems/RentItems.tsx b/user/src/components/Applications/MultiItemForms/RentItems/RentItems.tsx index 3511c9532..1334084d2 100644 --- a/user/src/components/Applications/MultiItemForms/RentItems/RentItems.tsx +++ b/user/src/components/Applications/MultiItemForms/RentItems/RentItems.tsx @@ -1,4 +1,5 @@ import { FC } from 'react'; +import { useTranslation } from 'next-i18next'; import AccordionMenu from '@/components/AccordionMenu'; import RentItemsForm from '@/components/Applications/MultiItemForms/RentItems/RentItemsForm'; @@ -15,9 +16,10 @@ const RentItems: FC = ({ groupId, groupCategoryId, }) => { + const { t } = useTranslation('common'); return ( = ({ groupCategoryId, isDeadline, }) => { + const { t } = useTranslation('common'); // 主に groupCategoryId を使って判断するように変更 const { form, @@ -50,7 +52,7 @@ const RentItemsForm: FC = ({ if (isLoading) { return (
-

データを読み込み中です...

+

{t('applications.rentItems.loading')}

); } @@ -58,9 +60,11 @@ const RentItemsForm: FC = ({ if (hasError) { return (
- エラー: + + {t('applications.rentItems.errors.fetchTitle')} + - データの取得に失敗しました。ページを再読込してください。 + {t('applications.rentItems.errors.fetchDescription')}
); @@ -72,8 +76,12 @@ const RentItemsForm: FC = ({
-

物品申請は不要(登録済み)

-

学校から借用する備品はありません。

+

+ {t('applications.rentItems.summary.noApplication.label')} +

+

+ {t('applications.rentItems.summary.noApplication.description')} +

{isDeadline && (
@@ -84,7 +92,7 @@ const RentItemsForm: FC = ({ icon="pencil" onClick={openEditMode} > - 修正 + {t('form.actions.edit')}
)} @@ -100,17 +108,17 @@ const RentItemsForm: FC = ({ {!hideLocationTypeSelect && ( // 特殊団体でない場合のみ表示

- 第一希望: + {t('applications.rentItems.location.displayLabel')} {form.getValues('locationType') === LOCATION_TYPES.INDOOR - ? '屋内' - : '屋外'} + ? t('applications.rentItems.location.options.indoor') + : t('applications.rentItems.location.options.outdoor')}

)} {isFoodSellingGroup && (

- ※食品販売団体は屋外での出店のみとなります + {t('applications.rentItems.location.notes.foodOnlyOutdoor')}

)} @@ -131,7 +139,9 @@ const RentItemsForm: FC = ({ )?.name || ''}

- {form.getValues(`items.${index}.count`)} 個 + {t('applications.rentItems.summary.count', { + value: form.getValues(`items.${index}.count`), + })}

@@ -148,7 +158,7 @@ const RentItemsForm: FC = ({ icon="pencil" onClick={openEditMode} > - 修正 + {t('form.actions.edit')}
)} @@ -163,17 +173,21 @@ const RentItemsForm: FC = ({ onSubmit={handleFormSubmit} >
-

- {!hideLocationTypeSelect && '会場申請を先に申請してください。'} - {isFoodSellingGroup && '※食品販売団体は屋外での出店のみとなります'} -

+
+ {!hideLocationTypeSelect && ( +

{t('applications.rentItems.location.notes.preApplication')}

+ )} + {isFoodSellingGroup && ( +

{t('applications.rentItems.location.notes.foodOnlyOutdoor')}

+ )} +

( { field.onChange(value === '1'); @@ -181,8 +195,14 @@ const RentItemsForm: FC = ({ }} required options={[ - { id: 1, name: 'はい' }, - { id: 0, name: 'いいえ' }, + { + id: 1, + name: t('applications.rentItems.radio.options.yes'), + }, + { + id: 0, + name: t('applications.rentItems.radio.options.no'), + }, ]} error={errors.hasItems?.message?.toString()} /> @@ -205,7 +225,7 @@ const RentItemsForm: FC = ({ } }} > - 登録 + {t('form.actions.register')}
@@ -221,7 +241,7 @@ const RentItemsForm: FC = ({ control={control} render={({ field }) => ( { // 新しい値でupdateLocationTypeを呼び出す @@ -229,8 +249,18 @@ const RentItemsForm: FC = ({ }} required options={[ - { id: 1, name: '屋内' }, - { id: 2, name: '屋外' }, + { + id: 1, + name: t( + 'applications.rentItems.location.options.indoor' + ), + }, + { + id: 2, + name: t( + 'applications.rentItems.location.options.outdoor' + ), + }, ]} error={errors.locationType?.message} /> @@ -242,7 +272,11 @@ const RentItemsForm: FC = ({
-

物品 {index + 1}

+

+ {t('applications.rentItems.fields.section', { + index: index + 1, + })} +

= ({ return ( { field.onChange(value); @@ -302,7 +336,7 @@ const RentItemsForm: FC = ({ return ( { const numValue = parseInt(value, 10); @@ -333,14 +367,14 @@ const RentItemsForm: FC = ({ }} />

- ※必要最低限の数だけ申請してください + {t('applications.rentItems.notes.minRequest')}

- 使用する個数が20個以上の場合はメールをお送りください + {t('applications.rentItems.notes.contactLimit')}
- nutfes.soumu@gmail.com + {t('applications.rentItems.notes.contactEmail')}

{fields.length > 1 && ( @@ -355,7 +389,7 @@ const RentItemsForm: FC = ({ >
- 削除 + {t('form.actions.delete')}
@@ -373,14 +407,18 @@ const RentItemsForm: FC = ({ {errors.root?.message && (
- {errors.root.message.toString()} + {t(errors.root.message.toString(), { + defaultValue: errors.root.message.toString(), + })}
)} {/* フォームバリデーションエラーがあれば表示(アイテム制限関連のエラーも含む) */} {errors.items?.message && (
- {errors.items.message.toString()} + {t(errors.items.message.toString(), { + defaultValue: errors.items.message.toString(), + })}
)} @@ -394,11 +432,14 @@ const RentItemsForm: FC = ({ }} >
- + 物品の追加 + +{' '} + {t('applications.rentItems.buttons.addItem')}
diff --git a/user/src/components/Applications/MultiItemForms/RentItems/RentItemsForm/schema.ts b/user/src/components/Applications/MultiItemForms/RentItems/RentItemsForm/schema.ts index 3bd17ed1d..a531b12b3 100644 --- a/user/src/components/Applications/MultiItemForms/RentItems/RentItemsForm/schema.ts +++ b/user/src/components/Applications/MultiItemForms/RentItems/RentItemsForm/schema.ts @@ -5,9 +5,11 @@ import { z } from 'zod'; // 物品申請フォームのスキーマ定義 export const rentItemSchema = z.object({ itemId: z.string().refine((val) => val !== '' && val !== '0', { - message: '物品を選択してください', + message: 'applications.rentItems.validation.selectItem', }), - count: z.number().min(1, '1つ以上選択してください'), + count: z + .number() + .min(1, { message: 'applications.rentItems.validation.minCount' }), }); // 物品IDの定数 @@ -33,7 +35,7 @@ export const rentItemsFormSchema = z hasItems: z.boolean(), locationType: z .string() - .min(1, '会場タイプを選択してください') + .min(1, { message: 'applications.rentItems.validation.selectLocation' }) .default('1'), items: z.array(rentItemSchema).optional().default([]), }) @@ -46,7 +48,7 @@ export const rentItemsFormSchema = z return true; }, { - message: '会場タイプを選択してください', + message: 'applications.rentItems.validation.selectLocation', path: ['locationType'], } ) @@ -59,7 +61,7 @@ export const rentItemsFormSchema = z return true; }, { - message: '少なくとも1つの物品を追加してください', + message: 'applications.rentItems.validation.addOneItem', path: ['items'], } ) @@ -79,7 +81,7 @@ export const rentItemsFormSchema = z return true; }, { - message: 'すべての物品情報を正しく入力してください', + message: 'applications.rentItems.validation.fillAllFields', path: ['items'], } ) @@ -93,7 +95,7 @@ export const rentItemsFormSchema = z return true; }, { - message: '同じ物品を複数回追加することはできません', + message: 'applications.rentItems.validation.noDuplicates', path: ['items'], } ) @@ -106,7 +108,7 @@ export const rentItemsFormSchema = z return !tentItem || tentItem.count <= 1; }, { - message: 'テントは1個までしか申請できません', + message: 'applications.rentItems.validation.tentLimit', path: ['items'], } ) @@ -125,7 +127,7 @@ export const rentItemsFormSchema = z return !(hasPartition && hasDisplayBoard); }, { - message: 'パーテーションと掲示板はどちらか一方のみ申請できます', + message: 'applications.rentItems.validation.partitionDisplayExclusive', path: ['items'], } ) @@ -140,7 +142,7 @@ export const rentItemsFormSchema = z return !longTableItem || longTableItem.count <= 1; }, { - message: '長机は1個までしか申請できません', + message: 'applications.rentItems.validation.longTableLimit', path: ['items'], } ) @@ -161,7 +163,7 @@ export const rentItemsFormSchema = z return data.locationType !== '2' || tableItem.count <= 20; }, { - message: '屋外団体は机を20個までしか申請できません', + message: 'applications.rentItems.validation.tableOutdoorLimit', path: ['items'], } ) @@ -182,7 +184,7 @@ export const rentItemsFormSchema = z return data.locationType !== '2' || chairItem.count <= 20; }, { - message: '屋外団体は椅子を20個までしか申請できません', + message: 'applications.rentItems.validation.chairOutdoorLimit', path: ['items'], } ); diff --git a/user/src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsFormLogic.ts b/user/src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsFormLogic.ts index 4d31ec589..29a870bac 100644 --- a/user/src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsFormLogic.ts +++ b/user/src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsFormLogic.ts @@ -11,6 +11,7 @@ import { } from '@/api/rentItemsApi'; import { useGetPlaceOrder } from '@/api/venueApplication'; import { GROUP_CATEGORY } from '@/utils/constants'; +import { useTranslation } from 'next-i18next'; import { SubmitHandler, useFieldArray, useForm } from 'react-hook-form'; import { toast } from 'react-toastify'; import { useAuthenticatedGet } from '@/hooks/useApi'; @@ -67,6 +68,7 @@ export const useRentItemsFormLogic = ( groupId: number, groupCategoryId?: number // 団体カテゴリID ) => { + const { t } = useTranslation('common'); // 認証基盤ができたら、グループIDを取得する const currentGroupId = groupId; const [submitError, setSubmitError] = useState(''); @@ -208,13 +210,13 @@ export const useRentItemsFormLogic = ( // 物品のオプション const itemOptions = useMemo( () => [ - { id: 0, name: '選んでください' }, + { id: 0, name: t('form.validation.select') }, ...filteredItems.map((item) => ({ id: item.id, name: item.name, })), ], - [filteredItems] + [filteredItems, t] ); // 特殊団体の場合に会場タイプの選択を非表示にするフラグ @@ -525,7 +527,7 @@ export const useRentItemsFormLogic = ( if (!unRegisteredResult.success) { console.error('登録エラー:', unRegisteredResult.error); // トースト通知でエラーを表示 - toast.error('物品申請の登録に失敗しました'); + toast.error(t('applications.rentItems.messages.registerNoItemsFailed')); return false; } @@ -536,9 +538,13 @@ export const useRentItemsFormLogic = ( ); if (!result.success) { - setSubmitError('既存の申請データ削除中にエラーが発生しました'); + setSubmitError( + t('applications.rentItems.messages.deleteExistingError') + ); // トースト通知でエラーを表示 - toast.error('既存の物品申請の削除に失敗しました'); + toast.error( + t('applications.rentItems.messages.deleteExistingFailed') + ); return false; } } @@ -551,15 +557,21 @@ export const useRentItemsFormLogic = ( // API更新の通知 await mutateRentalOrders(); // 成功時のトースト通知 - toast.success('物品申請を行わない設定を登録しました'); + toast.success( + t('applications.rentItems.messages.registerNoItemsSuccess') + ); return true; } catch (error) { console.error('予期せぬエラー:', error); const errorMessage = error instanceof Error ? error.message : '不明なエラー'; - setSubmitError('予期せぬエラーが発生しました: ' + errorMessage); + setSubmitError( + t('applications.rentItems.messages.unexpectedErrorWithDetail', { + message: errorMessage, + }) + ); // トースト通知でエラーを表示 - toast.error('予期せぬエラーが発生しました'); + toast.error(t('applications.rentItems.messages.unexpectedError')); return false; } }; @@ -595,25 +607,23 @@ export const useRentItemsFormLogic = ( // アラートの代わりにトースト通知を使用 toast.success( rentalOrders.length > 0 - ? '物品申請を更新しました' - : '物品申請を登録しました' + ? t('applications.rentItems.messages.updateSuccess') + : t('applications.rentItems.messages.createSuccess') ); await mutateRentalOrders(); setIsEditMode(false); userChangedLocationType.current = false; } else { - setSubmitError( - '送信中にエラーが発生しました。もう一度お試しください。' - ); + setSubmitError(t('applications.rentItems.messages.submitError')); // トースト通知でエラーを表示 - toast.error('物品申請の送信に失敗しました'); + toast.error(t('applications.rentItems.messages.submitFailed')); } } catch (error) { console.error('物品申請エラー:', error); - setSubmitError('予期せぬエラーが発生しました。もう一度お試しください。'); + setSubmitError(t('applications.rentItems.messages.unexpectedRetry')); // トースト通知でエラーを表示 - toast.error('予期せぬエラーが発生しました'); + toast.error(t('applications.rentItems.messages.unexpectedError')); } }; @@ -703,9 +713,9 @@ export const useRentItemsFormLogic = ( setTimeout(() => trigger(), 100); } catch (error) { console.error('編集モード起動エラー:', error); - setSubmitError('予期せぬエラーが発生しました。'); + setSubmitError(t('applications.rentItems.messages.unexpectedError')); // エラー時にトースト通知を表示 - toast.error('編集モードの開始に失敗しました'); + toast.error(t('applications.rentItems.messages.editStartFailed')); } }; From fa0e689a956a6953f85270eda15029a092527c8e Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Thu, 1 Jan 2026 12:55:54 +0900 Subject: [PATCH 19/57] =?UTF-8?q?[feat]=20Power=E3=82=B3=E3=83=B3=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=8A=E3=82=88=E3=81=B3?= =?UTF-8?q?=E9=96=A2=E9=80=A3=E3=83=95=E3=82=A9=E3=83=BC=E3=83=A0=E3=81=AB?= =?UTF-8?q?i18next=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=97=E3=81=9F=E7=BF=BB?= =?UTF-8?q?=E8=A8=B3=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97?= =?UTF-8?q?=E3=80=81=E8=A1=A8=E7=A4=BA=E3=83=86=E3=82=AD=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=84=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=82=92?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=A9=E3=82=A4=E3=82=BA=E3=80=82?= =?UTF-8?q?=E5=90=88=E8=A8=88=E9=9B=BB=E5=8A=9B=E5=88=B6=E9=99=90=E3=82=92?= =?UTF-8?q?=E5=AE=9A=E6=95=B0=E5=8C=96=E3=81=97=E3=80=81=E9=96=A2=E9=80=A3?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=83=AD=E3=82=B8=E3=83=83=E3=82=AF=E3=82=92?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Applications/Power/Power.tsx | 5 +- .../Power/components/PowerForm.tsx | 30 +++++++---- .../Power/components/PowerFormView.tsx | 32 ++++++++--- .../Power/components/PowerNegativeView.tsx | 28 +++++++--- .../Power/components/PowerSummaryView.tsx | 40 +++++++++++--- .../Applications/Power/constants.ts | 24 +++++---- .../Power/hooks/usePowerApplication.ts | 53 +++++++++++-------- .../components/Applications/Power/schema.ts | 6 +-- .../components/Applications/Power/types.ts | 2 +- 9 files changed, 146 insertions(+), 74 deletions(-) diff --git a/user/src/components/Applications/Power/Power.tsx b/user/src/components/Applications/Power/Power.tsx index 6ecab1bbd..98055d60f 100644 --- a/user/src/components/Applications/Power/Power.tsx +++ b/user/src/components/Applications/Power/Power.tsx @@ -1,4 +1,5 @@ import { FC } from 'react'; +import { useTranslation } from 'next-i18next'; import AccordionMenu from '@/components/AccordionMenu'; import { PowerNegativeView, PowerSummaryView } from './components'; import { PowerFormView } from './components/PowerFormView'; @@ -13,6 +14,8 @@ type PowerProps = { }; const Power: FC = ({ isDeadline, isRegistered, groupId }) => { + const { t } = useTranslation('common'); + // 電力申請のカスタムフックから状態とロジックの取得 const { state, @@ -142,7 +145,7 @@ const Power: FC = ({ isDeadline, isRegistered, groupId }) => { return ( = ({ const PowerForm: FC = ({ index, form, onRemove }) => { const { control, formState } = form; + const { t } = useTranslation('common'); // エラーメッセージを取得 - DeviceField型を受け取るように修正 const getErrorMessage = (name: DeviceField) => { @@ -63,7 +65,7 @@ const PowerForm: FC = ({ index, form, onRemove }) => {
= ({ index, form, onRemove }) => { = ({ index, form, onRemove }) => { = ({ index, form, onRemove }) => {
-

電力量が1500W以上の場合はメールを送ってください。

-

nutfes.soumu@gmail.com

+

+ {t('applications.power.form.notes.emailWarning', { + limit: POWER_LIMIT, + })} +

+

{t('applications.power.form.notes.contactEmail')}

{index > 0 && !form.getValues().devices[index]?.productName && ( @@ -124,7 +132,7 @@ const PowerForm: FC = ({ index, form, onRemove }) => { variant onClick={() => onRemove(index)} > - 削除 + {t('form.actions.delete')}
)} diff --git a/user/src/components/Applications/Power/components/PowerFormView.tsx b/user/src/components/Applications/Power/components/PowerFormView.tsx index ee60f2098..42be31b34 100644 --- a/user/src/components/Applications/Power/components/PowerFormView.tsx +++ b/user/src/components/Applications/Power/components/PowerFormView.tsx @@ -1,6 +1,8 @@ -import { FC } from 'react'; +import { FC, useMemo } from 'react'; +import { useTranslation } from 'next-i18next'; import Button from '@/components/Button/Button'; import Radio from '@/components/Form/Radio/Radio'; +import { POWER_LIMIT } from '../constants'; import { PowerFormViewProps } from '../types'; import PowerForm from './PowerForm'; @@ -18,16 +20,25 @@ export const PowerFormView: FC = ({ onSubmit, }) => { const { handleSubmit } = formMethods; + const { t } = useTranslation('common'); + const translatedRadioOptions = useMemo( + () => + radioOptions.map((option) => ({ + id: option.id, + name: t(option.labelKey), + })), + [radioOptions, t] + ); return (
{/* ラジオボタン */} {/* 申請する場合のフォーム */} @@ -46,9 +57,14 @@ export const PowerFormView: FC = ({
{/* 電力超過警告 */}
- {totalPower > 1500 && ( + {totalPower > POWER_LIMIT && (
-

合計電力が1500Wを超えています(現在: {totalPower}W)

+

+ {t('applications.power.form.totalPowerWarning', { + limit: POWER_LIMIT, + value: totalPower, + })} +

)} {/* 操作ボタン */} @@ -61,15 +77,15 @@ export const PowerFormView: FC = ({ variant onClick={onAddDevice} > - 物品の追加 + {t('applications.power.form.addDevice')}
diff --git a/user/src/components/Applications/Power/components/PowerNegativeView.tsx b/user/src/components/Applications/Power/components/PowerNegativeView.tsx index f3ec9a275..0caed0e06 100644 --- a/user/src/components/Applications/Power/components/PowerNegativeView.tsx +++ b/user/src/components/Applications/Power/components/PowerNegativeView.tsx @@ -1,4 +1,5 @@ -import { FC } from 'react'; +import { FC, useMemo } from 'react'; +import { useTranslation } from 'next-i18next'; import Button from '@/components/Button/Button'; import Radio from '@/components/Form/Radio/Radio'; import FormList from '@/components/FormList/FormList'; @@ -18,10 +19,19 @@ export const PowerNegativeView: FC = ({ onCancel, isDeadline, }) => { + const { t } = useTranslation('common'); + const translatedRadioOptions = useMemo( + () => + radioOptions.map((option) => ({ + id: option.id, + name: t(option.labelKey), + })), + [radioOptions, t] + ); const noApplicationItems: FormItem[] = [ { - label: '電力申請は不要(登録済み)', - content: '電力が必要な機器は使用しません。', + label: t('applications.power.summary.noApplication.label'), + content: t('applications.power.summary.noApplication.description'), }, ]; @@ -30,11 +40,11 @@ export const PowerNegativeView: FC = ({ {isEdit && ( <> {onCancel && (
@@ -45,7 +55,7 @@ export const PowerNegativeView: FC = ({ variant onClick={onCancel} > - キャンセル + {t('form.actions.cancel')}
)} @@ -64,7 +74,9 @@ export const PowerNegativeView: FC = ({
{submitError && (
- エラー: + + {t('applications.power.errors.submitTitle')} + {submitError}
)} @@ -74,7 +86,7 @@ export const PowerNegativeView: FC = ({ color="main" onClick={onNegativeSubmit} > - 登録 + {t('form.actions.register')}
)} diff --git a/user/src/components/Applications/Power/components/PowerSummaryView.tsx b/user/src/components/Applications/Power/components/PowerSummaryView.tsx index 48ef3bc7b..e4cca3999 100644 --- a/user/src/components/Applications/Power/components/PowerSummaryView.tsx +++ b/user/src/components/Applications/Power/components/PowerSummaryView.tsx @@ -1,19 +1,41 @@ import { FC } from 'react'; +import { TFunction } from 'i18next'; +import { useTranslation } from 'next-i18next'; import Button from '@/components/Button/Button'; import FormList from '@/components/FormList/FormList'; import { FormItem } from '@/components/FormList/type'; import { Device, PowerSummaryViewProps } from '../types'; // デバイス情報からフォームアイテムを作成する関数 -export const createFormItemsForDevice = (device: Device): FormItem[] => { +export const createFormItemsForDevice = ( + device: Device, + t: TFunction<'common'> +): FormItem[] => { const items: FormItem[] = []; - items.push({ label: '製品名', content: device.productName }); - items.push({ label: 'メーカー名', content: device.manufacturer }); - items.push({ label: '型番', content: device.model }); + items.push({ + label: t('applications.power.summary.fields.productName'), + content: device.productName, + }); + items.push({ + label: t('applications.power.summary.fields.manufacturer'), + content: device.manufacturer, + }); + items.push({ + label: t('applications.power.summary.fields.model'), + content: device.model, + }); if (device.url) { - items.push({ label: '製品URL', content: device.url }); + items.push({ + label: t('applications.power.summary.fields.url'), + content: device.url, + }); } - items.push({ label: '消費電力[W]', content: `${device.maxPower}W` }); + items.push({ + label: t('applications.power.summary.fields.maxPower'), + content: t('applications.power.summary.powerValue', { + value: device.maxPower, + }), + }); return items; }; @@ -23,12 +45,14 @@ export const PowerSummaryView: FC = ({ onDeleteDevice, isDeadline, }) => { + const { t } = useTranslation('common'); + return (
{devices.map((device, index) => (
onDeleteDevice(device.id!) : undefined} @@ -45,7 +69,7 @@ export const PowerSummaryView: FC = ({ icon="pencil" onClick={onEdit} > - 修正 + {t('form.actions.edit')}
)} diff --git a/user/src/components/Applications/Power/constants.ts b/user/src/components/Applications/Power/constants.ts index fac4b35dd..16b927ea6 100644 --- a/user/src/components/Applications/Power/constants.ts +++ b/user/src/components/Applications/Power/constants.ts @@ -1,9 +1,11 @@ import { RadioOption } from './types'; +export const POWER_LIMIT = 1500; + // ラジオボタンの選択肢 export const RADIO_OPTIONS: RadioOption[] = [ - { id: 1, name: 'はい' }, - { id: 2, name: 'いいえ' }, + { id: 1, labelKey: 'applications.power.radio.options.yes' }, + { id: 2, labelKey: 'applications.power.radio.options.no' }, ]; // デフォルトのデバイス情報 @@ -26,13 +28,13 @@ export const FIELD_NAMES = { // バリデーションメッセージの定数 export const VALIDATION_MESSAGES = { - REQUIRED_PRODUCT_NAME: '製品名を入力してください', - REQUIRED_MANUFACTURER: 'メーカー名を入力してください', - REQUIRED_MODEL: '型番を入力してください', - INVALID_URL: '有効なURLを入力してください', - INVALID_NUMBER: '数値を入力してください', - MIN_POWER: '1W以上で入力してください', - MAX_POWER: '1500W以下で入力してください', - MIN_DEVICES: '少なくとも1つの機器を登録してください', - TOTAL_POWER_LIMIT: '合計消費電力は1500W以下にしてください', + REQUIRED_PRODUCT_NAME: 'applications.power.validation.productNameRequired', + REQUIRED_MANUFACTURER: 'applications.power.validation.manufacturerRequired', + REQUIRED_MODEL: 'applications.power.validation.modelRequired', + INVALID_URL: 'applications.power.validation.invalidUrl', + INVALID_NUMBER: 'applications.power.validation.invalidNumber', + MIN_POWER: 'applications.power.validation.minPower', + MAX_POWER: 'applications.power.validation.maxPower', + MIN_DEVICES: 'applications.power.validation.minDevices', + TOTAL_POWER_LIMIT: 'applications.power.validation.totalPowerLimit', }; diff --git a/user/src/components/Applications/Power/hooks/usePowerApplication.ts b/user/src/components/Applications/Power/hooks/usePowerApplication.ts index c7ba8ac5e..c27ccc655 100644 --- a/user/src/components/Applications/Power/hooks/usePowerApplication.ts +++ b/user/src/components/Applications/Power/hooks/usePowerApplication.ts @@ -5,6 +5,7 @@ import { useGetUnregisteredGroup, useMutateUnregisteredGroup, } from '@/api/unRegisteredGroupApi'; +import { useTranslation } from 'next-i18next'; import { toast } from 'react-toastify'; import { mutate } from 'swr'; import { DEFAULT_DEVICE } from '../constants'; @@ -21,6 +22,7 @@ type PowerApplicationState = { }; export const usePowerApplication = (groupId: number) => { + const { t } = useTranslation('common'); // 電力申請のステート管理 const [state, setState] = useState({ isEditing: false, @@ -158,9 +160,7 @@ export const usePowerApplication = (groupId: number) => { const hasFailures = deleteResults.some((result) => !result.success); if (hasFailures) { console.warn('一部のデバイス削除に失敗しましたが、処理を続行します'); - toast.warning( - '一部の機器情報の削除に失敗しましたが、処理を続行します' - ); + toast.warning(t('applications.power.messages.partialDeleteWarning')); } } @@ -172,19 +172,21 @@ export const usePowerApplication = (groupId: number) => { await mutatePowerOrders(); await mutateUnregisteredGroup(); mutate(`/check_all_registered/${groupId}`); // 全体登録状態を再取得 - toast.success('電力申請を行わない登録が完了しました'); + toast.success(t('applications.power.messages.registerNegativeSuccess')); } else { + const message = t('applications.power.messages.registerNegativeFailed'); updateState({ - submitError: '申請の登録に失敗しました。もう一度お試しください。', + submitError: message, }); - toast.error('申請の登録に失敗しました'); + toast.error(message); } } catch (error) { console.error('申請処理中のエラー:', error); + const message = t('applications.power.messages.processError'); updateState({ - submitError: '申請の処理に失敗しました。もう一度お試しください。', + submitError: message, }); - toast.error('申請の処理に失敗しました'); + toast.error(message); } }; @@ -193,8 +195,9 @@ export const usePowerApplication = (groupId: number) => { updateState({ submitError: null }); if (!groupId) { - updateState({ submitError: 'グループIDが取得できませんでした。' }); - toast.error('グループIDが取得できませんでした'); + const message = t('applications.power.messages.missingGroup'); + updateState({ submitError: message }); + toast.error(message); return; } @@ -205,7 +208,7 @@ export const usePowerApplication = (groupId: number) => { if (!deleteResult.success) { console.warn('未登録テーブル削除エラー:', deleteResult.error); toast.warning( - '未登録データの削除に問題がありましたが、処理を続行します' + t('applications.power.messages.unregisteredDeleteWarning') ); } } catch (error) { @@ -214,7 +217,7 @@ export const usePowerApplication = (groupId: number) => { error ); toast.warning( - '未登録データの削除に問題がありましたが、処理を続行します' + t('applications.power.messages.unregisteredDeleteWarning') ); } @@ -236,22 +239,24 @@ export const usePowerApplication = (groupId: number) => { updateState({ isEditing: false }); // 編集か新規登録かによって通知メッセージを変える if (hasExisting) { - toast.success('電力申請情報を更新しました'); + toast.success(t('applications.power.messages.updateSuccess')); } else { - toast.success('電力申請情報を登録しました'); + toast.success(t('applications.power.messages.createSuccess')); } } else { + const message = t('applications.power.messages.submitFailed'); updateState({ - submitError: '申請の送信に失敗しました。もう一度お試しください。', + submitError: message, }); - toast.error('申請の送信に失敗しました'); + toast.error(message); } } catch (error) { console.error('申請送信中のエラー:', error); + const message = t('applications.power.messages.submitUnexpectedError'); updateState({ - submitError: '申請の送信中にエラーが発生しました。', + submitError: message, }); - toast.error('申請の送信中にエラーが発生しました'); + toast.error(message); } }; @@ -265,7 +270,7 @@ export const usePowerApplication = (groupId: number) => { const result = await deletePowerOrder(deviceId); if (result.success) { await mutatePowerOrders(); - toast.success('機器情報を削除しました'); + toast.success(t('applications.power.messages.deviceDeleteSuccess')); // すべてのデバイスが削除された場合、編集モードに切り替える if (willBeEmpty) { @@ -278,16 +283,18 @@ export const usePowerApplication = (groupId: number) => { formMethods.reset({ devices: [{ ...DEFAULT_DEVICE }] }); } } else { + const message = t('applications.power.messages.deviceDeleteFailed'); updateState({ - submitError: '機器の削除に失敗しました。もう一度お試しください。', + submitError: message, }); - toast.error('機器の削除に失敗しました'); + toast.error(message); } } catch { + const message = t('applications.power.messages.deviceDeleteError'); updateState({ - submitError: '機器の削除中にエラーが発生しました。', + submitError: message, }); - toast.error('機器の削除中にエラーが発生しました'); + toast.error(message); } }; diff --git a/user/src/components/Applications/Power/schema.ts b/user/src/components/Applications/Power/schema.ts index 5e702578c..1ade3aaf4 100644 --- a/user/src/components/Applications/Power/schema.ts +++ b/user/src/components/Applications/Power/schema.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { VALIDATION_MESSAGES } from './constants'; +import { POWER_LIMIT, VALIDATION_MESSAGES } from './constants'; import { Device } from './types'; // 合計電力を計算する関数 @@ -15,7 +15,7 @@ export const deviceSchema = z.object({ maxPower: z .number({ invalid_type_error: VALIDATION_MESSAGES.INVALID_NUMBER }) .min(1, { message: VALIDATION_MESSAGES.MIN_POWER }) - .max(1500, { message: VALIDATION_MESSAGES.MAX_POWER }), + .max(POWER_LIMIT, { message: VALIDATION_MESSAGES.MAX_POWER }), manufacturer: z .string() .min(1, { message: VALIDATION_MESSAGES.REQUIRED_MANUFACTURER }), @@ -33,7 +33,7 @@ export const powerApplicationSchema = z .array(deviceSchema) .min(1, { message: VALIDATION_MESSAGES.MIN_DEVICES }), }) - .refine((data) => calculateTotalPower(data.devices) <= 1500, { + .refine((data) => calculateTotalPower(data.devices) <= POWER_LIMIT, { message: VALIDATION_MESSAGES.TOTAL_POWER_LIMIT, path: ['devices'], }); diff --git a/user/src/components/Applications/Power/types.ts b/user/src/components/Applications/Power/types.ts index 83795222c..9664227bb 100644 --- a/user/src/components/Applications/Power/types.ts +++ b/user/src/components/Applications/Power/types.ts @@ -18,7 +18,7 @@ export type PowerApplicationOption = 'yes' | 'no' | 'undecided'; export type RadioOption = { id: number; - name: string; + labelKey: string; }; export type DeviceField = keyof Device; From b97af9d83352cf37d5edab5811241db3b717084c Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Thu, 1 Jan 2026 12:56:03 +0900 Subject: [PATCH 20/57] =?UTF-8?q?[feat]=20PublicRelations=E3=82=B3?= =?UTF-8?q?=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=8A?= =?UTF-8?q?=E3=82=88=E3=81=B3=E9=96=A2=E9=80=A3=E3=83=95=E3=82=A9=E3=83=BC?= =?UTF-8?q?=E3=83=A0=E3=81=ABi18next=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=97?= =?UTF-8?q?=E3=81=9F=E7=BF=BB=E8=A8=B3=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E3=81=97=E3=80=81=E8=A1=A8=E7=A4=BA=E3=83=86=E3=82=AD?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=82=84=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E3=82=92=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=82=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PublicRelations/PublicRelations.tsx | 9 ++-- .../PublicRelationsForm.tsx | 35 +++++++++------- .../PublicRelationsForm/hooks.ts | 41 +++++++++++-------- .../PublicRelationsForm/schema.ts | 18 ++++---- .../Applications/PublicRelations/hooks.ts | 22 ++++++---- 5 files changed, 76 insertions(+), 49 deletions(-) diff --git a/user/src/components/Applications/PublicRelations/PublicRelations.tsx b/user/src/components/Applications/PublicRelations/PublicRelations.tsx index fb4202522..4139b9439 100644 --- a/user/src/components/Applications/PublicRelations/PublicRelations.tsx +++ b/user/src/components/Applications/PublicRelations/PublicRelations.tsx @@ -1,5 +1,6 @@ import { FC } from 'react'; import { PublicRelationResponse } from '@/api/publicRelationsApi'; +import { useTranslation } from 'next-i18next'; import AccordionMenu from '@/components/AccordionMenu/AccordionMenu'; import PublicRelationsForm from '@/components/Applications/PublicRelations/PublicRelationsForm/PublicRelationsForm'; import { usePublicRelationsHooks } from '@/components/Applications/PublicRelations/hooks'; @@ -33,14 +34,15 @@ const Content: FC = ({ formItem, groupId, }) => { + const { t } = useTranslation('common'); if (isLoading) { - return
Loading...
; + return
{t('general.loading')}
; } if (hasError) { return (
- データの取得に失敗しました。 + {t('general.errors.fetch')}
); } @@ -67,12 +69,13 @@ const PublicRelations: FC = ({ isDeadline, isRegistered, }) => { + const { t } = useTranslation('common'); const { formItem, isEditing, toEdit, publicRelation, isLoading, hasError } = usePublicRelationsHooks(groupId); return ( = ({ publicRelation, toEdit, }) => { + const { t } = useTranslation('common'); // PublicRelationsForm receives toEdit as a required prop const { handleSubmit, @@ -33,11 +35,14 @@ const PublicRelationsForm: FC = ({ onSubmit, validateEdit, } = usePublicRelationsFormHooks(groupId, publicRelation); + const uploadNote = t('applications.publicRelations.notes.upload', { + returnObjects: true, + }) as string[]; return ( {isFetching || isMutating ? ( -
loading...
+
{t('general.loading')}
) : (
= ({ {/* PR文入力 */}
@@ -137,7 +120,7 @@ const GroupForm: FC = ({ type="button" onClick={toEdit} > - {t('form.actions.cancel')} + {groupFormTexts.buttons.cancel}
)} @@ -147,7 +130,9 @@ const GroupForm: FC = ({ type="submit" isDisable={createIsMutating || updateIsMutating || validateEdit()} > - {groups ? t('form.actions.edit') : t('form.actions.register')} + {groups + ? groupFormTexts.buttons.edit + : groupFormTexts.buttons.register}
diff --git a/user/src/components/Applications/Group/GroupForm/hooks.ts b/user/src/components/Applications/Group/GroupForm/hooks.ts index a4fe0dd11..02da15522 100644 --- a/user/src/components/Applications/Group/GroupForm/hooks.ts +++ b/user/src/components/Applications/Group/GroupForm/hooks.ts @@ -9,6 +9,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useTranslation } from 'next-i18next'; import { useForm } from 'react-hook-form'; import { toast } from 'react-toastify'; +import { groupLabels } from '../../label'; import { GroupForm, groupSchema } from './schema'; export const useGroupFormHooks = ( @@ -46,6 +47,46 @@ export const useGroupFormHooks = ( // フォームをリアルタイム監視 const values = watch(); + const groupFormTexts = { + fields: { + name: t(groupLabels[0]), + projectName: t(groupLabels[1]), + isInternational: t(groupLabels[2]), + isExternal: t(groupLabels[3]), + groupCategory: t(groupLabels[4]), + activity: t(groupLabels[5]), + }, + notes: { + name: t('applications.group.notes.name'), + projectName: t('applications.group.notes.projectName'), + international: t('applications.group.notes.international'), + external: t('applications.group.notes.external'), + groupCategory: t('applications.group.notes.groupCategory'), + activity: t('applications.group.notes.activity'), + }, + options: { + international: [ + { id: 0, name: t('applications.group.options.international.no') }, + { id: 1, name: t('applications.group.options.international.yes') }, + ], + external: [ + { id: 0, name: t('applications.group.options.external.no') }, + { id: 1, name: t('applications.group.options.external.yes') }, + ], + }, + buttons: { + cancel: t('form.actions.cancel'), + edit: t('form.actions.edit'), + register: t('form.actions.register'), + }, + messages: { + registerFailed: t('form.messages.registerFailed'), + registerSuccess: t('form.messages.registerSuccess'), + updateSuccess: t('form.messages.updateSuccess'), + updateFailed: t('form.messages.updateFailed'), + }, + }; + // 団体カテゴリーが「実行委員」の場合は,committeeを1にする useEffect(() => { if (values.groupCategoryId === GROUP_CATEGORY.COMMITTEE) { @@ -74,15 +115,23 @@ export const useGroupFormHooks = ( isMutating: updateIsMutating, } = useUpdateGroups(groups?.id ?? 0); + const registerFailedMessage = groupFormTexts.messages.registerFailed; + + useEffect(() => { + if (createError || updateError) { + toast.error(registerFailedMessage); + } + }, [createError, updateError, registerFailedMessage]); + const onSubmit = async (formData: GroupForm) => { // 既存の団体申請がある場合は更新 if (groups) { try { await update({ query: formData }); mutateGroups(); - toast.success(t('form.messages.updateSuccess')); + toast.success(groupFormTexts.messages.updateSuccess); } catch { - toast.error(t('form.messages.updateFailed')); + toast.error(groupFormTexts.messages.updateFailed); } // 団体申請がない場合は新規作成 } else { @@ -91,9 +140,9 @@ export const useGroupFormHooks = ( mutateGroups(); mutateCheckAllRegisteredGroups(); mutateGroupByUserId(); - toast.success(t('form.messages.registerSuccess')); + toast.success(groupFormTexts.messages.registerSuccess); } catch { - toast.error(t('form.messages.registerFailed')); + toast.error(groupFormTexts.messages.registerFailed); } reset(); } @@ -120,12 +169,11 @@ export const useGroupFormHooks = ( errors, onSubmit, setValue, - createError, createIsMutating, - updateError, updateIsMutating, formatRadioValue, validateEdit, values, + groupFormTexts, }; }; diff --git a/user/src/components/Applications/Group/hooks.ts b/user/src/components/Applications/Group/hooks.ts index cb88f4dd1..80b59b104 100644 --- a/user/src/components/Applications/Group/hooks.ts +++ b/user/src/components/Applications/Group/hooks.ts @@ -8,31 +8,43 @@ export const useGroupHooks = (groupId: number) => { const { t } = useTranslation('common'); const { groups, isLoading, hasError, mutateGroups } = useGetGroups(groupId); const { groupCategories } = useGetGroupCategories(); + const groupTexts = { + title: t('applications.group.title'), + loading: t('applications.group.loading'), + errors: { + fetch: t('applications.group.errors.fetch'), + }, + boolean: { + yes: t('applications.group.boolean.yes'), + no: t('applications.group.boolean.no'), + }, + summaryLabels: groupLabels.map((labelKey) => t(labelKey)), + }; // 団体申請のフォーム内容 const formItem: FormItem[] = [ { - label: t(groupLabels[0]), + label: groupTexts.summaryLabels[0], content: groups?.name, }, { - label: t(groupLabels[1]), + label: groupTexts.summaryLabels[1], content: groups?.projectName, }, { - label: t(groupLabels[2]), + label: groupTexts.summaryLabels[2], content: groups?.isInternational - ? t('applications.group.boolean.yes') - : t('applications.group.boolean.no'), + ? groupTexts.boolean.yes + : groupTexts.boolean.no, }, { - label: t(groupLabels[3]), + label: groupTexts.summaryLabels[3], content: groups?.isExternal - ? t('applications.group.boolean.yes') - : t('applications.group.boolean.no'), + ? groupTexts.boolean.yes + : groupTexts.boolean.no, }, { - label: t(groupLabels[4]), + label: groupTexts.summaryLabels[4], content: groups?.groupCategoryId ? groupCategories?.find( (category) => category.id === groups.groupCategoryId @@ -40,7 +52,7 @@ export const useGroupHooks = (groupId: number) => { : '', }, { - label: t(groupLabels[5]), + label: groupTexts.summaryLabels[5], content: groups?.activity, }, ]; @@ -68,5 +80,6 @@ export const useGroupHooks = (groupId: number) => { formItem, groupCategories, mutateGroups, + groupTexts, }; }; From 94d8775434d1bb8867916a6bb2598b08391a82ec Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Fri, 9 Jan 2026 15:32:39 +0900 Subject: [PATCH 44/57] =?UTF-8?q?[feat]=20RentItems=E3=82=B3=E3=83=B3?= =?UTF-8?q?=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=8A=E3=82=88?= =?UTF-8?q?=E3=81=B3=E9=96=A2=E9=80=A3=E3=83=95=E3=82=A9=E3=83=BC=E3=83=A0?= =?UTF-8?q?=E3=81=ABi18next=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=97=E3=81=9F?= =?UTF-8?q?=E7=BF=BB=E8=A8=B3=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=97=E3=80=81=E3=83=86=E3=82=AD=E3=82=B9=E3=83=88=E3=82=92?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=A9=E3=82=A4=E3=82=BA=E3=80=82?= =?UTF-8?q?=E3=83=95=E3=83=83=E3=82=AF=E3=81=8B=E3=82=89=E3=81=AE=E3=83=86?= =?UTF-8?q?=E3=82=AD=E3=82=B9=E3=83=88=E5=8F=96=E5=BE=97=E3=82=92=E7=B5=B1?= =?UTF-8?q?=E4=B8=80=E3=81=97=E3=80=81=E3=82=B3=E3=83=BC=E3=83=89=E3=81=AE?= =?UTF-8?q?=E5=8F=AF=E8=AA=AD=E6=80=A7=E3=82=92=E5=90=91=E4=B8=8A=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MultiItemForms/RentItems/RentItems.tsx | 6 +- .../RentItems/RentItemsForm/RentItemsForm.tsx | 94 +++++++++---------- .../MultiItemForms/RentItems/hooks/index.ts | 3 +- .../hooks/useRentItemsAccordionHooks.ts | 11 +++ ...sFormLogic.ts => useRentItemsFormHooks.ts} | 69 +++++++++++++- 5 files changed, 125 insertions(+), 58 deletions(-) create mode 100644 user/src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsAccordionHooks.ts rename user/src/components/Applications/MultiItemForms/RentItems/hooks/{useRentItemsFormLogic.ts => useRentItemsFormHooks.ts} (91%) diff --git a/user/src/components/Applications/MultiItemForms/RentItems/RentItems.tsx b/user/src/components/Applications/MultiItemForms/RentItems/RentItems.tsx index 1334084d2..3d1acf0a1 100644 --- a/user/src/components/Applications/MultiItemForms/RentItems/RentItems.tsx +++ b/user/src/components/Applications/MultiItemForms/RentItems/RentItems.tsx @@ -1,7 +1,7 @@ import { FC } from 'react'; -import { useTranslation } from 'next-i18next'; import AccordionMenu from '@/components/AccordionMenu'; import RentItemsForm from '@/components/Applications/MultiItemForms/RentItems/RentItemsForm'; +import { useRentItemsAccordionHooks } from '@/components/Applications/MultiItemForms/RentItems/hooks'; type RentItemsProps = { isDeadline: boolean | undefined; @@ -16,10 +16,10 @@ const RentItems: FC = ({ groupId, groupCategoryId, }) => { - const { t } = useTranslation('common'); + const { rentItemsAccordionTexts } = useRentItemsAccordionHooks(); return ( = ({ groupCategoryId, isDeadline, }) => { - const { t } = useTranslation('common'); - // 主に groupCategoryId を使って判断するように変更 const { form, fields, @@ -47,12 +44,13 @@ const RentItemsForm: FC = ({ hideLocationTypeSelect, // 会場タイプ選択を非表示にするフラグ isFoodSellingGroup, // 食品販売団体かどうかのフラグ getMaxCountByItemId, // 物品ID別の最大個数を取得する関数 - } = useRentItemsFormLogic(groupId, groupCategoryId); + rentItemsFormTexts, + } = useRentItemsFormHooks(groupId, groupCategoryId); if (isLoading) { return (
-

{t('applications.rentItems.loading')}

+

{rentItemsFormTexts.general.loading}

); } @@ -61,10 +59,10 @@ const RentItemsForm: FC = ({ return (
- {t('applications.rentItems.errors.fetchTitle')} + {rentItemsFormTexts.errors.fetch.title} - {t('applications.rentItems.errors.fetchDescription')} + {rentItemsFormTexts.errors.fetch.description}
); @@ -77,11 +75,9 @@ const RentItemsForm: FC = ({

- {t('applications.rentItems.summary.noApplication.label')} -

-

- {t('applications.rentItems.summary.noApplication.description')} + {rentItemsFormTexts.summary.noApplication.label}

+

{rentItemsFormTexts.summary.noApplication.description}

{isDeadline && (
@@ -92,7 +88,7 @@ const RentItemsForm: FC = ({ icon="pencil" onClick={openEditMode} > - {t('form.actions.edit')} + {rentItemsFormTexts.buttons.edit}
)} @@ -108,17 +104,17 @@ const RentItemsForm: FC = ({ {!hideLocationTypeSelect && ( // 特殊団体でない場合のみ表示

- {t('applications.rentItems.location.displayLabel')} + {rentItemsFormTexts.location.displayLabel} {form.getValues('locationType') === LOCATION_TYPES.INDOOR - ? t('applications.rentItems.location.options.indoor') - : t('applications.rentItems.location.options.outdoor')} + ? rentItemsFormTexts.location.options.indoor + : rentItemsFormTexts.location.options.outdoor}

)} {isFoodSellingGroup && (

- {t('applications.rentItems.location.notes.foodOnlyOutdoor')} + {rentItemsFormTexts.location.notes.foodOnlyOutdoor}

)} @@ -139,9 +135,9 @@ const RentItemsForm: FC = ({ )?.name || ''}

- {t('applications.rentItems.summary.count', { - value: form.getValues(`items.${index}.count`), - })} + {rentItemsFormTexts.summary.count( + form.getValues(`items.${index}.count`) + )}

@@ -158,7 +154,7 @@ const RentItemsForm: FC = ({ icon="pencil" onClick={openEditMode} > - {t('form.actions.edit')} + {rentItemsFormTexts.buttons.edit}
)} @@ -175,10 +171,10 @@ const RentItemsForm: FC = ({
{!hideLocationTypeSelect && ( -

{t('applications.rentItems.location.notes.preApplication')}

+

{rentItemsFormTexts.location.notes.preApplication}

)} {isFoodSellingGroup && ( -

{t('applications.rentItems.location.notes.foodOnlyOutdoor')}

+

{rentItemsFormTexts.location.notes.foodOnlyOutdoor}

)}

@@ -187,7 +183,7 @@ const RentItemsForm: FC = ({ control={control} render={({ field }) => ( { field.onChange(value === '1'); @@ -197,11 +193,11 @@ const RentItemsForm: FC = ({ options={[ { id: 1, - name: t('applications.rentItems.radio.options.yes'), + name: rentItemsFormTexts.radio.options.yes, }, { id: 0, - name: t('applications.rentItems.radio.options.no'), + name: rentItemsFormTexts.radio.options.no, }, ]} error={errors.hasItems?.message?.toString()} @@ -225,7 +221,7 @@ const RentItemsForm: FC = ({ } }} > - {t('form.actions.register')} + {rentItemsFormTexts.buttons.register}
@@ -241,7 +237,7 @@ const RentItemsForm: FC = ({ control={control} render={({ field }) => ( { // 新しい値でupdateLocationTypeを呼び出す @@ -251,15 +247,11 @@ const RentItemsForm: FC = ({ options={[ { id: 1, - name: t( - 'applications.rentItems.location.options.indoor' - ), + name: rentItemsFormTexts.location.options.indoor, }, { id: 2, - name: t( - 'applications.rentItems.location.options.outdoor' - ), + name: rentItemsFormTexts.location.options.outdoor, }, ]} error={errors.locationType?.message} @@ -273,9 +265,7 @@ const RentItemsForm: FC = ({

- {t('applications.rentItems.fields.section', { - index: index + 1, - })} + {rentItemsFormTexts.fields.sectionTitle(index + 1)}

= ({ return ( { field.onChange(value); @@ -336,7 +326,7 @@ const RentItemsForm: FC = ({ return ( { const numValue = parseInt(value, 10); @@ -367,14 +357,14 @@ const RentItemsForm: FC = ({ }} />

- {t('applications.rentItems.notes.minRequest')} + {rentItemsFormTexts.notes.minRequest}

- {t('applications.rentItems.notes.contactLimit')} + {rentItemsFormTexts.notes.contactLimit}
- {t('applications.rentItems.notes.contactEmail')} + {rentItemsFormTexts.notes.contactEmail}

{fields.length > 1 && ( @@ -389,7 +379,7 @@ const RentItemsForm: FC = ({ >
- {t('form.actions.delete')} + {rentItemsFormTexts.buttons.delete}
@@ -407,18 +397,18 @@ const RentItemsForm: FC = ({ {errors.root?.message && (
- {t(errors.root.message.toString(), { - defaultValue: errors.root.message.toString(), - })} + {rentItemsFormTexts.errors.translate( + errors.root.message.toString() + )}
)} {/* フォームバリデーションエラーがあれば表示(アイテム制限関連のエラーも含む) */} {errors.items?.message && (
- {t(errors.items.message.toString(), { - defaultValue: errors.items.message.toString(), - })} + {rentItemsFormTexts.errors.translate( + errors.items.message.toString() + )}
)} @@ -433,13 +423,13 @@ const RentItemsForm: FC = ({ >
+{' '} - {t('applications.rentItems.buttons.addItem')} + {rentItemsFormTexts.buttons.addItem}
diff --git a/user/src/components/Applications/MultiItemForms/RentItems/hooks/index.ts b/user/src/components/Applications/MultiItemForms/RentItems/hooks/index.ts index b63b904cf..41d31f895 100644 --- a/user/src/components/Applications/MultiItemForms/RentItems/hooks/index.ts +++ b/user/src/components/Applications/MultiItemForms/RentItems/hooks/index.ts @@ -1 +1,2 @@ -export * from './useRentItemsFormLogic'; +export * from './useRentItemsAccordionHooks'; +export * from './useRentItemsFormHooks'; diff --git a/user/src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsAccordionHooks.ts b/user/src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsAccordionHooks.ts new file mode 100644 index 000000000..67c3ddf7c --- /dev/null +++ b/user/src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsAccordionHooks.ts @@ -0,0 +1,11 @@ +import { useTranslation } from 'next-i18next'; + +export const useRentItemsAccordionHooks = () => { + const { t } = useTranslation('common'); + + const rentItemsAccordionTexts = { + title: t('applications.rentItems.title'), + }; + + return { rentItemsAccordionTexts }; +}; diff --git a/user/src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsFormLogic.ts b/user/src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsFormHooks.ts similarity index 91% rename from user/src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsFormLogic.ts rename to user/src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsFormHooks.ts index 29a870bac..217ac4579 100644 --- a/user/src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsFormLogic.ts +++ b/user/src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsFormHooks.ts @@ -1,4 +1,4 @@ -// src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsFormLogic.ts +// src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsFormHooks.ts import { FormEvent, useEffect, useMemo, useRef, useState } from 'react'; import { ORDER_TYPES, @@ -64,7 +64,7 @@ const TABLE_CHAIR_MAX_COUNT = { [LOCATION_TYPES.OUTDOOR]: 20, // 屋外: 20個 }; -export const useRentItemsFormLogic = ( +export const useRentItemsFormHooks = ( groupId: number, groupCategoryId?: number // 団体カテゴリID ) => { @@ -719,6 +719,70 @@ export const useRentItemsFormLogic = ( } }; + const rentItemsFormTexts = { + general: { + loading: t('applications.rentItems.loading'), + }, + errors: { + fetch: { + title: t('applications.rentItems.errors.fetchTitle'), + description: t('applications.rentItems.errors.fetchDescription'), + }, + translate: (key?: string) => + key ? t(key, { defaultValue: key }) : undefined, + }, + summary: { + noApplication: { + label: t('applications.rentItems.summary.noApplication.label'), + description: t( + 'applications.rentItems.summary.noApplication.description' + ), + }, + count: (value: number | string) => + t('applications.rentItems.summary.count', { value }), + }, + location: { + displayLabel: t('applications.rentItems.location.displayLabel'), + radioQuestion: t('applications.rentItems.location.radioQuestion'), + notes: { + foodOnlyOutdoor: t( + 'applications.rentItems.location.notes.foodOnlyOutdoor' + ), + preApplication: t( + 'applications.rentItems.location.notes.preApplication' + ), + }, + options: { + indoor: t('applications.rentItems.location.options.indoor'), + outdoor: t('applications.rentItems.location.options.outdoor'), + }, + }, + radio: { + question: t('applications.rentItems.radio.question'), + options: { + yes: t('applications.rentItems.radio.options.yes'), + no: t('applications.rentItems.radio.options.no'), + }, + }, + fields: { + sectionTitle: (index: number) => + t('applications.rentItems.fields.section', { index }), + item: t('applications.rentItems.fields.item'), + count: t('applications.rentItems.fields.count'), + }, + notes: { + minRequest: t('applications.rentItems.notes.minRequest'), + contactLimit: t('applications.rentItems.notes.contactLimit'), + contactEmail: t('applications.rentItems.notes.contactEmail'), + }, + buttons: { + edit: t('form.actions.edit'), + register: t('form.actions.register'), + delete: t('form.actions.delete'), + addItem: t('applications.rentItems.buttons.addItem'), + }, + }; + return { form, fields, @@ -744,5 +808,6 @@ export const useRentItemsFormLogic = ( hideLocationTypeSelect, // 団体タイプに応じたUI表示制御フラグ isFoodSellingGroup, // 食品販売団体かどうかのフラグ getMaxCountByItemId, // 物品IDに基づいて最大個数を取得する関数 + rentItemsFormTexts, }; }; From f3edb9f3bf6d59d5965d137d101170eeac31647d Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Fri, 9 Jan 2026 15:32:48 +0900 Subject: [PATCH 45/57] =?UTF-8?q?[feat]=20Power=E3=82=B3=E3=83=B3=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=8A=E3=82=88=E3=81=B3?= =?UTF-8?q?=E9=96=A2=E9=80=A3=E3=83=95=E3=82=A9=E3=83=BC=E3=83=A0=E3=81=AB?= =?UTF-8?q?i18next=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=97=E3=81=9F=E7=BF=BB?= =?UTF-8?q?=E8=A8=B3=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97?= =?UTF-8?q?=E3=80=81=E3=83=86=E3=82=AD=E3=82=B9=E3=83=88=E3=82=92=E3=83=AD?= =?UTF-8?q?=E3=83=BC=E3=82=AB=E3=83=A9=E3=82=A4=E3=82=BA=E3=80=82=E3=82=AB?= =?UTF-8?q?=E3=82=B9=E3=82=BF=E3=83=A0=E3=83=95=E3=83=83=E3=82=AF=E3=82=92?= =?UTF-8?q?=E5=B0=8E=E5=85=A5=E3=81=97=E3=81=A6=E3=83=86=E3=82=AD=E3=82=B9?= =?UTF-8?q?=E3=83=88=E5=8F=96=E5=BE=97=E3=82=92=E7=B5=B1=E4=B8=80=E3=81=97?= =?UTF-8?q?=E3=80=81=E3=82=B3=E3=83=BC=E3=83=89=E3=81=AE=E5=8F=AF=E8=AA=AD?= =?UTF-8?q?=E6=80=A7=E3=82=92=E5=90=91=E4=B8=8A=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Applications/Power/Power.tsx | 6 +-- .../Power/components/PowerForm.tsx | 30 +++++------ .../Power/components/PowerFormView.tsx | 30 ++++------- .../Power/components/PowerNegativeView.tsx | 33 ++++-------- .../Power/components/PowerSummaryView.tsx | 46 +++-------------- .../Applications/Power/hooks/index.ts | 5 ++ .../Power/hooks/usePowerAccordionHooks.ts | 13 +++++ .../Power/hooks/usePowerDeviceFormHooks.ts | 30 +++++++++++ .../Power/hooks/usePowerFormViewHooks.ts | 28 ++++++++++ .../Power/hooks/usePowerNegativeViewHooks.ts | 36 +++++++++++++ .../Power/hooks/usePowerSummaryViewHooks.ts | 51 +++++++++++++++++++ 11 files changed, 204 insertions(+), 104 deletions(-) create mode 100644 user/src/components/Applications/Power/hooks/usePowerAccordionHooks.ts create mode 100644 user/src/components/Applications/Power/hooks/usePowerDeviceFormHooks.ts create mode 100644 user/src/components/Applications/Power/hooks/usePowerFormViewHooks.ts create mode 100644 user/src/components/Applications/Power/hooks/usePowerNegativeViewHooks.ts create mode 100644 user/src/components/Applications/Power/hooks/usePowerSummaryViewHooks.ts diff --git a/user/src/components/Applications/Power/Power.tsx b/user/src/components/Applications/Power/Power.tsx index 98055d60f..74d750cd3 100644 --- a/user/src/components/Applications/Power/Power.tsx +++ b/user/src/components/Applications/Power/Power.tsx @@ -1,9 +1,9 @@ import { FC } from 'react'; -import { useTranslation } from 'next-i18next'; import AccordionMenu from '@/components/AccordionMenu'; import { PowerNegativeView, PowerSummaryView } from './components'; import { PowerFormView } from './components/PowerFormView'; import { RADIO_OPTIONS } from './constants'; +import { usePowerAccordionHooks } from './hooks/usePowerAccordionHooks'; import { usePowerApplication } from './hooks/usePowerApplication'; import { usePowerDisplay } from './hooks/usePowerDisplay'; @@ -14,7 +14,7 @@ type PowerProps = { }; const Power: FC = ({ isDeadline, isRegistered, groupId }) => { - const { t } = useTranslation('common'); + const powerAccordionHooks = usePowerAccordionHooks(); // 電力申請のカスタムフックから状態とロジックの取得 const { @@ -145,7 +145,7 @@ const Power: FC = ({ isDeadline, isRegistered, groupId }) => { return ( = ({ const PowerForm: FC = ({ index, form, onRemove }) => { const { control, formState } = form; - const { t } = useTranslation('common'); + const { powerDeviceFormTexts } = usePowerDeviceFormHooks(); // エラーメッセージを取得 - DeviceField型を受け取るように修正 const getErrorMessage = (name: DeviceField) => { @@ -65,7 +65,7 @@ const PowerForm: FC = ({ index, form, onRemove }) => {
= ({ index, form, onRemove }) => { = ({ index, form, onRemove }) => { = ({ index, form, onRemove }) => {
-

- {t('applications.power.form.notes.emailWarning', { - limit: POWER_LIMIT, - })} -

-

{t('applications.power.form.notes.contactEmail')}

+

{powerDeviceFormTexts.notes.emailWarning(POWER_LIMIT)}

+

{powerDeviceFormTexts.notes.contactEmail}

{index > 0 && !form.getValues().devices[index]?.productName && ( @@ -132,7 +126,7 @@ const PowerForm: FC = ({ index, form, onRemove }) => { variant onClick={() => onRemove(index)} > - {t('form.actions.delete')} + {powerDeviceFormTexts.actions.delete}
)} diff --git a/user/src/components/Applications/Power/components/PowerFormView.tsx b/user/src/components/Applications/Power/components/PowerFormView.tsx index 42be31b34..b00c1efd4 100644 --- a/user/src/components/Applications/Power/components/PowerFormView.tsx +++ b/user/src/components/Applications/Power/components/PowerFormView.tsx @@ -1,8 +1,8 @@ -import { FC, useMemo } from 'react'; -import { useTranslation } from 'next-i18next'; +import { FC } from 'react'; import Button from '@/components/Button/Button'; import Radio from '@/components/Form/Radio/Radio'; import { POWER_LIMIT } from '../constants'; +import { usePowerFormViewHooks } from '../hooks/usePowerFormViewHooks'; import { PowerFormViewProps } from '../types'; import PowerForm from './PowerForm'; @@ -20,25 +20,17 @@ export const PowerFormView: FC = ({ onSubmit, }) => { const { handleSubmit } = formMethods; - const { t } = useTranslation('common'); - const translatedRadioOptions = useMemo( - () => - radioOptions.map((option) => ({ - id: option.id, - name: t(option.labelKey), - })), - [radioOptions, t] - ); + const { powerFormViewTexts } = usePowerFormViewHooks(radioOptions); return (
{/* ラジオボタン */} {/* 申請する場合のフォーム */} @@ -60,10 +52,10 @@ export const PowerFormView: FC = ({ {totalPower > POWER_LIMIT && (

- {t('applications.power.form.totalPowerWarning', { - limit: POWER_LIMIT, - value: totalPower, - })} + {powerFormViewTexts.warnings.totalPower( + POWER_LIMIT, + totalPower + )}

)} @@ -77,7 +69,7 @@ export const PowerFormView: FC = ({ variant onClick={onAddDevice} > - {t('applications.power.form.addDevice')} + {powerFormViewTexts.actions.addDevice}
diff --git a/user/src/components/Applications/Power/components/PowerNegativeView.tsx b/user/src/components/Applications/Power/components/PowerNegativeView.tsx index 0caed0e06..5acfa1b1d 100644 --- a/user/src/components/Applications/Power/components/PowerNegativeView.tsx +++ b/user/src/components/Applications/Power/components/PowerNegativeView.tsx @@ -1,9 +1,8 @@ -import { FC, useMemo } from 'react'; -import { useTranslation } from 'next-i18next'; +import { FC } from 'react'; import Button from '@/components/Button/Button'; import Radio from '@/components/Form/Radio/Radio'; import FormList from '@/components/FormList/FormList'; -import { FormItem } from '@/components/FormList/type'; +import { usePowerNegativeViewHooks } from '../hooks/usePowerNegativeViewHooks'; import { PowerNegativeViewProps } from '../types'; export const PowerNegativeView: FC = ({ @@ -19,32 +18,18 @@ export const PowerNegativeView: FC = ({ onCancel, isDeadline, }) => { - const { t } = useTranslation('common'); - const translatedRadioOptions = useMemo( - () => - radioOptions.map((option) => ({ - id: option.id, - name: t(option.labelKey), - })), - [radioOptions, t] - ); - const noApplicationItems: FormItem[] = [ - { - label: t('applications.power.summary.noApplication.label'), - content: t('applications.power.summary.noApplication.description'), - }, - ]; + const { powerNegativeViewTexts } = usePowerNegativeViewHooks(radioOptions); return (
{isEdit && ( <> {onCancel && (
@@ -55,7 +40,7 @@ export const PowerNegativeView: FC = ({ variant onClick={onCancel} > - {t('form.actions.cancel')} + {powerNegativeViewTexts.actions.cancel}
)} @@ -64,7 +49,7 @@ export const PowerNegativeView: FC = ({ {!isEdit && ( @@ -75,7 +60,7 @@ export const PowerNegativeView: FC = ({ {submitError && (
- {t('applications.power.errors.submitTitle')} + {powerNegativeViewTexts.errors.submitTitle} {submitError}
@@ -86,7 +71,7 @@ export const PowerNegativeView: FC = ({ color="main" onClick={onNegativeSubmit} > - {t('form.actions.register')} + {powerNegativeViewTexts.actions.register}
)} diff --git a/user/src/components/Applications/Power/components/PowerSummaryView.tsx b/user/src/components/Applications/Power/components/PowerSummaryView.tsx index e4cca3999..ba3c7f7d6 100644 --- a/user/src/components/Applications/Power/components/PowerSummaryView.tsx +++ b/user/src/components/Applications/Power/components/PowerSummaryView.tsx @@ -1,43 +1,8 @@ import { FC } from 'react'; -import { TFunction } from 'i18next'; -import { useTranslation } from 'next-i18next'; import Button from '@/components/Button/Button'; import FormList from '@/components/FormList/FormList'; -import { FormItem } from '@/components/FormList/type'; -import { Device, PowerSummaryViewProps } from '../types'; - -// デバイス情報からフォームアイテムを作成する関数 -export const createFormItemsForDevice = ( - device: Device, - t: TFunction<'common'> -): FormItem[] => { - const items: FormItem[] = []; - items.push({ - label: t('applications.power.summary.fields.productName'), - content: device.productName, - }); - items.push({ - label: t('applications.power.summary.fields.manufacturer'), - content: device.manufacturer, - }); - items.push({ - label: t('applications.power.summary.fields.model'), - content: device.model, - }); - if (device.url) { - items.push({ - label: t('applications.power.summary.fields.url'), - content: device.url, - }); - } - items.push({ - label: t('applications.power.summary.fields.maxPower'), - content: t('applications.power.summary.powerValue', { - value: device.maxPower, - }), - }); - return items; -}; +import { usePowerSummaryViewHooks } from '../hooks/usePowerSummaryViewHooks'; +import { PowerSummaryViewProps } from '../types'; export const PowerSummaryView: FC = ({ devices, @@ -45,14 +10,15 @@ export const PowerSummaryView: FC = ({ onDeleteDevice, isDeadline, }) => { - const { t } = useTranslation('common'); + const { createSummaryItemsForDevice, powerSummaryViewTexts } = + usePowerSummaryViewHooks(); return (
{devices.map((device, index) => (
onDeleteDevice(device.id!) : undefined} @@ -69,7 +35,7 @@ export const PowerSummaryView: FC = ({ icon="pencil" onClick={onEdit} > - {t('form.actions.edit')} + {powerSummaryViewTexts.actions.edit}
)} diff --git a/user/src/components/Applications/Power/hooks/index.ts b/user/src/components/Applications/Power/hooks/index.ts index 5dff57c56..fe06a4166 100644 --- a/user/src/components/Applications/Power/hooks/index.ts +++ b/user/src/components/Applications/Power/hooks/index.ts @@ -1,2 +1,7 @@ export * from './usePowerApplication'; export * from './usePowerForm'; +export * from './usePowerAccordionHooks'; +export * from './usePowerNegativeViewHooks'; +export * from './usePowerSummaryViewHooks'; +export * from './usePowerFormViewHooks'; +export * from './usePowerDeviceFormHooks'; diff --git a/user/src/components/Applications/Power/hooks/usePowerAccordionHooks.ts b/user/src/components/Applications/Power/hooks/usePowerAccordionHooks.ts new file mode 100644 index 000000000..32e43527c --- /dev/null +++ b/user/src/components/Applications/Power/hooks/usePowerAccordionHooks.ts @@ -0,0 +1,13 @@ +import { useTranslation } from 'next-i18next'; + +export const usePowerAccordionHooks = () => { + const { t } = useTranslation('common'); + + const powerAccordionTexts = { + title: t('applications.power.title'), + }; + + return { + powerAccordionTexts, + }; +}; diff --git a/user/src/components/Applications/Power/hooks/usePowerDeviceFormHooks.ts b/user/src/components/Applications/Power/hooks/usePowerDeviceFormHooks.ts new file mode 100644 index 000000000..5e4176ad1 --- /dev/null +++ b/user/src/components/Applications/Power/hooks/usePowerDeviceFormHooks.ts @@ -0,0 +1,30 @@ +import { useTranslation } from 'next-i18next'; + +export const usePowerDeviceFormHooks = () => { + const { t } = useTranslation('common'); + + const powerDeviceFormTexts = { + fields: { + productName: t('applications.power.form.fields.productName'), + manufacturer: t('applications.power.form.fields.manufacturer'), + model: t('applications.power.form.fields.model'), + url: t('applications.power.form.fields.url'), + maxPower: t('applications.power.form.fields.maxPower'), + }, + notes: { + url: t('applications.power.form.notes.url'), + totalPower: (limit: number) => + t('applications.power.form.notes.totalPower', { limit }), + emailWarning: (limit: number) => + t('applications.power.form.notes.emailWarning', { limit }), + contactEmail: t('applications.power.form.notes.contactEmail'), + }, + actions: { + delete: t('form.actions.delete'), + }, + }; + + return { + powerDeviceFormTexts, + }; +}; diff --git a/user/src/components/Applications/Power/hooks/usePowerFormViewHooks.ts b/user/src/components/Applications/Power/hooks/usePowerFormViewHooks.ts new file mode 100644 index 000000000..c46bff114 --- /dev/null +++ b/user/src/components/Applications/Power/hooks/usePowerFormViewHooks.ts @@ -0,0 +1,28 @@ +import { useTranslation } from 'next-i18next'; +import { RadioOption } from '../types'; + +export const usePowerFormViewHooks = (radioOptions: RadioOption[]) => { + const { t } = useTranslation('common'); + + const powerFormViewTexts = { + radio: { + label: t('applications.power.radio.question'), + options: radioOptions.map((option) => ({ + id: option.id, + name: t(option.labelKey), + })), + }, + warnings: { + totalPower: (limit: number, value: number) => + t('applications.power.form.totalPowerWarning', { limit, value }), + }, + actions: { + addDevice: t('applications.power.form.addDevice'), + register: t('form.actions.register'), + }, + }; + + return { + powerFormViewTexts, + }; +}; diff --git a/user/src/components/Applications/Power/hooks/usePowerNegativeViewHooks.ts b/user/src/components/Applications/Power/hooks/usePowerNegativeViewHooks.ts new file mode 100644 index 000000000..ce5153b47 --- /dev/null +++ b/user/src/components/Applications/Power/hooks/usePowerNegativeViewHooks.ts @@ -0,0 +1,36 @@ +import { useTranslation } from 'next-i18next'; +import { FormItem } from '@/components/FormList/type'; +import { RadioOption } from '../types'; + +export const usePowerNegativeViewHooks = (radioOptions: RadioOption[]) => { + const { t } = useTranslation('common'); + + const powerNegativeViewTexts = { + radio: { + label: t('applications.power.radio.question'), + options: radioOptions.map((option) => ({ + id: option.id, + name: t(option.labelKey), + })), + }, + summary: { + noApplicationItems: [ + { + label: t('applications.power.summary.noApplication.label'), + content: t('applications.power.summary.noApplication.description'), + }, + ] as FormItem[], + }, + errors: { + submitTitle: t('applications.power.errors.submitTitle'), + }, + actions: { + cancel: t('form.actions.cancel'), + register: t('form.actions.register'), + }, + }; + + return { + powerNegativeViewTexts, + }; +}; diff --git a/user/src/components/Applications/Power/hooks/usePowerSummaryViewHooks.ts b/user/src/components/Applications/Power/hooks/usePowerSummaryViewHooks.ts new file mode 100644 index 000000000..d7463a8b0 --- /dev/null +++ b/user/src/components/Applications/Power/hooks/usePowerSummaryViewHooks.ts @@ -0,0 +1,51 @@ +import { useTranslation } from 'next-i18next'; +import { FormItem } from '@/components/FormList/type'; +import { Device } from '../types'; + +export const usePowerSummaryViewHooks = () => { + const { t } = useTranslation('common'); + + const createSummaryItemsForDevice = (device: Device): FormItem[] => { + const items: FormItem[] = [ + { + label: t('applications.power.summary.fields.productName'), + content: device.productName, + }, + { + label: t('applications.power.summary.fields.manufacturer'), + content: device.manufacturer, + }, + { + label: t('applications.power.summary.fields.model'), + content: device.model, + }, + ]; + + if (device.url) { + items.push({ + label: t('applications.power.summary.fields.url'), + content: device.url, + }); + } + + items.push({ + label: t('applications.power.summary.fields.maxPower'), + content: t('applications.power.summary.powerValue', { + value: device.maxPower, + }), + }); + + return items; + }; + + const powerSummaryViewTexts = { + actions: { + edit: t('form.actions.edit'), + }, + }; + + return { + createSummaryItemsForDevice, + powerSummaryViewTexts, + }; +}; From 5ed7ef674cb4820040aff5c7f0021535628209e9 Mon Sep 17 00:00:00 2001 From: hikahana <22.h.hanada.nutfes@gmail.com> Date: Fri, 9 Jan 2026 15:32:57 +0900 Subject: [PATCH 46/57] =?UTF-8?q?[feat]=20PublicRelations=E3=82=B3?= =?UTF-8?q?=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3=E3=83=88=E3=81=8A?= =?UTF-8?q?=E3=82=88=E3=81=B3=E9=96=A2=E9=80=A3=E3=83=95=E3=82=A9=E3=83=BC?= =?UTF-8?q?=E3=83=A0=E3=81=ABi18next=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=97?= =?UTF-8?q?=E3=81=9F=E7=BF=BB=E8=A8=B3=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E3=81=97=E3=80=81=E3=83=86=E3=82=AD=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=A9=E3=82=A4=E3=82=BA?= =?UTF-8?q?=E3=80=82=E3=82=AB=E3=82=B9=E3=82=BF=E3=83=A0=E3=83=95=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=81=8B=E3=82=89=E3=81=AE=E3=83=86=E3=82=AD=E3=82=B9?= =?UTF-8?q?=E3=83=88=E5=8F=96=E5=BE=97=E3=82=92=E7=B5=B1=E4=B8=80=E3=81=97?= =?UTF-8?q?=E3=80=81=E3=82=B3=E3=83=BC=E3=83=89=E3=81=AE=E5=8F=AF=E8=AA=AD?= =?UTF-8?q?=E6=80=A7=E3=82=92=E5=90=91=E4=B8=8A=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PublicRelations/PublicRelations.tsx | 25 ++-- .../PublicRelationsForm.tsx | 33 ++---- .../PublicRelationsForm/hooks.ts | 108 +++++++++++++----- .../Applications/PublicRelations/hooks.ts | 37 ++++-- 4 files changed, 137 insertions(+), 66 deletions(-) diff --git a/user/src/components/Applications/PublicRelations/PublicRelations.tsx b/user/src/components/Applications/PublicRelations/PublicRelations.tsx index 4139b9439..cb68d1ddc 100644 --- a/user/src/components/Applications/PublicRelations/PublicRelations.tsx +++ b/user/src/components/Applications/PublicRelations/PublicRelations.tsx @@ -1,6 +1,5 @@ import { FC } from 'react'; import { PublicRelationResponse } from '@/api/publicRelationsApi'; -import { useTranslation } from 'next-i18next'; import AccordionMenu from '@/components/AccordionMenu/AccordionMenu'; import PublicRelationsForm from '@/components/Applications/PublicRelations/PublicRelationsForm/PublicRelationsForm'; import { usePublicRelationsHooks } from '@/components/Applications/PublicRelations/hooks'; @@ -22,6 +21,9 @@ type ContentProps = { publicRelation?: PublicRelationResponse | null; formItem: FormItem[]; groupId: number; + publicRelationsTexts: ReturnType< + typeof usePublicRelationsHooks + >['publicRelationsTexts']; }; const Content: FC = ({ @@ -33,16 +35,16 @@ const Content: FC = ({ publicRelation, formItem, groupId, + publicRelationsTexts, }) => { - const { t } = useTranslation('common'); if (isLoading) { - return
{t('general.loading')}
; + return
{publicRelationsTexts.loading}
; } if (hasError) { return (
- {t('general.errors.fetch')} + {publicRelationsTexts.errors.fetch}
); } @@ -69,13 +71,19 @@ const PublicRelations: FC = ({ isDeadline, isRegistered, }) => { - const { t } = useTranslation('common'); - const { formItem, isEditing, toEdit, publicRelation, isLoading, hasError } = - usePublicRelationsHooks(groupId); + const { + formItem, + isEditing, + toEdit, + publicRelation, + isLoading, + hasError, + publicRelationsTexts, + } = usePublicRelationsHooks(groupId); return ( = ({ publicRelation={publicRelation} formItem={formItem} groupId={groupId} + publicRelationsTexts={publicRelationsTexts} /> ); diff --git a/user/src/components/Applications/PublicRelations/PublicRelationsForm/PublicRelationsForm.tsx b/user/src/components/Applications/PublicRelations/PublicRelationsForm/PublicRelationsForm.tsx index c742d2fbf..e920aa1d9 100644 --- a/user/src/components/Applications/PublicRelations/PublicRelationsForm/PublicRelationsForm.tsx +++ b/user/src/components/Applications/PublicRelations/PublicRelationsForm/PublicRelationsForm.tsx @@ -1,6 +1,5 @@ import { FC } from 'react'; import { PublicRelationResponse } from '@/api/publicRelationsApi'; -import { useTranslation } from 'next-i18next'; import Button from '@/components/Button/Button'; import Radio from '@/components/Form/Radio/Radio'; import TextArea from '@/components/Form/TextArea/TextArea'; @@ -19,8 +18,6 @@ const PublicRelationsForm: FC = ({ publicRelation, toEdit, }) => { - const { t } = useTranslation('common'); - // PublicRelationsForm receives toEdit as a required prop const { handleSubmit, errors, @@ -34,15 +31,13 @@ const PublicRelationsForm: FC = ({ announceOptions, onSubmit, validateEdit, + publicRelationsFormTexts, } = usePublicRelationsFormHooks(groupId, publicRelation); - const uploadNote = t('applications.publicRelations.notes.upload', { - returnObjects: true, - }) as string[]; return ( {isFetching || isMutating ? ( -
{t('general.loading')}
+
{publicRelationsFormTexts.general.loading}
) : (
= ({ {/* PR文入力 */}