diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 000000000..49282b29d --- /dev/null +++ b/.cursorrules @@ -0,0 +1 @@ +すべて日本語で出力してください diff --git a/user/.storybook/i18n.ts b/user/.storybook/i18n.ts new file mode 100644 index 000000000..2596154b4 --- /dev/null +++ b/user/.storybook/i18n.ts @@ -0,0 +1,40 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import nextI18NextConfig from '../next-i18next.config.js'; +import commonEn from '../public/locales/en/common.json'; +import commonJa from '../public/locales/ja/common.json'; + +const resources = { + ja: { + common: commonJa, + }, + en: { + common: commonEn, + }, +} as const; + +const { defaultLocale, locales } = nextI18NextConfig.i18n; + +export const initStorybookI18n = () => { + if (!i18n.isInitialized) { + i18n.use(initReactI18next).init({ + resources, + lng: defaultLocale, + fallbackLng: defaultLocale, + supportedLngs: locales, + ns: ['common'], + defaultNS: 'common', + interpolation: { + escapeValue: false, + }, + react: { + useSuspense: false, + }, + initImmediate: false, + }); + } + + return i18n; +}; + +export default initStorybookI18n(); diff --git a/user/.storybook/preview.tsx b/user/.storybook/preview.tsx index d0472038b..df77ecaf5 100755 --- a/user/.storybook/preview.tsx +++ b/user/.storybook/preview.tsx @@ -1,27 +1,32 @@ // .storybook/preview.tsx +import React from 'react'; import type { Preview } from '@storybook/react'; import { themes } from '@storybook/theming'; import { SessionProvider } from 'next-auth/react'; +import { I18nextProvider } from 'react-i18next'; import { ToastContainer } from 'react-toastify'; +import i18n from './i18n'; const preview: Preview = { decorators: [ (Story) => ( - - - - + + + + + + ), ], tags: ['autodocs'], 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..53b17a225 100644 --- a/user/next.config.ts +++ b/user/next.config.ts @@ -2,6 +2,9 @@ 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]: { SSR_API_URL: string; @@ -34,6 +37,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) diff --git a/user/public/locales/en/common.json b/user/public/locales/en/common.json new file mode 100644 index 000000000..dfd89d033 --- /dev/null +++ b/user/public/locales/en/common.json @@ -0,0 +1,892 @@ +{ + "languageSwitcher": { + "label": "Language selector", + "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.", + "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." + } + }, + "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 new file mode 100644 index 000000000..f6ec27b2e --- /dev/null +++ b/user/public/locales/ja/common.json @@ -0,0 +1,892 @@ +{ + "languageSwitcher": { + "label": "言語切り替え", + "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": "お知らせはありません。", + "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": "再度ログインしてください。" + } + }, + "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." + } +} diff --git a/user/src/components/AccordionMenu/AccordionMenu.tsx b/user/src/components/AccordionMenu/AccordionMenu.tsx index 7ac1c2d6e..3f79358bf 100755 --- a/user/src/components/AccordionMenu/AccordionMenu.tsx +++ b/user/src/components/AccordionMenu/AccordionMenu.tsx @@ -2,6 +2,7 @@ import React, { FC, useState } from 'react'; import { RiArrowDownWideLine } from 'react-icons/ri'; import { Textfit } from 'react-textfitfix'; import Status from '@/components/Status'; +import { useAccordionMenuTexts } from './hooks'; type AccordionMenuProps = { title: string; @@ -22,6 +23,7 @@ const AccordionMenu: FC = ({ required, note, }) => { + const { labels } = useAccordionMenuTexts(); const receptionStatus = isEdit ? 'open' : 'closed'; const registerStatus = @@ -40,29 +42,35 @@ const AccordionMenu: FC = ({ }; return ( -
+
@@ -83,32 +86,49 @@ const CookingProcessOrder: FC = ({ cookingProcessOrder ? [ { - label: '販売品名', + label: + cookingProcessOrderTexts.summary.labels + .foodProduct, content: foodProduct.name, }, { - label: '調理場の使用有無(営業前)', + label: + cookingProcessOrderTexts.summary.labels + .preOpen, content: cookingProcessOrder.preOpenKitchen - ? '使用する' - : '使用しない', + ? cookingProcessOrderTexts.summary.status.use + : cookingProcessOrderTexts.summary.status + .notUse, }, { - label: '調理場の使用有無(営業中)', + label: + cookingProcessOrderTexts.summary.labels + .duringOpen, content: cookingProcessOrder.duringOpenKitchen - ? '使用する' - : '使用しない', + ? cookingProcessOrderTexts.summary.status.use + : cookingProcessOrderTexts.summary.status + .notUse, }, { - label: '調理内容', + label: + cookingProcessOrderTexts.summary.labels + .description, content: cookingProcessOrder.tent || '', }, ] : [ { - label: '販売品名', + label: + cookingProcessOrderTexts.summary.labels + .foodProduct, content: foodProduct.name, }, - { label: '調理工程', content: '未登録' }, + { + label: cookingProcessOrderTexts.title, + content: + cookingProcessOrderTexts.summary.status + .notRegistered, + }, ] } /> @@ -125,7 +145,7 @@ const CookingProcessOrder: FC = ({ icon="pencil" onClick={handleEditClick} > - 修正 + {cookingProcessOrderTexts.buttons.edit}
)} diff --git a/user/src/components/Applications/CookingProcessOrder/CookingProcessOrderForm/CookingProcessOrderForm.tsx b/user/src/components/Applications/CookingProcessOrder/CookingProcessOrderForm/CookingProcessOrderForm.tsx index 2ba4e9ac7..8d6d86d0a 100755 --- a/user/src/components/Applications/CookingProcessOrder/CookingProcessOrderForm/CookingProcessOrderForm.tsx +++ b/user/src/components/Applications/CookingProcessOrder/CookingProcessOrderForm/CookingProcessOrderForm.tsx @@ -16,7 +16,8 @@ const CookingProcessOrderForm: FC = ({ foodProductName, }) => { const { setValue } = useFormContext(); - const { values, getError } = useCookingProcessOrderForm(index); + const { values, getError, cookingProcessOrderFormTexts } = + useCookingProcessOrderForm(index); // 調理場使用状況の定数 const KITCHEN_USAGE = { @@ -24,39 +25,29 @@ const CookingProcessOrderForm: FC = ({ NOT_USE: 0, } as const; - const option = [ - { id: KITCHEN_USAGE.USE, name: '使用する' }, - { id: KITCHEN_USAGE.NOT_USE, name: '使用しない' }, - ]; - - const confirmCookingProcess = [ - { - id: '1', - name: '衛生管理の工程をできるだけ詳しく記載しました', - }, - { - id: '2', - name: '最終的に加熱して提供するか確認しました', - }, - { - id: '3', - name: 'お酒の調理工程も提出しました', - }, - ]; + const option = cookingProcessOrderFormTexts.options.kitchenUsage; + const confirmCookingProcess = + cookingProcessOrderFormTexts.options.confirmCookingProcess; return (
-
販売品名
+
+ {cookingProcessOrderFormTexts.summaryLabels.foodProduct} +
{foodProductName}
-

調理場の使用有無

-

※必須

+

+ {cookingProcessOrderFormTexts.fields.kitchenUsage} +

+

+ ※{cookingProcessOrderFormTexts.general.required} +

= ({ error={getError('preOpenKitchen')} /> = ({ error={getError('duringOpenKitchen')} />
@@ -132,7 +120,7 @@ const GroupForm: FC = ({ type="button" onClick={toEdit} > - キャンセル + {groupFormTexts.buttons.cancel} )} @@ -142,7 +130,9 @@ const GroupForm: FC = ({ type="submit" isDisable={createIsMutating || updateIsMutating || validateEdit()} > - {groups ? '修正' : '登録'} + {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 6fcc6070f..02da15522 100644 --- a/user/src/components/Applications/Group/GroupForm/hooks.ts +++ b/user/src/components/Applications/Group/GroupForm/hooks.ts @@ -6,8 +6,10 @@ 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 { groupLabels } from '../../label'; import { GroupForm, groupSchema } from './schema'; export const useGroupFormHooks = ( @@ -18,6 +20,7 @@ export const useGroupFormHooks = ( mutateCheckAllRegisteredGroups: () => void, mutateGroupByUserId: () => void ) => { + const { t } = useTranslation('common'); // 団体カテゴリー一覧を取得 const { handleSubmit, @@ -44,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) { @@ -72,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('送信しました'); + toast.success(groupFormTexts.messages.updateSuccess); } catch { - toast.error('送信に失敗しました。'); + toast.error(groupFormTexts.messages.updateFailed); } // 団体申請がない場合は新規作成 } else { @@ -89,9 +140,9 @@ export const useGroupFormHooks = ( mutateGroups(); mutateCheckAllRegisteredGroups(); mutateGroupByUserId(); - toast.success('送信しました'); + toast.success(groupFormTexts.messages.registerSuccess); } catch { - toast.error('送信に失敗しました。'); + toast.error(groupFormTexts.messages.registerFailed); } reset(); } @@ -118,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/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..80b59b104 100644 --- a/user/src/components/Applications/Group/hooks.ts +++ b/user/src/components/Applications/Group/hooks.ts @@ -1,32 +1,50 @@ 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 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: groupLabels[0], + label: groupTexts.summaryLabels[0], content: groups?.name, }, { - label: groupLabels[1], + label: groupTexts.summaryLabels[1], content: groups?.projectName, }, { - label: groupLabels[2], - content: groups?.isInternational ? 'はい' : 'いいえ', + label: groupTexts.summaryLabels[2], + content: groups?.isInternational + ? groupTexts.boolean.yes + : groupTexts.boolean.no, }, { - label: groupLabels[3], - content: groups?.isExternal ? 'はい' : 'いいえ', + label: groupTexts.summaryLabels[3], + content: groups?.isExternal + ? groupTexts.boolean.yes + : groupTexts.boolean.no, }, { - label: groupLabels[4], + label: groupTexts.summaryLabels[4], content: groups?.groupCategoryId ? groupCategories?.find( (category) => category.id === groups.groupCategoryId @@ -34,7 +52,7 @@ export const useGroupHooks = (groupId: number) => { : '', }, { - label: groupLabels[5], + label: groupTexts.summaryLabels[5], content: groups?.activity, }, ]; @@ -62,5 +80,6 @@ export const useGroupHooks = (groupId: number) => { formItem, groupCategories, mutateGroups, + groupTexts, }; }; diff --git a/user/src/components/Applications/MultiItemForms/RentItems/RentItems.tsx b/user/src/components/Applications/MultiItemForms/RentItems/RentItems.tsx index 3511c9532..3d1acf0a1 100644 --- a/user/src/components/Applications/MultiItemForms/RentItems/RentItems.tsx +++ b/user/src/components/Applications/MultiItemForms/RentItems/RentItems.tsx @@ -1,6 +1,7 @@ import { FC } from 'react'; 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; @@ -15,9 +16,10 @@ const RentItems: FC = ({ groupId, groupCategoryId, }) => { + const { rentItemsAccordionTexts } = useRentItemsAccordionHooks(); return ( = ({ groupCategoryId, isDeadline, }) => { - // 主に groupCategoryId を使って判断するように変更 const { form, fields, @@ -45,12 +44,13 @@ const RentItemsForm: FC = ({ hideLocationTypeSelect, // 会場タイプ選択を非表示にするフラグ isFoodSellingGroup, // 食品販売団体かどうかのフラグ getMaxCountByItemId, // 物品ID別の最大個数を取得する関数 - } = useRentItemsFormLogic(groupId, groupCategoryId); + rentItemsFormTexts, + } = useRentItemsFormHooks(groupId, groupCategoryId); if (isLoading) { return (
-

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

+

{rentItemsFormTexts.general.loading}

); } @@ -58,9 +58,11 @@ const RentItemsForm: FC = ({ if (hasError) { return (
- エラー: + + {rentItemsFormTexts.errors.fetch.title} + - データの取得に失敗しました。ページを再読込してください。 + {rentItemsFormTexts.errors.fetch.description}
); @@ -72,8 +74,10 @@ const RentItemsForm: FC = ({
-

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

-

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

+

+ {rentItemsFormTexts.summary.noApplication.label} +

+

{rentItemsFormTexts.summary.noApplication.description}

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

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

)} {isFoodSellingGroup && (

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

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

- {form.getValues(`items.${index}.count`)} 個 + {rentItemsFormTexts.summary.count( + form.getValues(`items.${index}.count`) + )}

@@ -148,7 +154,7 @@ const RentItemsForm: FC = ({ icon="pencil" onClick={openEditMode} > - 修正 + {rentItemsFormTexts.buttons.edit} )} @@ -163,17 +169,21 @@ const RentItemsForm: FC = ({ onSubmit={handleFormSubmit} >
-

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

+
+ {!hideLocationTypeSelect && ( +

{rentItemsFormTexts.location.notes.preApplication}

+ )} + {isFoodSellingGroup && ( +

{rentItemsFormTexts.location.notes.foodOnlyOutdoor}

+ )} +

( { field.onChange(value === '1'); @@ -181,8 +191,14 @@ const RentItemsForm: FC = ({ }} required options={[ - { id: 1, name: 'はい' }, - { id: 0, name: 'いいえ' }, + { + id: 1, + name: rentItemsFormTexts.radio.options.yes, + }, + { + id: 0, + name: rentItemsFormTexts.radio.options.no, + }, ]} error={errors.hasItems?.message?.toString()} /> @@ -205,7 +221,7 @@ const RentItemsForm: FC = ({ } }} > - 登録 + {rentItemsFormTexts.buttons.register}
@@ -221,7 +237,7 @@ const RentItemsForm: FC = ({ control={control} render={({ field }) => ( { // 新しい値でupdateLocationTypeを呼び出す @@ -229,8 +245,14 @@ const RentItemsForm: FC = ({ }} required options={[ - { id: 1, name: '屋内' }, - { id: 2, name: '屋外' }, + { + id: 1, + name: rentItemsFormTexts.location.options.indoor, + }, + { + id: 2, + name: rentItemsFormTexts.location.options.outdoor, + }, ]} error={errors.locationType?.message} /> @@ -242,7 +264,9 @@ const RentItemsForm: FC = ({
-

物品 {index + 1}

+

+ {rentItemsFormTexts.fields.sectionTitle(index + 1)} +

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

- ※必要最低限の数だけ申請してください + {rentItemsFormTexts.notes.minRequest}

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

{fields.length > 1 && ( @@ -355,7 +379,7 @@ const RentItemsForm: FC = ({ >
- 削除 + {rentItemsFormTexts.buttons.delete}
@@ -373,14 +397,18 @@ const RentItemsForm: FC = ({ {errors.root?.message && (
- {errors.root.message.toString()} + {rentItemsFormTexts.errors.translate( + errors.root.message.toString() + )}
)} {/* フォームバリデーションエラーがあれば表示(アイテム制限関連のエラーも含む) */} {errors.items?.message && (
- {errors.items.message.toString()} + {rentItemsFormTexts.errors.translate( + errors.items.message.toString() + )}
)} @@ -394,11 +422,14 @@ const RentItemsForm: FC = ({ }} >
- + 物品の追加 + +{' '} + {rentItemsFormTexts.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/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 86% rename from user/src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsFormLogic.ts rename to user/src/components/Applications/MultiItemForms/RentItems/hooks/useRentItemsFormHooks.ts index 4d31ec589..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, @@ -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'; @@ -63,10 +64,11 @@ const TABLE_CHAIR_MAX_COUNT = { [LOCATION_TYPES.OUTDOOR]: 20, // 屋外: 20個 }; -export const useRentItemsFormLogic = ( +export const useRentItemsFormHooks = ( 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,12 +713,76 @@ 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')); } }; + 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, @@ -734,5 +808,6 @@ export const useRentItemsFormLogic = ( hideLocationTypeSelect, // 団体タイプに応じたUI表示制御フラグ isFoodSellingGroup, // 食品販売団体かどうかのフラグ getMaxCountByItemId, // 物品IDに基づいて最大個数を取得する関数 + rentItemsFormTexts, }; }; diff --git a/user/src/components/Applications/Power/Power.tsx b/user/src/components/Applications/Power/Power.tsx index 6ecab1bbd..74d750cd3 100644 --- a/user/src/components/Applications/Power/Power.tsx +++ b/user/src/components/Applications/Power/Power.tsx @@ -3,6 +3,7 @@ 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'; @@ -13,6 +14,8 @@ type PowerProps = { }; const Power: FC = ({ isDeadline, isRegistered, groupId }) => { + const powerAccordionHooks = usePowerAccordionHooks(); + // 電力申請のカスタムフックから状態とロジックの取得 const { state, @@ -142,7 +145,7 @@ const Power: FC = ({ isDeadline, isRegistered, groupId }) => { return ( = ({ const PowerForm: FC = ({ index, form, onRemove }) => { const { control, formState } = form; + const { powerDeviceFormTexts } = usePowerDeviceFormHooks(); // エラーメッセージを取得 - 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

+

{powerDeviceFormTexts.notes.emailWarning(POWER_LIMIT)}

+

{powerDeviceFormTexts.notes.contactEmail}

{index > 0 && !form.getValues().devices[index]?.productName && ( @@ -124,7 +126,7 @@ const PowerForm: FC = ({ index, form, onRemove }) => { variant onClick={() => onRemove(index)} > - 削除 + {powerDeviceFormTexts.actions.delete}
)} diff --git a/user/src/components/Applications/Power/components/PowerFormView.tsx b/user/src/components/Applications/Power/components/PowerFormView.tsx index ee60f2098..b00c1efd4 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 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'; @@ -18,16 +20,17 @@ export const PowerFormView: FC = ({ onSubmit, }) => { const { handleSubmit } = formMethods; + const { powerFormViewTexts } = usePowerFormViewHooks(radioOptions); return (
{/* ラジオボタン */} {/* 申請する場合のフォーム */} @@ -46,9 +49,14 @@ export const PowerFormView: FC = ({
{/* 電力超過警告 */}
- {totalPower > 1500 && ( + {totalPower > POWER_LIMIT && (
-

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

+

+ {powerFormViewTexts.warnings.totalPower( + POWER_LIMIT, + totalPower + )} +

)} {/* 操作ボタン */} @@ -61,15 +69,15 @@ export const PowerFormView: FC = ({ variant onClick={onAddDevice} > - 物品の追加 + {powerFormViewTexts.actions.addDevice}
diff --git a/user/src/components/Applications/Power/components/PowerNegativeView.tsx b/user/src/components/Applications/Power/components/PowerNegativeView.tsx index f3ec9a275..5acfa1b1d 100644 --- a/user/src/components/Applications/Power/components/PowerNegativeView.tsx +++ b/user/src/components/Applications/Power/components/PowerNegativeView.tsx @@ -2,7 +2,7 @@ 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 = ({ @@ -18,23 +18,18 @@ export const PowerNegativeView: FC = ({ onCancel, isDeadline, }) => { - const noApplicationItems: FormItem[] = [ - { - label: '電力申請は不要(登録済み)', - content: '電力が必要な機器は使用しません。', - }, - ]; + const { powerNegativeViewTexts } = usePowerNegativeViewHooks(radioOptions); return (
{isEdit && ( <> {onCancel && (
@@ -45,7 +40,7 @@ export const PowerNegativeView: FC = ({ variant onClick={onCancel} > - キャンセル + {powerNegativeViewTexts.actions.cancel}
)} @@ -54,7 +49,7 @@ export const PowerNegativeView: FC = ({ {!isEdit && ( @@ -64,7 +59,9 @@ export const PowerNegativeView: FC = ({
{submitError && (
- エラー: + + {powerNegativeViewTexts.errors.submitTitle} + {submitError}
)} @@ -74,7 +71,7 @@ export const PowerNegativeView: FC = ({ color="main" onClick={onNegativeSubmit} > - 登録 + {powerNegativeViewTexts.actions.register}
)} diff --git a/user/src/components/Applications/Power/components/PowerSummaryView.tsx b/user/src/components/Applications/Power/components/PowerSummaryView.tsx index 48ef3bc7b..ba3c7f7d6 100644 --- a/user/src/components/Applications/Power/components/PowerSummaryView.tsx +++ b/user/src/components/Applications/Power/components/PowerSummaryView.tsx @@ -1,21 +1,8 @@ import { FC } from 'react'; 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[] => { - const items: FormItem[] = []; - items.push({ label: '製品名', content: device.productName }); - items.push({ label: 'メーカー名', content: device.manufacturer }); - items.push({ label: '型番', content: device.model }); - if (device.url) { - items.push({ label: '製品URL', content: device.url }); - } - items.push({ label: '消費電力[W]', content: `${device.maxPower}W` }); - return items; -}; +import { usePowerSummaryViewHooks } from '../hooks/usePowerSummaryViewHooks'; +import { PowerSummaryViewProps } from '../types'; export const PowerSummaryView: FC = ({ devices, @@ -23,12 +10,15 @@ export const PowerSummaryView: FC = ({ onDeleteDevice, isDeadline, }) => { + const { createSummaryItemsForDevice, powerSummaryViewTexts } = + usePowerSummaryViewHooks(); + return (
{devices.map((device, index) => (
onDeleteDevice(device.id!) : undefined} @@ -45,7 +35,7 @@ export const PowerSummaryView: FC = ({ icon="pencil" onClick={onEdit} > - 修正 + {powerSummaryViewTexts.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/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/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/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, + }; +}; 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; diff --git a/user/src/components/Applications/PublicRelations/PublicRelations.tsx b/user/src/components/Applications/PublicRelations/PublicRelations.tsx index fb4202522..cb68d1ddc 100644 --- a/user/src/components/Applications/PublicRelations/PublicRelations.tsx +++ b/user/src/components/Applications/PublicRelations/PublicRelations.tsx @@ -21,6 +21,9 @@ type ContentProps = { publicRelation?: PublicRelationResponse | null; formItem: FormItem[]; groupId: number; + publicRelationsTexts: ReturnType< + typeof usePublicRelationsHooks + >['publicRelationsTexts']; }; const Content: FC = ({ @@ -32,15 +35,16 @@ const Content: FC = ({ publicRelation, formItem, groupId, + publicRelationsTexts, }) => { if (isLoading) { - return
Loading...
; + return
{publicRelationsTexts.loading}
; } if (hasError) { return (
- データの取得に失敗しました。 + {publicRelationsTexts.errors.fetch}
); } @@ -67,12 +71,19 @@ const PublicRelations: FC = ({ isDeadline, isRegistered, }) => { - 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 d7bd7e021..e920aa1d9 100644 --- a/user/src/components/Applications/PublicRelations/PublicRelationsForm/PublicRelationsForm.tsx +++ b/user/src/components/Applications/PublicRelations/PublicRelationsForm/PublicRelationsForm.tsx @@ -18,7 +18,6 @@ const PublicRelationsForm: FC = ({ publicRelation, toEdit, }) => { - // PublicRelationsForm receives toEdit as a required prop const { handleSubmit, errors, @@ -32,12 +31,13 @@ const PublicRelationsForm: FC = ({ announceOptions, onSubmit, validateEdit, + publicRelationsFormTexts, } = usePublicRelationsFormHooks(groupId, publicRelation); return ( {isFetching || isMutating ? ( -
loading...
+
{publicRelationsFormTexts.general.loading}
) : (
= ({ {/* PR文入力 */}