From be0daf20975cef9a506ac9fed995ae58527498b5 Mon Sep 17 00:00:00 2001 From: Yoshihiro OKUMURA Date: Fri, 4 Jul 2025 15:32:01 +0900 Subject: [PATCH] support language translation. --- package-lock.json | 198 ++++++++++++++++++-------- package.json | 8 +- src/common/kintoneLanguageDetector.ts | 10 ++ src/common/kintoneUtils.ts | 9 -- src/config/Settings.tsx | 23 +-- src/config/i18n.ts | 27 ++++ src/config/index.tsx | 2 + src/config/locales/en.json | 20 +++ src/config/locales/ja.json | 20 +++ src/desktop/MenuPanel.tsx | 55 ++++--- src/desktop/i18n.ts | 27 ++++ src/desktop/index.tsx | 2 + src/desktop/locales/en.json | 14 ++ src/desktop/locales/ja.json | 14 ++ 14 files changed, 329 insertions(+), 100 deletions(-) create mode 100644 src/common/kintoneLanguageDetector.ts delete mode 100644 src/common/kintoneUtils.ts create mode 100644 src/config/i18n.ts create mode 100644 src/config/locales/en.json create mode 100644 src/config/locales/ja.json create mode 100644 src/desktop/i18n.ts create mode 100644 src/desktop/locales/en.json create mode 100644 src/desktop/locales/ja.json diff --git a/package-lock.json b/package-lock.json index 1808b6a..6776e10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,18 @@ "version": "1.0.2", "dependencies": { "@kintone/rest-api-client": "^5.7.4", - "angular-expressions": "^1.4.3", + "angular-expressions": "^1.5.0", "clsx": "^2.1.1", "dayjs": "^1.11.13", "docxtemplater": "^3.65.1", "file-saver": "^2.0.5", + "i18next": "^25.3.0", "moize": "^6.1.6", "pizzip": "^3.2.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-error-boundary": "^6.0.0", + "react-i18next": "^15.6.0", "tiny-invariant": "^1.3.3" }, "devDependencies": { @@ -26,8 +28,8 @@ "@kintone/dts-gen": "^8.1.2", "@kintone/plugin-uploader": "^9.1.5", "@kintone/webpack-plugin-kintone-plugin": "^8.0.11", - "@rspack/cli": "^1.4.2", - "@rspack/core": "^1.4.2", + "@rspack/cli": "^1.4.3", + "@rspack/core": "^1.4.3", "@shin-chan/kypes": "^0.0.7", "@types/file-saver": "^2.0.7", "@types/node": "^24.0.10", @@ -1152,28 +1154,28 @@ } }, "node_modules/@rspack/binding": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.4.2.tgz", - "integrity": "sha512-NdTLlA20ufD0thFvDIwwPk+bX9yo3TDE4XjfvZYbwFyYvBgqJOWQflnbwLgvSTck0MSTiOqWIqpR88ymAvWTqg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.4.3.tgz", + "integrity": "sha512-bDKAruEbEdlozi8NkLrC0H+e/CfWuQgxi08akofLBp227Nd/n0yLF4VWaUkZr4lSbMJQBxXPattryDijDnoLwA==", "dev": true, "license": "MIT", "optionalDependencies": { - "@rspack/binding-darwin-arm64": "1.4.2", - "@rspack/binding-darwin-x64": "1.4.2", - "@rspack/binding-linux-arm64-gnu": "1.4.2", - "@rspack/binding-linux-arm64-musl": "1.4.2", - "@rspack/binding-linux-x64-gnu": "1.4.2", - "@rspack/binding-linux-x64-musl": "1.4.2", - "@rspack/binding-wasm32-wasi": "1.4.2", - "@rspack/binding-win32-arm64-msvc": "1.4.2", - "@rspack/binding-win32-ia32-msvc": "1.4.2", - "@rspack/binding-win32-x64-msvc": "1.4.2" + "@rspack/binding-darwin-arm64": "1.4.3", + "@rspack/binding-darwin-x64": "1.4.3", + "@rspack/binding-linux-arm64-gnu": "1.4.3", + "@rspack/binding-linux-arm64-musl": "1.4.3", + "@rspack/binding-linux-x64-gnu": "1.4.3", + "@rspack/binding-linux-x64-musl": "1.4.3", + "@rspack/binding-wasm32-wasi": "1.4.3", + "@rspack/binding-win32-arm64-msvc": "1.4.3", + "@rspack/binding-win32-ia32-msvc": "1.4.3", + "@rspack/binding-win32-x64-msvc": "1.4.3" } }, "node_modules/@rspack/binding-darwin-arm64": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.4.2.tgz", - "integrity": "sha512-0fPOew7D0l/x6qFZYdyUqutbw15K98VLvES2/7x2LPssTgypE4rVmnQSmVBnge3Nr8Qs/9qASPRpMWXBaqMfOA==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.4.3.tgz", + "integrity": "sha512-YwPYWvo+WhdQgb76ZnH6m4sXClcRJJ5UB3Qj7xABKDQNJ62MaczWHEPfh2LM4iSJ1IWMo9dW4yeEXa7U9aE94w==", "cpu": [ "arm64" ], @@ -1185,9 +1187,9 @@ ] }, "node_modules/@rspack/binding-darwin-x64": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.4.2.tgz", - "integrity": "sha512-0Dh6ssGgwnd9G+IO8SwQaJ0RJ8NkQbk4hwoJH/u52Mnfl0EvhmNvuhkbSEoKn1U3kElOA2cxH/3gbYzuYExn3g==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.4.3.tgz", + "integrity": "sha512-aynWl0uCfIVfzDiZtSA6l75U8zyIc0UBa0p/ZETHrIQlBHPKDmxVIOlpbJWprilw5i4a3nWbadKCzvb0Gb92iA==", "cpu": [ "x64" ], @@ -1199,9 +1201,9 @@ ] }, "node_modules/@rspack/binding-linux-arm64-gnu": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.4.2.tgz", - "integrity": "sha512-UHAzggS8Mc7b3Xguhj82HwujLqBZquCeo8qJj5XreNaMKGb6YRw/91dJOVmkNiLCB0bj71CRE1Cocd+Peq3N9A==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.4.3.tgz", + "integrity": "sha512-x6OlSqt4esxj5hAZq+aPSG1pbNtjLPDw3cnQlfqv04kJO6MOwrH+j4DPc+/Q2qKdFzLw807eyvKd3O9leu+iPg==", "cpu": [ "arm64" ], @@ -1213,9 +1215,9 @@ ] }, "node_modules/@rspack/binding-linux-arm64-musl": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.4.2.tgz", - "integrity": "sha512-QybZ0VxlFih+upLoE7Le5cN3LpxJwk6EnEQTigmzpfc4c4SOC889ftBoIAO3IeBk+mF3H2C9xD+/NolTdwoeiw==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.4.3.tgz", + "integrity": "sha512-6aCa76fW8WlSBc0bJKXLSql79NoFlua4b+59XN1kZln+yLstgicFiJSvAnw+9U7mrl7IP9UmVWTw3JBnHUtw4A==", "cpu": [ "arm64" ], @@ -1227,9 +1229,9 @@ ] }, "node_modules/@rspack/binding-linux-x64-gnu": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.4.2.tgz", - "integrity": "sha512-ucCCWdtH1tekZadrsYj6GNJ8EP21BM2uSE7MootbwLw8aBtgVTKUuRDQEps1h/rtrdthzd9XBX6Lc2N926gM+g==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.4.3.tgz", + "integrity": "sha512-N2kUPPVjkky9KlK/a1QXvRivtTS0RJ1ZGRfkacycOTcKwB3nvrz6IqYOFhIst9Wjed677KELKP5zZv/VImfY7Q==", "cpu": [ "x64" ], @@ -1241,9 +1243,9 @@ ] }, "node_modules/@rspack/binding-linux-x64-musl": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.4.2.tgz", - "integrity": "sha512-+Y2LS6Qyk2AZor8DqlA8yKCqElYr0Urjc3M66O4ZzlxDT5xXX0J2vp04AtFp0g81q/+UgV3cbC//dqDvO0SiBA==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.4.3.tgz", + "integrity": "sha512-SAzoCLCHQMFoQ41i9APIfE2ndv3JT5LkPvl6V1FmiFRgZwH0jVKpIybulZtgLNgNh4nFIYES0+2XK8dZ28YR5g==", "cpu": [ "x64" ], @@ -1255,9 +1257,9 @@ ] }, "node_modules/@rspack/binding-wasm32-wasi": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.4.2.tgz", - "integrity": "sha512-3WvfHY7NvzORek3FcQWLI/B8wQ7NZe0e0Bub9GyLNVxe5Bi+dxnSzEg6E7VsjbUzKnYufJA0hDKbEJ2qCMvpdw==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.4.3.tgz", + "integrity": "sha512-jrakte9rA3a+VfRqm4Qa3o+MI4lfYZGqQTdpWyC9GLi3USxyaU55hGYdd/TtGvDY82KvJfGTV8o+LRn0Pl77OA==", "cpu": [ "wasm32" ], @@ -1269,9 +1271,9 @@ } }, "node_modules/@rspack/binding-win32-arm64-msvc": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.4.2.tgz", - "integrity": "sha512-Y6L9DrLFRW6qBBCY3xBt7townStN5mlcbBTuG1zeXl0KcORPv1G1Cq6HXP6f1em+YsHE1iwnNqLvv4svg5KsnQ==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.4.3.tgz", + "integrity": "sha512-Tho05w0sUWKe7avQYYGwaL5xNDFRav4A4Su5DTCC6mQeokbndg0Lk7jtyYIp9ZHCStUOojR4PY/4zG918ky95w==", "cpu": [ "arm64" ], @@ -1283,9 +1285,9 @@ ] }, "node_modules/@rspack/binding-win32-ia32-msvc": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.4.2.tgz", - "integrity": "sha512-FyTJrL7GcYXPWKUB9Oj2X29kfma6MUgM9PyXGy8gDMti21kMMhpHp/bGVqfurRbazDyklDuLLtbHuawpa6toeA==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.4.3.tgz", + "integrity": "sha512-Hc8tC30FvW5h8r3wW7C0pqY5b5BlMk0Y7vi03Zt60r8WqmeNINvAsjzyifHikD4S4lyQQO4CGXZOzSBWUcYesw==", "cpu": [ "ia32" ], @@ -1297,9 +1299,9 @@ ] }, "node_modules/@rspack/binding-win32-x64-msvc": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.4.2.tgz", - "integrity": "sha512-ODSU26tmG8MfMFDHCaMLCORB64EVdEtDvPP5zJs0Mgh7vQaqweJtqgG0ukZCQy4ApUatOrMaZrLk557jp9Biyw==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.4.3.tgz", + "integrity": "sha512-D+I6fl6Phq8+VElvf3sVTh+bqatk9Rjtgh4l2D7elIUkBguT5nAY3SX5wE/7iYnSdOBbP5mghU4exOG7NYqJYA==", "cpu": [ "x64" ], @@ -1311,9 +1313,9 @@ ] }, "node_modules/@rspack/cli": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@rspack/cli/-/cli-1.4.2.tgz", - "integrity": "sha512-S1d82mOdL0Iio/KoJ8E8HRAamBqpfvePZ+qkffs2sAYEzLrBQyI8dEADJ5SXPoHdyy8IafcrlRJnBh0ETLVVsg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@rspack/cli/-/cli-1.4.3.tgz", + "integrity": "sha512-6OLbIL5TxVsAiWjx7MZvA8hq7fz1m8rPDuvB9QhvpEFIpL3OqZf0+LTY60ApOFOSy+/vYWFe0oT+8NGOX65RjA==", "dev": true, "license": "MIT", "dependencies": { @@ -1334,14 +1336,14 @@ } }, "node_modules/@rspack/core": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.4.2.tgz", - "integrity": "sha512-Mmk3X3fbOLtRq4jX8Ebp3rfjr75YgupvNksQb0WbaGEVr5l1b6woPH/LaXF2v9U9DP83wmpZJXJ8vclB5JfL/w==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.4.3.tgz", + "integrity": "sha512-zWdAXleiYZ+SlappgDbjsoBWIQzyYJX5WwPXmoQnHJPb4K+zmjCL8MRfdIMIxXcg5IiQsBfiWY/lYhaZ2Jc0EA==", "dev": true, "license": "MIT", "dependencies": { "@module-federation/runtime-tools": "0.15.0", - "@rspack/binding": "1.4.2", + "@rspack/binding": "1.4.3", "@rspack/lite-tapable": "1.0.1" }, "engines": { @@ -2576,9 +2578,9 @@ } }, "node_modules/angular-expressions": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/angular-expressions/-/angular-expressions-1.4.3.tgz", - "integrity": "sha512-r7j+dqOuHy0OYiR5AazDixU/Us3TDN2FfuxGX4Dq6d61Y2MhBQHMdUNBfkkLPjDqVm2Is394h31gC3bcBwy9zw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/angular-expressions/-/angular-expressions-1.5.0.tgz", + "integrity": "sha512-ksiE8I2lg124ssP8BmJlQg5RgjyBl1SmZluQ2Sj89n6aHbbQhqRsPUGwW80uiHDhUVxhan0FBDemfei0rbWKlg==", "license": "Unlicense" }, "node_modules/ansi-escapes": { @@ -4679,14 +4681,13 @@ } }, "node_modules/eslint-plugin-n": { - "version": "17.20.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.20.0.tgz", - "integrity": "sha512-IRSoatgB/NQJZG5EeTbv/iAx1byOGdbbyhQrNvWdCfTnmPxUT0ao9/eGOeG7ljD8wJBsxwE8f6tES5Db0FRKEw==", + "version": "17.21.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.21.0.tgz", + "integrity": "sha512-1+iZ8We4ZlwVMtb/DcHG3y5/bZOdazIpa/4TySo22MLKdwrLcfrX0hbadnCvykSQCCmkAnWmIP8jZVb2AAq29A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.5.0", - "@typescript-eslint/utils": "^8.26.1", "enhanced-resolve": "^5.17.1", "eslint-plugin-es-x": "^7.8.0", "get-tsconfig": "^4.8.1", @@ -5942,6 +5943,15 @@ "dev": true, "license": "MIT" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -6061,6 +6071,37 @@ "node": ">=10.18" } }, + "node_modules/i18next": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.0.tgz", + "integrity": "sha512-ZSQIiNGfqSG6yoLHaCvrkPp16UejHI8PCDxFYaNG/1qxtmqNmqEg4JlWKlxkrUmrin2sEjsy+Mjy1TRozBhOgw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -8586,6 +8627,32 @@ "react": ">=16.13.1" } }, + "node_modules/react-i18next": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.6.0.tgz", + "integrity": "sha512-W135dB0rDfiFmbMipC17nOhGdttO5mzH8BivY+2ybsQBbXvxWIwl3cmeH3T9d+YPBSJu/ouyJKFJTtkK7rJofw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -10525,7 +10592,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -10737,6 +10804,15 @@ "node": ">= 0.8" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/watchpack": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", diff --git a/package.json b/package.json index fc510b9..90be46f 100644 --- a/package.json +++ b/package.json @@ -15,16 +15,18 @@ }, "dependencies": { "@kintone/rest-api-client": "^5.7.4", - "angular-expressions": "^1.4.3", + "angular-expressions": "^1.5.0", "clsx": "^2.1.1", "dayjs": "^1.11.13", "docxtemplater": "^3.65.1", "file-saver": "^2.0.5", + "i18next": "^25.3.0", "moize": "^6.1.6", "pizzip": "^3.2.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-error-boundary": "^6.0.0", + "react-i18next": "^15.6.0", "tiny-invariant": "^1.3.3" }, "devDependencies": { @@ -32,8 +34,8 @@ "@kintone/dts-gen": "^8.1.2", "@kintone/plugin-uploader": "^9.1.5", "@kintone/webpack-plugin-kintone-plugin": "^8.0.11", - "@rspack/cli": "^1.4.2", - "@rspack/core": "^1.4.2", + "@rspack/cli": "^1.4.3", + "@rspack/core": "^1.4.3", "@shin-chan/kypes": "^0.0.7", "@types/file-saver": "^2.0.7", "@types/node": "^24.0.10", diff --git a/src/common/kintoneLanguageDetector.ts b/src/common/kintoneLanguageDetector.ts new file mode 100644 index 0000000..c9c5abd --- /dev/null +++ b/src/common/kintoneLanguageDetector.ts @@ -0,0 +1,10 @@ +import { LanguageDetectorModule } from 'i18next'; + +const KintoneLanguageDetector: LanguageDetectorModule = { + type: 'languageDetector', + // init: () => {}, + detect: () => kintone.getLoginUser().language, + // cacheUserLanguage: () => {}, +}; + +export default KintoneLanguageDetector; diff --git a/src/common/kintoneUtils.ts b/src/common/kintoneUtils.ts deleted file mode 100644 index 7cd9920..0000000 --- a/src/common/kintoneUtils.ts +++ /dev/null @@ -1,9 +0,0 @@ -import invariant from 'tiny-invariant'; - -const KintoneUserLanguages = ['en', 'ja', 'zh', 'zh-TW', 'es', 'pt-BR', 'th'] as const; -export type KintoneUserLanguages = (typeof KintoneUserLanguages)[number]; -export const LANGUAGE = kintone.getLoginUser().language as KintoneUserLanguages; -invariant( - KintoneUserLanguages.includes(LANGUAGE), - `Unsupported language: ${LANGUAGE}. Supported languages are: ${KintoneUserLanguages.join(', ')}`, -); diff --git a/src/config/Settings.tsx b/src/config/Settings.tsx index 363c405..71be54e 100644 --- a/src/config/Settings.tsx +++ b/src/config/Settings.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { KintoneRestAPIClient } from '@kintone/rest-api-client'; import moize from 'moize'; +import { useTranslation } from 'react-i18next'; import invariant from 'tiny-invariant'; import { naturalCompare } from '../common/stringUtils'; import { KintoneFormFieldProperties } from '../common/types'; @@ -18,7 +19,7 @@ import styles from './Settings.module.css'; const cachedFormFieldsProperties = moize.promise(async (appId: number): Promise => { const client = new KintoneRestAPIClient(); - const { properties } = await client.app.getFormFields({ app: appId, lang: 'en', preview: false }); + const { properties } = await client.app.getFormFields({ app: appId, preview: true }); return properties; }); @@ -28,6 +29,7 @@ interface SettingsProps { const Settings: React.FC = (props) => { const { pluginId } = props; + const { t } = useTranslation(); const config = kintone.plugin.app.getConfig(pluginId); const appId = kintone.app.getId(); invariant(appId, 'The app ID is not available. Please ensure you are on a Kintone app page.'); @@ -36,7 +38,7 @@ const Settings: React.FC = (props) => { .filter((property) => property.type === 'FILE') .sort((a, b) => naturalCompare(`${a.label} (${a.code})`, `${b.label} (${b.code})`)); const options: KintonePluginSelectOptionData[] = [ - { value: '', label: 'Select a File field', disabled: true }, // Default option + { value: '', label: t('settings.template.messages.select-an-attachment-fields'), disabled: true }, // Default option ...fileFields.map((property) => ({ value: property.code, label: `${property.label} (${property.code})`, @@ -50,7 +52,7 @@ const Settings: React.FC = (props) => { const handleOnSubmit = (e: React.FormEvent) => { e.preventDefault(); kintone.plugin.app.setConfig({ template }, () => { - alert('The plug-in settings have been saved. Please update the app!'); + alert(t('on-saved')); window.location.href = `../../flow?app=${appId}`; }); }; @@ -65,27 +67,26 @@ const Settings: React.FC = (props) => { return (
- Settings for the Kintone Word output plugin + {t('title')}
- Template* + {t('settings.template.title')} + * - Select a file field that contains the Word template file. + {t('settings.template.description')} {fileFields.length === 0 ? ( - - No file fields found in the app. Please add a file field to use this plugin. - + {t('settings.template.errors.no-attachment-field-found')} ) : ( )} - Cancel + {t('buttons.cancel')} - Save + {t('buttons.save')}
diff --git a/src/config/i18n.ts b/src/config/i18n.ts new file mode 100644 index 0000000..6b03e0c --- /dev/null +++ b/src/config/i18n.ts @@ -0,0 +1,27 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import kintoneLanguageDetector from '../common/kintoneLanguageDetector'; + +import enTranslation from './locales/en.json'; +import jaTranslation from './locales/ja.json'; + +i18n + .use(kintoneLanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: 'en', + debug: true, + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, + resources: { + en: { + translation: enTranslation, + }, + ja: { + translation: jaTranslation, + }, + }, + }); + +export default i18n; diff --git a/src/config/index.tsx b/src/config/index.tsx index de63d1b..77e72e7 100644 --- a/src/config/index.tsx +++ b/src/config/index.tsx @@ -6,6 +6,8 @@ import ConfigApp from './ConfigApp'; import '../common/ui/51-modern-default.css'; +import './i18n'; + ((PLUGIN_ID) => { const root = document.getElementById('plugin-config-root'); invariant(root, 'The plugin configuration root element "plugin-config-root" is not found.'); diff --git a/src/config/locales/en.json b/src/config/locales/en.json new file mode 100644 index 0000000..ce380c3 --- /dev/null +++ b/src/config/locales/en.json @@ -0,0 +1,20 @@ +{ + "title": "Settings for the Kintone Word output plugin", + "settings": { + "template": { + "title": "Template", + "description": "Select a attachment field that contains the Word template file.", + "messages": { + "select-an-attachment-fields": "(Select an attachment field)" + }, + "errors": { + "no-attachment-field-found": "No attachment fields found in the app. Please add an attachment field to use this plugin." + } + } + }, + "buttons": { + "save": "Save", + "cancel": "Cancel" + }, + "on-saved": "The plug-in settings have been saved. Please update the app!" +} diff --git a/src/config/locales/ja.json b/src/config/locales/ja.json new file mode 100644 index 0000000..37d2667 --- /dev/null +++ b/src/config/locales/ja.json @@ -0,0 +1,20 @@ +{ + "title": "Word出力プラグイン設定", + "settings": { + "template": { + "title": "雛形ファイル", + "description": "Word雛形ファイルを保存する添付ファイルフィールドを選択してください。", + "messages": { + "select-an-attachment-fields": "(選択してください)" + }, + "errors": { + "no-attachment-field-found": "このアプリに添付ファイルフィールドが見つかりません。フォームに添付ファイルフィールドを追加してください。" + } + } + }, + "buttons": { + "save": "保存", + "cancel": "キャンセル" + }, + "on-saved": "設定を保存しました。アプリを更新してください。" +} diff --git a/src/desktop/MenuPanel.tsx b/src/desktop/MenuPanel.tsx index 97483a0..a311b0f 100644 --- a/src/desktop/MenuPanel.tsx +++ b/src/desktop/MenuPanel.tsx @@ -8,9 +8,9 @@ import expressionParser from 'docxtemplater/expressions'; import { saveAs } from 'file-saver'; import moize from 'moize'; import PizZip from 'pizzip'; +import { useTranslation } from 'react-i18next'; import invariant from 'tiny-invariant'; import { DOCX_CONTENT_TYPE } from '../common/constants'; -import { LANGUAGE } from '../common/kintoneUtils'; import { KintoneFormFieldProperties } from '../common/types'; import KintonePluginAlert from '../common/ui/KintonePluginAlert'; import KintonePluginButton from '../common/ui/KintonePluginButton'; @@ -26,7 +26,7 @@ interface MenuPanelProps { const cachedFormFieldsProperties = moize.promise(async (appId: number): Promise => { const client = new KintoneRestAPIClient(); - const { properties } = await client.app.getFormFields({ app: appId, lang: 'en', preview: false }); + const { properties } = await client.app.getFormFields({ app: appId, preview: false }); return properties; }); @@ -72,7 +72,11 @@ const formatNumberRecordValue = ( return formatValueWithUnit(formattedValue, property); }; -const formatCalculatedRecordValue = (value: string, property: KintoneFormFieldProperty.Calc): string => { +const formatCalculatedRecordValue = ( + value: string, + property: KintoneFormFieldProperty.Calc, + language: string, +): string => { const { format } = property; if (value === '') { return ''; @@ -108,7 +112,7 @@ const formatCalculatedRecordValue = (value: string, property: KintoneFormFieldPr !isNaN(hours) && !isNaN(minutes), `Expected hours and minutes to be numbers, but got hours: ${hours}, minutes: ${minutes}`, ); - formattedValue = LANGUAGE === 'ja' ? `${hours}時間${minutes}分` : `${hours} hours ${minutes} minutes`; + formattedValue = language === 'ja' ? `${hours}時間${minutes}分` : `${hours} hours ${minutes} minutes`; } else if (format === 'DAY_HOUR_MINUTE') { // "49:30" -> en: "2 days 1 hour 30 minutes", ja: "2日1時間30分 const [hours, minutes] = value.split(':').map((v) => Number(v)); @@ -119,7 +123,7 @@ const formatCalculatedRecordValue = (value: string, property: KintoneFormFieldPr const days = Math.floor(Number(hours) / 24); const remainingHours = Number(hours) % 24; formattedValue = - LANGUAGE === 'ja' + language === 'ja' ? `${days}日${remainingHours}時間${minutes}分` : `${days} days ${remainingHours} hours ${minutes} minutes`; } else { @@ -129,7 +133,11 @@ const formatCalculatedRecordValue = (value: string, property: KintoneFormFieldPr return formatValueWithUnit(formattedValue, property); }; -const record2data = (properties: KintoneFormFieldProperties, record: Partial): TemplateData => { +const record2data = ( + properties: KintoneFormFieldProperties, + record: Partial, + language: string, +): TemplateData => { const data: TemplateData = {}; for (const key in record) { if (Object.prototype.hasOwnProperty.call(record, key) && Object.prototype.hasOwnProperty.call(properties, key)) { @@ -149,7 +157,7 @@ const record2data = (properties: KintoneFormFieldProperties, record: Partial v); } else if ( @@ -163,7 +171,7 @@ const record2data = (properties: KintoneFormFieldProperties, record: Partial record2data(property.fields, subRecord.value)); + data[key] = value.map((subRecord) => record2data(property.fields, subRecord.value, language)); } else if (type === 'FILE') { invariant(property.type === 'FILE', `Expected property type to be FILE, but got ${property.type}`); data[key] = value.map((file) => ({ @@ -184,31 +192,46 @@ const record2data = (properties: KintoneFormFieldProperties, record: Partial = (props) => { const { pluginId, event } = props; + const { t } = useTranslation(); const appId = event.appId; const properties = React.use(cachedFormFieldsProperties(appId)); + const language = kintone.getLoginUser().language; const config = kintone.plugin.app.getConfig(pluginId); const template: string = config.template ?? ''; + if (template === '') { return ( - Word output plugin: Template field is not set. Please configure the plugin. + {t('name')}: {t('errors.template-field-is-not-set')} ); } const record = event.record[template]; if (record == null) { - return Word output plugin: Template field is not available in this app.; + return ( + + {t('name')}: {t('errors.template-field-is-not-available')} + + ); } if (record.type !== 'FILE') { - return Word output plugin: Template field must be a file field.; + return ( + + {t('name')}: {t('errors.template-field-must-be-an-attachment-field')} + + ); } if (record.value.length === 0) { - return Word output plugin: Template field does not contain any files.; + return ( + + {t('name')}: {t('errors.template-field-does-not-contain-any-files')} + + ); } if (record.value.length > 1) { return ( - Word output plugin: Template field contains multiple files. Please ensure it contains only one file. + {t('name')}: {t('errors.template-field-contains-multiple-files')} ); } @@ -216,7 +239,7 @@ const MenuPanel: React.FC = (props) => { if (contentType !== DOCX_CONTENT_TYPE) { return ( - Word output plugin: The template file must be a DOCX file. The current file type is {contentType}. + {t('name')}: {t('errors.template-file-must-be-a-docx', { contentType })} ); } @@ -234,7 +257,7 @@ const MenuPanel: React.FC = (props) => { parser: expressionParser, nullGetter: () => '', }); - doc.render(record2data(properties, event.record)); + doc.render(record2data(properties, event.record, language)); const out = doc.getZip().generate({ type: 'blob', mimeType: DOCX_CONTENT_TYPE }); saveAs(out, 'output.docx'); }) @@ -246,7 +269,7 @@ const MenuPanel: React.FC = (props) => { return ( - Word出力 + {t('buttons.output')} ); }; diff --git a/src/desktop/i18n.ts b/src/desktop/i18n.ts new file mode 100644 index 0000000..6b03e0c --- /dev/null +++ b/src/desktop/i18n.ts @@ -0,0 +1,27 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import kintoneLanguageDetector from '../common/kintoneLanguageDetector'; + +import enTranslation from './locales/en.json'; +import jaTranslation from './locales/ja.json'; + +i18n + .use(kintoneLanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: 'en', + debug: true, + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, + resources: { + en: { + translation: enTranslation, + }, + ja: { + translation: jaTranslation, + }, + }, + }); + +export default i18n; diff --git a/src/desktop/index.tsx b/src/desktop/index.tsx index 6f26fd9..ce2cb15 100644 --- a/src/desktop/index.tsx +++ b/src/desktop/index.tsx @@ -6,6 +6,8 @@ import DesktopApp from './DesktopApp'; import '../common/ui/51-modern-default.css'; +import './i18n'; + ((PLUGIN_ID) => { kintone.events.on( ['app.record.detail.show', 'mobile.app.record.detail.show'], diff --git a/src/desktop/locales/en.json b/src/desktop/locales/en.json new file mode 100644 index 0000000..76dbecd --- /dev/null +++ b/src/desktop/locales/en.json @@ -0,0 +1,14 @@ +{ + "name": "Word output plugin", + "errors": { + "template-field-is-not-set": "Template field is not set. Please configure the plugin.", + "template-field-is-not-available": "Template field is not available in this app.", + "template-field-must-be-an-attachment-field": "Template field must be an attachment field.", + "template-field-does-not-contain-any-files": "Template field does not contain any files.", + "template-field-contains-multiple-files": "Template field contains multiple files. Please ensure it contains only one file.", + "template-file-must-be-a-docx": "The template file must be a Word (.docx) file. 'Mime-Type: {{contentType}}'." + }, + "buttons": { + "output": "Output Word" + } +} diff --git a/src/desktop/locales/ja.json b/src/desktop/locales/ja.json new file mode 100644 index 0000000..51ba155 --- /dev/null +++ b/src/desktop/locales/ja.json @@ -0,0 +1,14 @@ +{ + "name": "Word出力プラグイン", + "errors": { + "template-field-is-not-set": "雛形ファイルが設定されていません。プラグインの設定を見直してください。", + "template-field-is-not-available": "雛形ファイルに設定された添付ファイルフィールドが見つかりませんでした。プラグインの設定を見直してください。", + "template-field-must-be-an-attachment-field": "雛形ファイルに設定されたフィールド型が添付ファイルフィールドではありません。プラグインの設定を見直してください。", + "template-field-does-not-contain-any-files": "雛形ファイルが添付されていません。", + "template-field-contains-multiple-files": "雛形ファイルとして複数登録されています。1つのみを指定してください。", + "template-file-must-be-a-docx": "雛形ファイルがWord形式(.docx)ではありません。'Mime-Type: {{contentType}}'" + }, + "buttons": { + "output": "Word出力" + } +}