From 31faf1d12e47331a9e52811b865031c61ea033fa Mon Sep 17 00:00:00 2001 From: hubcio2115 Date: Thu, 26 Sep 2024 11:11:22 +0200 Subject: [PATCH 01/34] installed i18next and support packages --- apps/web/package.json | 6 ++ pnpm-lock.yaml | 147 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 135 insertions(+), 18 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 33b7726e..b4e508c4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -42,6 +42,7 @@ "@total-typescript/ts-reset": "^0.5.1", "@uidotdev/usehooks": "^2.4.1", "@vercel/postgres": "^0.9.0", + "accept-language": "^3.0.20", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -50,6 +51,9 @@ "drizzle-zod": "^0.5.1", "geist": "^1.3.1", "google-auth-library": "^9.14.0", + "i18next": "^23.15.1", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-resources-to-backend": "^1.2.1", "jiti": "^1.21.6", "ky": "^1.7.1", "lucide-react": "^0.412.0", @@ -58,8 +62,10 @@ "next-themes": "^0.3.0", "pg": "^8.12.0", "react": "18.3.1", + "react-cookie": "^7.2.0", "react-dom": "18.3.1", "react-hook-form": "^7.52.2", + "react-i18next": "^15.0.2", "tailwind-merge": "^2.5.1", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c870b6f9..613f03a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: '@vercel/postgres': specifier: ^0.9.0 version: 0.9.0 + accept-language: + specifier: ^3.0.20 + version: 3.0.20 class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -128,6 +131,15 @@ importers: google-auth-library: specifier: ^9.14.0 version: 9.14.0 + i18next: + specifier: ^23.15.1 + version: 23.15.1 + i18next-browser-languagedetector: + specifier: ^8.0.0 + version: 8.0.0 + i18next-resources-to-backend: + specifier: ^1.2.1 + version: 1.2.1 jiti: specifier: ^1.21.6 version: 1.21.6 @@ -152,12 +164,18 @@ importers: react: specifier: 18.3.1 version: 18.3.1 + react-cookie: + specifier: ^7.2.0 + version: 7.2.0(react@18.3.1) react-dom: specifier: 18.3.1 version: 18.3.1(react@18.3.1) react-hook-form: specifier: ^7.52.2 version: 7.53.0(react@18.3.1) + react-i18next: + specifier: ^15.0.2 + version: 15.0.2(i18next@23.15.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwind-merge: specifier: ^2.5.1 version: 2.5.2 @@ -1872,6 +1890,9 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/hoist-non-react-statics@3.3.5': + resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -2098,6 +2119,9 @@ packages: '@vitest/utils@2.0.5': resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} + accept-language@3.0.20: + resolution: {integrity: sha512-xklPzRma4aoDEPk0ZfMjeuxB2FP4JBYlAR25OFUqCoOYDjYo6wGwAs49SnTN/MoB5VpnNX9tENfZ+vEIFmHQMQ==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2229,6 +2253,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + bcp47@1.1.2: + resolution: {integrity: sha512-JnkkL4GUpOvvanH9AZPX38CxhiLsXMBicBY2IAtqiVN8YulGDQybUydWA4W6yAMtw6iShtw+8HEF6cfrTHU+UQ==} + engines: {node: '>=0.10'} + bignumber.js@9.1.2: resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} @@ -3113,6 +3141,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -3120,6 +3151,9 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -3132,6 +3166,15 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} + i18next-browser-languagedetector@8.0.0: + resolution: {integrity: sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==} + + i18next-resources-to-backend@1.2.1: + resolution: {integrity: sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==} + + i18next@23.15.1: + resolution: {integrity: sha512-wB4abZ3uK7EWodYisHl/asf8UYEhrI/vj/8aoSsrj/ZDxj4/UXPOa1KvFt1Fq5hkUHquNqwFlDprmjZ8iySgYA==} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -3982,6 +4025,11 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-cookie@7.2.0: + resolution: {integrity: sha512-mqhPERUyfOljq5yJ4woDFI33bjEtigsl8JDJdPPeNhr0eSVZmBc/2Vdf8mFxOUktQxhxTR1T+uF0/FRTZyBEgw==} + peerDependencies: + react: '>= 16.3.0' + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -3993,6 +4041,19 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-i18next@15.0.2: + resolution: {integrity: sha512-z0W3/RES9Idv3MmJUcf0mDNeeMOUXe+xoL0kPfQPbDoZHmni/XsIoq5zgT2MCFUiau283GuBUK578uD/mkAbLQ==} + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4498,6 +4559,9 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + universal-cookie@7.2.0: + resolution: {integrity: sha512-PvcyflJAYACJKr28HABxkGemML5vafHmiL4ICe3e+BEKXRMt0GaFLZhAwgv637kFFnnfiSJ8e6jknrKkMrU+PQ==} + universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -4612,6 +4676,10 @@ packages: jsdom: optional: true + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -6140,6 +6208,11 @@ snapshots: '@types/estree@1.0.5': {} + '@types/hoist-non-react-statics@3.3.5': + dependencies: + '@types/react': 18.3.5 + hoist-non-react-statics: 3.3.2 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -6399,10 +6472,10 @@ snapshots: '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4) '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.5.4) eslint-config-prettier: 9.1.0(eslint@8.57.0) - eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)) + eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)) eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-playwright: 0.16.0(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0) @@ -6468,6 +6541,10 @@ snapshots: loupe: 3.1.1 tinyrainbow: 1.2.0 + accept-language@3.0.20: + dependencies: + bcp47: 1.1.2 + acorn-jsx@5.3.2(acorn@8.12.1): dependencies: acorn: 8.12.1 @@ -6619,6 +6696,8 @@ snapshots: base64-js@1.5.1: {} + bcp47@1.1.2: {} + bignumber.js@9.1.2: {} binary-extensions@2.3.0: {} @@ -7149,9 +7228,9 @@ snapshots: eslint: 8.57.0 eslint-plugin-turbo: 2.1.1(eslint@8.57.0) - eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)): + eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)): dependencies: - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) eslint-import-resolver-node@0.3.9: dependencies: @@ -7192,7 +7271,7 @@ snapshots: is-bun-module: 1.1.0 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node @@ -7210,17 +7289,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.2(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.5.4) - eslint: 8.57.0 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0) - transitivePeerDependencies: - - supports-color - eslint-module-utils@2.8.2(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 @@ -7251,7 +7319,7 @@ snapshots: eslint: 8.57.0 ignore: 5.3.2 - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -7261,7 +7329,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.2(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.2(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -7767,12 +7835,20 @@ snapshots: dependencies: function-bind: 1.1.2 + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + hosted-git-info@2.8.9: {} html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1 @@ -7789,6 +7865,18 @@ snapshots: human-signals@5.0.0: {} + i18next-browser-languagedetector@8.0.0: + dependencies: + '@babel/runtime': 7.25.0 + + i18next-resources-to-backend@1.2.1: + dependencies: + '@babel/runtime': 7.25.0 + + i18next@23.15.1: + dependencies: + '@babel/runtime': 7.25.0 + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -8519,6 +8607,13 @@ snapshots: queue-microtask@1.2.3: {} + react-cookie@7.2.0(react@18.3.1): + dependencies: + '@types/hoist-non-react-statics': 3.3.5 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + universal-cookie: 7.2.0 + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -8529,6 +8624,15 @@ snapshots: dependencies: react: 18.3.1 + react-i18next@15.0.2(i18next@23.15.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.25.0 + html-parse-stringify: 3.0.1 + i18next: 23.15.1 + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + react-is@16.13.1: {} react-is@17.0.2: {} @@ -9083,6 +9187,11 @@ snapshots: undici-types@5.26.5: {} + universal-cookie@7.2.0: + dependencies: + '@types/cookie': 0.6.0 + cookie: 0.6.0 + universalify@0.2.0: {} update-browserslist-db@1.1.0(browserslist@4.23.3): @@ -9191,6 +9300,8 @@ snapshots: - supports-color - terser + void-elements@3.1.0: {} + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 From 520b5a54492e8e3e4d9fd80087e861a3344bdb95 Mon Sep 17 00:00:00 2001 From: hubcio2115 Date: Thu, 26 Sep 2024 11:11:37 +0200 Subject: [PATCH 02/34] added configs for i18n --- apps/web/src/app/i18n/client.ts | 77 +++++++++++++++++++++++++++++++ apps/web/src/app/i18n/index.ts | 49 ++++++++++++++++++++ apps/web/src/app/i18n/settings.ts | 19 ++++++++ 3 files changed, 145 insertions(+) create mode 100644 apps/web/src/app/i18n/client.ts create mode 100644 apps/web/src/app/i18n/index.ts create mode 100644 apps/web/src/app/i18n/settings.ts diff --git a/apps/web/src/app/i18n/client.ts b/apps/web/src/app/i18n/client.ts new file mode 100644 index 00000000..69f2287f --- /dev/null +++ b/apps/web/src/app/i18n/client.ts @@ -0,0 +1,77 @@ +"use client"; + +import { useEffect, useState } from "react"; +import i18next from "i18next"; +import type { FlatNamespace, KeyPrefix } from "i18next"; +import { + initReactI18next, + useTranslation as useTranslationOrg, +} from "react-i18next"; +import type { + UseTranslationOptions, + UseTranslationResponse, + FallbackNs, +} from "react-i18next"; +import { useCookies } from "react-cookie"; +import resourcesToBackend from "i18next-resources-to-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; +import { getOptions, LANGUAGES, COOKIE_NAME } from "./settings"; + +const runsOnServerSide = typeof window === "undefined"; + +// on client side the normal singleton is ok +i18next + .use(initReactI18next) + .use(LanguageDetector) + .use( + resourcesToBackend( + (language: string, namespace: string) => + import(`./locales/${language}/${namespace}.json`), + ), + ) + .init({ + ...getOptions(), + lng: undefined, // let detect the language on client side + detection: { + order: ["path", "htmlTag", "cookie", "navigator"], + }, + preload: runsOnServerSide ? LANGUAGES : [], + }); + +export function useTranslation< + Ns extends FlatNamespace, + KPrefix extends KeyPrefix> = undefined, +>( + lng: string, + ns?: Ns, + options?: UseTranslationOptions, +): UseTranslationResponse, KPrefix> { + const [cookies, setCookie] = useCookies([COOKIE_NAME]); + const ret = useTranslationOrg(ns, options); + const { i18n } = ret; + + if (runsOnServerSide && lng && i18n.resolvedLanguage !== lng) { + i18n.changeLanguage(lng); + } else { + const [activeLng, setActiveLng] = useState(i18n.resolvedLanguage); + + useEffect(() => { + if (activeLng !== i18n.resolvedLanguage) return; + + setActiveLng(i18n.resolvedLanguage); + }, [activeLng, i18n.resolvedLanguage]); + + useEffect(() => { + if (!lng || i18n.resolvedLanguage === lng) return; + + i18n.changeLanguage(lng); + }, [lng, i18n]); + + useEffect(() => { + if (cookies.i18next === lng) return; + + setCookie(COOKIE_NAME, lng, { path: "/" }); + }, [lng, cookies.i18next]); + } + return ret; +} diff --git a/apps/web/src/app/i18n/index.ts b/apps/web/src/app/i18n/index.ts new file mode 100644 index 00000000..70a5d5aa --- /dev/null +++ b/apps/web/src/app/i18n/index.ts @@ -0,0 +1,49 @@ +import { + createInstance, + type FlatNamespace, + type KeyPrefix, + type Namespace, +} from "i18next"; +import { type FallbackNs } from "react-i18next"; +import resourcesToBackend from "i18next-resources-to-backend"; +import { initReactI18next } from "react-i18next/initReactI18next"; +import { getOptions, type SupportedLanguages } from "./settings"; + +async function initI18next(lang: SupportedLanguages, ns: string | string[]) { + const i18nInstance = createInstance(); + await i18nInstance + .use(initReactI18next) + .use( + resourcesToBackend( + (language: string, namespace: string) => + import(`./locales/${language}/${namespace}.json`), + ), + ) + .init(getOptions(lang, ns)); + return i18nInstance; +} + +type $Tuple = readonly [T?, ...T[]]; +type $FirstNamespace = Ns extends readonly any[] + ? Ns[0] + : Ns; + +export async function translation< + Ns extends FlatNamespace | $Tuple, + KPrefix extends KeyPrefix< + FallbackNs< + Ns extends FlatNamespace ? FlatNamespace : $FirstNamespace + > + > = undefined, +>(lang: SupportedLanguages, ns?: Ns, options: { keyPrefix?: KPrefix } = {}) { + const i18nextInstance = await initI18next( + lang, + Array.isArray(ns) ? (ns as string[]) : (ns as string), + ); + return { + t: Array.isArray(ns) + ? i18nextInstance.getFixedT(lang, ns[0], options.keyPrefix) + : i18nextInstance.getFixedT(lang, ns as FlatNamespace, options.keyPrefix), + i18n: i18nextInstance, + }; +} diff --git a/apps/web/src/app/i18n/settings.ts b/apps/web/src/app/i18n/settings.ts new file mode 100644 index 00000000..34613909 --- /dev/null +++ b/apps/web/src/app/i18n/settings.ts @@ -0,0 +1,19 @@ +export const FALLBACK_LANG = "en"; +export const LANGUAGES = ["en", "pl"] as const; +export const DEFAULT_NS = "translation"; +export const COOKIE_NAME = "i18next"; + +export type SupportedLanguages = (typeof LANGUAGES)[number]; + +export function getOptions (lang: SupportedLanguages = FALLBACK_LANG, ns: string | string[] = DEFAULT_NS) { + return { + debug: true, + supportedLngs: LANGUAGES, + // preload: languages, + fallbackLng: FALLBACK_LANG, + lng: lang, + fallbackNS: DEFAULT_NS, + defaultNS: DEFAULT_NS, + ns, + } +} From c31378fd8b431c76d612bcc0dd42d242becba151 Mon Sep 17 00:00:00 2001 From: hubcio2115 Date: Thu, 26 Sep 2024 11:12:00 +0200 Subject: [PATCH 03/34] home-page translations --- .../src/app/i18n/locales/en/home-page.json | 70 +++++++++++++++++++ .../src/app/i18n/locales/pl/home-page.json | 70 +++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 apps/web/src/app/i18n/locales/en/home-page.json create mode 100644 apps/web/src/app/i18n/locales/pl/home-page.json diff --git a/apps/web/src/app/i18n/locales/en/home-page.json b/apps/web/src/app/i18n/locales/en/home-page.json new file mode 100644 index 00000000..227b7b72 --- /dev/null +++ b/apps/web/src/app/i18n/locales/en/home-page.json @@ -0,0 +1,70 @@ +{ + "hero": { + "title": "Tool to streamline your video production pipeline", + "description": "Giving feedback, suggestions and managing your channel's access don't get easier than this. Inspired by Git a tool for iterating on software that actually works.", + "learn_more": "Learn more" + }, + "feature_section": { + "subtitle": "Upload faster", + "title": "Everything you need to upload your new video", + "description": "Quis tellus eget adipiscing convallis sit sit eget aliquet quis. Suspendisse eget egestas a elementum pulvinar et feugiat blandit at. In mi viverra elit nunc.", + "features": [ + { + "name": "Merge to upload", + "description": "If you know what git flow is you exactly know what this tool's about. You'll get comfortable real quick." + }, + { + "name": "Secure", + "description": "You finally have a tool to manage access you your channel in a way that gives you security and doesn't restrain your flow." + }, + { + "name": "Simple flow", + "description": "We didn't invent anything new, we took something that works and translated it to a new area." + }, + { + "name": "We don't track you", + "description": "We store as little about you as necessary, your access keys are encrypted." + } + ] + }, + "pricing_section": { + "monthly": "Monthly", + "yearly": "Yearly", + "buy_plan_button": "Buy plan", + "pricing_plans": [ + { + "title": "Small channel", + "description": "The essentials for running a small or an up and comming channel.", + "price": 50, + "buttonText": "Buy plan", + "features": [ + "Full access to our tools", + "You + 2 seats", + "Up to 1 channel" + ] + }, + { + "title": "Small network of channels", + "description": "Everything you need to manage small organization.", + "price": 250, + "buttonText": "Buy plan", + "features": [ + "Everything in Small channel plan", + "You + 10 seats", + "Up to 3 channels" + ] + }, + { + "title": "Enterprise", + "description": "A plan tailored to the exact needs of your company.", + "price": "Custom", + "buttonText": "Contact sales", + "features": [ + "Everything in Small network of channels", + "You + any number of seats you'd like", + "You specify the number of channels" + ] + } + ] + } +} diff --git a/apps/web/src/app/i18n/locales/pl/home-page.json b/apps/web/src/app/i18n/locales/pl/home-page.json new file mode 100644 index 00000000..8cfb0b18 --- /dev/null +++ b/apps/web/src/app/i18n/locales/pl/home-page.json @@ -0,0 +1,70 @@ +{ + "hero": { + "title": "Narzędzie do usprawnienia procesu produkcji wideo", + "description": "Dawanie opinii, sugestii i zarządzanie dostępem do Twojego kanału nigdy nie było łatwiejsze. Zainspirowane Gitem – narzędziem do iteracyjnego tworzenia oprogramowania, które faktycznie działa.", + "learn_more": "Dowiedz się więcej" + }, + "feature_section": { + "subtitle": "Przesyłaj szybciej", + "title": "Wszystko, czego potrzebujesz, aby przesłać nowe wideo", + "description": "Quis tellus eget adipiscing convallis sit sit eget aliquet quis. Suspendisse eget egestas a elementum pulvinar et feugiat blandit at. In mi viverra elit nunc.", + "features": [ + { + "name": "Scalanie do przesłania", + "description": "Jeśli wiesz, czym jest git flow, to dokładnie wiesz, o co chodzi w tym narzędziu. Szybko się z nim oswoisz." + }, + { + "name": "Bezpieczeństwo", + "description": "W końcu masz narzędzie do zarządzania dostępem do swojego kanału w sposób, który zapewnia bezpieczeństwo i nie ogranicza Twojego przepływu pracy." + }, + { + "name": "Prosty przepływ", + "description": "Nie wymyśliliśmy nic nowego, wzięliśmy coś, co działa, i przenieśliśmy to na nowy obszar." + }, + { + "name": "Nie śledzimy Cię", + "description": "Przechowujemy o Tobie tylko tyle, ile to konieczne, Twoje klucze dostępu są zaszyfrowane." + } + ] + }, + "pricing_section": { + "monthly": "Miesięcznie", + "yearly": "Rocznie", + "buy_plan_button": "Kup plan", + "pricing_plans": [ + { + "title": "Mały kanał", + "description": "Podstawowe narzędzia do prowadzenia małego lub rozwijającego się kanału.", + "price": 50, + "buttonText": "Kup plan", + "features": [ + "Pełny dostęp do naszych narzędzi", + "Ty + 2 dodatkowe miejsca", + "Do 1 kanału" + ] + }, + { + "title": "Mała sieć kanałów", + "description": "Wszystko, czego potrzebujesz do zarządzania małą organizacją.", + "price": 250, + "buttonText": "Kup plan", + "features": [ + "Wszystko w planie Mały kanał", + "Ty + 10 dodatkowych miejsc", + "Do 3 kanałów" + ] + }, + { + "title": "Przedsiębiorstwo", + "description": "Plan dostosowany do specyficznych potrzeb Twojej firmy.", + "price": "Indywidualnie", + "buttonText": "Skontaktuj się z działem sprzedaży", + "features": [ + "Wszystko w planie Mała sieć kanałów", + "Ty + dowolna liczba dodatkowych miejsc", + "Ty określasz liczbę kanałów" + ] + } + ] + } +} From 0326daa74c67ef1ade4ac0abd19b573b3eab3ca3 Mon Sep 17 00:00:00 2001 From: hubcio2115 Date: Thu, 26 Sep 2024 11:12:14 +0200 Subject: [PATCH 04/34] dashboard overview translations --- apps/web/src/app/i18n/locales/en/overview.json | 4 ++++ apps/web/src/app/i18n/locales/pl/overview.json | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 apps/web/src/app/i18n/locales/en/overview.json create mode 100644 apps/web/src/app/i18n/locales/pl/overview.json diff --git a/apps/web/src/app/i18n/locales/en/overview.json b/apps/web/src/app/i18n/locales/en/overview.json new file mode 100644 index 00000000..e842da35 --- /dev/null +++ b/apps/web/src/app/i18n/locales/en/overview.json @@ -0,0 +1,4 @@ +{ + "search_placeholder": "Filter by title...", + "create_project_button": "Add video" +} diff --git a/apps/web/src/app/i18n/locales/pl/overview.json b/apps/web/src/app/i18n/locales/pl/overview.json new file mode 100644 index 00000000..88b3b3be --- /dev/null +++ b/apps/web/src/app/i18n/locales/pl/overview.json @@ -0,0 +1,4 @@ +{ + "search_placeholder": "Wyszukaj po tytule...", + "create_project_button": "Dodaj film" +} From 3a2e53d77b5e20a187b3d7fd02da497f052d2752 Mon Sep 17 00:00:00 2001 From: hubcio2115 Date: Thu, 26 Sep 2024 11:12:25 +0200 Subject: [PATCH 05/34] generic translations --- .../src/app/i18n/locales/en/translation.json | 18 ++++++++++++++++++ .../src/app/i18n/locales/pl/translation.json | 12 ++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 apps/web/src/app/i18n/locales/en/translation.json create mode 100644 apps/web/src/app/i18n/locales/pl/translation.json diff --git a/apps/web/src/app/i18n/locales/en/translation.json b/apps/web/src/app/i18n/locales/en/translation.json new file mode 100644 index 00000000..b8beeeb7 --- /dev/null +++ b/apps/web/src/app/i18n/locales/en/translation.json @@ -0,0 +1,18 @@ +{ + "navbar": { + "dashboard_button": "Go to Dashboard", + "logout_button": "Logout", + "signin_button": "Sign In", + "add_new_organization_button": "Add New new" + }, + + "dashnav": { + "overview_button": "Overview", + "settings_button": "Settings" + }, + + "alert_modal": { + "confirm_button": "Confirm", + "cancel_button": "Cancel" + } +} diff --git a/apps/web/src/app/i18n/locales/pl/translation.json b/apps/web/src/app/i18n/locales/pl/translation.json new file mode 100644 index 00000000..2d3356c2 --- /dev/null +++ b/apps/web/src/app/i18n/locales/pl/translation.json @@ -0,0 +1,12 @@ +{ + "navbar": { + "dashboard_button": "Przejdź do pulpitu", + "logout_button": "Wyloguj", + "signin_button": "Zaloguj się", + "add_new_organization_button": "Dodaj nową organizację" + }, + "dashnav": { + "overview_button": "Przegląd", + "settings_button": "Ustawienia" + } +} From 837a89e94dcd3b7d3c609f3236d46113c3487e2a Mon Sep 17 00:00:00 2001 From: hubcio2115 Date: Thu, 26 Sep 2024 11:12:35 +0200 Subject: [PATCH 06/34] settings translation --- .../web/src/app/i18n/locales/en/settings.json | 98 +++++++++++++++++++ .../web/src/app/i18n/locales/pl/settings.json | 9 ++ 2 files changed, 107 insertions(+) create mode 100644 apps/web/src/app/i18n/locales/en/settings.json create mode 100644 apps/web/src/app/i18n/locales/pl/settings.json diff --git a/apps/web/src/app/i18n/locales/en/settings.json b/apps/web/src/app/i18n/locales/en/settings.json new file mode 100644 index 00000000..e92c87ae --- /dev/null +++ b/apps/web/src/app/i18n/locales/en/settings.json @@ -0,0 +1,98 @@ +{ + "success": "Success", + "error": "Error", + + "general": { + "title": "General", + "description": "Settings and options for the <1>{{name}} organization.", + "name_form": { + "label": "Organization name", + "submit_button": "Save changes" + }, + + "delete_section": { + "title": "Delete organization", + "description": "Please note that deletion of <1>{{name}} is <2>permanent and <3>cannot be undone.", + + "default_org": { + "title": "Default organization", + "description": "The default organization <1>cannot be deleted." + }, + + "delete_modal": { + "title": "Are you absolutely sure?", + "description": "This will permanently delete the organization and all its data. Enter the name of the organization \"{{name}}\" to confirm." + } + }, + + "toast": { + "delete": { + "success": "Organization deleted successfully.", + "error": "Couldn't delete organization: {{error}}" + }, + + "update_name": { + "success": "Organization name updated to {{name}}.", + "error": "Failed to update organization name: {{error}}" + } + } + }, + + "members": { + "title": "Members", + "description": "All members and administrators with access to the organization.", + + "add_member_button": "Invite member", + "member_table": { + "columns": { + "member": "Member", + "role": "Role" + }, + + "roles": { + "admin": "admin", + "owner": "owner", + "user": "user" + } + }, + + "leave_section": { + "leave_button": "Leave", + "remove_button": "Remove", + + "alert": { + "title": "Are you absolutely sure?", + "description": "You will transter the ownership of the organization to the selected user. Your role will be changed to admin" + } + }, + + "invite_modal": { + "title": "Invite member", + "footer": "Send invitation", + "email_label": "Email", + "input_placeholder": "Enter email address" + }, + + "toast": { + "update_role": { + "success": "User role was updated successfully.", + "error": "Couldn't update member role: {{error}}" + }, + + "remove_user": { + "success": "Member role updated successfully.", + "error": "Failed to remove member: {{error}}" + }, + + "leave_org": { + "success": "You have left the organization.", + "error": "Failed to leave organization: {{error}}" + }, + + "invite_member": { + "success": "User with {{email}} was added successfully", + "error": "Failed to send an invitation: {{error}}" + } + } + } +} diff --git a/apps/web/src/app/i18n/locales/pl/settings.json b/apps/web/src/app/i18n/locales/pl/settings.json new file mode 100644 index 00000000..55ab4371 --- /dev/null +++ b/apps/web/src/app/i18n/locales/pl/settings.json @@ -0,0 +1,9 @@ +{ + "general": { + "title": "Ogólne", + "description": "Ustawienia i opcje dla organizacji <1>{{name}}.", + "name_form": { + "label": "Nazwa organizacji" + } + } +} From a4d4d0cb67d9b58f61bb2b8c5da3983a73beb36c Mon Sep 17 00:00:00 2001 From: hubcio2115 Date: Thu, 26 Sep 2024 11:12:47 +0200 Subject: [PATCH 07/34] initial create-project translations --- apps/web/src/app/i18n/locales/en/create-project.json | 1 + apps/web/src/app/i18n/locales/pl/create-project.json | 1 + 2 files changed, 2 insertions(+) create mode 100644 apps/web/src/app/i18n/locales/en/create-project.json create mode 100644 apps/web/src/app/i18n/locales/pl/create-project.json diff --git a/apps/web/src/app/i18n/locales/en/create-project.json b/apps/web/src/app/i18n/locales/en/create-project.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/apps/web/src/app/i18n/locales/en/create-project.json @@ -0,0 +1 @@ +{} diff --git a/apps/web/src/app/i18n/locales/pl/create-project.json b/apps/web/src/app/i18n/locales/pl/create-project.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/apps/web/src/app/i18n/locales/pl/create-project.json @@ -0,0 +1 @@ +{} From ea0ec55e8d76da5a1834cce770dbdbc67c1ebde9 Mon Sep 17 00:00:00 2001 From: hubcio2115 Date: Thu, 26 Sep 2024 11:13:04 +0200 Subject: [PATCH 08/34] extended middleware with redirecting to locales --- apps/web/src/middleware.ts | 48 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index 286fada9..72043862 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -1,9 +1,53 @@ import { auth } from "./server/auth"; +import { NextResponse } from "next/server"; +import acceptLanguage from "accept-language"; +import { FALLBACK_LANG, LANGUAGES, COOKIE_NAME } from "./app/i18n/settings"; -export { auth as middleware }; +const authedPathsRegex = new RegExp(`^/(${LANGUAGES.join("|")})/dashboard.*`); + +export default auth((req) => { + let lang; + if (req.cookies.has(COOKIE_NAME)) + lang = acceptLanguage.get(req.cookies.get(COOKIE_NAME)?.value); + if (!lang) lang = acceptLanguage.get(req.headers.get("Accept-Language")); + if (!lang) lang = FALLBACK_LANG; + + // Redirect if lang in path is not supported + if ( + !LANGUAGES.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) && + !req.nextUrl.pathname.startsWith("/_next") + ) { + return NextResponse.redirect( + new URL(`/${lang}${req.nextUrl.pathname}`, req.url), + ); + } else if (req.headers.has("referer")) { + const refererUrl = new URL(req.headers.get("referer") ?? ""); + const langInReferer = LANGUAGES.find((l) => + refererUrl.pathname.startsWith(`/${l}`), + ); + const response = NextResponse.next(); + if (langInReferer) response.cookies.set(COOKIE_NAME, langInReferer); + return response; + } + + if (!req.auth && authedPathsRegex.test(req.nextUrl.pathname)) { + const callbackUrl = req.nextUrl.pathname; + + const encodedCallbackUrl = encodeURIComponent(callbackUrl); + + return Response.redirect( + new URL( + `api/auth/signin?callbackUrl=${encodedCallbackUrl}`, + req.nextUrl.origin, + ), + ); + } + + return NextResponse.next(); +}); export const config = { matcher: [ - "/dashboard/:path*", + "/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js|site.webmanifest).*)", ], }; From 4d2ec213ea2e4fe598bf7d693b068dc4f80c38e0 Mon Sep 17 00:00:00 2001 From: hubcio2115 Date: Thu, 26 Sep 2024 11:15:11 +0200 Subject: [PATCH 09/34] fixed some issues with searchParams on project grid page --- .../src/components/dashboard/project-grid.tsx | 21 +++---------------- .../dashboard/project-pagination.tsx | 11 ++-------- apps/web/src/lib/queries/useProjectsQuery.ts | 4 ++-- 3 files changed, 7 insertions(+), 29 deletions(-) diff --git a/apps/web/src/components/dashboard/project-grid.tsx b/apps/web/src/components/dashboard/project-grid.tsx index d80b0952..99a8caaa 100644 --- a/apps/web/src/components/dashboard/project-grid.tsx +++ b/apps/web/src/components/dashboard/project-grid.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import ProjectCard from "./project-card"; import type { Organization } from "~/lib/validators/organization"; import Image from "next/image"; -import { usePathname, useSearchParams, useRouter } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import { useProjectsPaginatedQuery } from "~/lib/queries/useProjectsQuery"; import ProjectsSkeleton from "./project-grid-skeleton"; @@ -37,25 +37,10 @@ interface ProjectGridProps { } export default function ProjectGrid({ organization }: ProjectGridProps) { - const router = useRouter(); - const pathname = usePathname(); const searchParams = useSearchParams(); - const page = searchParams.get("page"); - if (page === null || isNaN(parseInt(page))) { - const params = new URLSearchParams(searchParams); - params.set("page", "1"); - - router.push(pathname + "?" + params.toString()); - } - - const query = searchParams.get("q"); - if (query === null) { - const params = new URLSearchParams(searchParams); - params.set("q", ""); - - router.push(pathname + "?" + params.toString()); - } + const page = searchParams.get("page") ?? "1"; + const query = searchParams.get("q") ?? ""; const { data } = useProjectsPaginatedQuery( organization.name, diff --git a/apps/web/src/components/dashboard/project-pagination.tsx b/apps/web/src/components/dashboard/project-pagination.tsx index 0d47f7da..3cbf9a8f 100644 --- a/apps/web/src/components/dashboard/project-pagination.tsx +++ b/apps/web/src/components/dashboard/project-pagination.tsx @@ -24,15 +24,8 @@ export default function ProjectPagination({ const pathname = usePathname(); const searchParams = useSearchParams(); - const page = searchParams.get("page"); - if (page === null || isNaN(parseInt(page))) { - const params = new URLSearchParams(searchParams); - params.set("page", "1"); - - router.push(pathname + "?" + params.toString()); - } - - const query = searchParams.get("q"); + const page = searchParams.get("page") ?? "1"; + const query = searchParams.get("q") ?? ""; const { data } = useProjectsPaginatedQuery(organizationName, +page!, query!); diff --git a/apps/web/src/lib/queries/useProjectsQuery.ts b/apps/web/src/lib/queries/useProjectsQuery.ts index 07e14976..543e590e 100644 --- a/apps/web/src/lib/queries/useProjectsQuery.ts +++ b/apps/web/src/lib/queries/useProjectsQuery.ts @@ -23,8 +23,8 @@ type UseProjectsQueryOptions = Omit< export function useProjectsPaginatedQuery( organizationName: Organization["name"], - page: number, - query: Project["title"], + page: number = 1, + query: Project["title"] = "", options: UseProjectsQueryOptions = {}, ) { return useQuery({ From 729e86edebb79550c591edc586f6c7ab87d141c5 Mon Sep 17 00:00:00 2001 From: hubcio2115 Date: Thu, 26 Sep 2024 11:15:34 +0200 Subject: [PATCH 10/34] added translations to most of the pages --- .../dashboard/[name]/create-project/page.tsx | 0 .../{ => [lang]}/dashboard/[name]/layout.tsx | 6 +- .../dashboard/[name]/overview/page.tsx | 27 +++- .../dashboard/[name]/project/[id]/page.tsx | 0 .../dashboard/[name]/settings/layout.tsx | 15 +- .../[name]/settings/members/page.tsx | 118 ++++++++-------- .../dashboard/[name]/settings/page.tsx | 130 ++++++++++-------- .../src/app/{ => [lang]}/dashboard/error.tsx | 0 .../src/app/{ => [lang]}/dashboard/route.ts | 4 +- apps/web/src/app/{ => [lang]}/layout.tsx | 24 +++- apps/web/src/app/{ => [lang]}/not-found.tsx | 0 apps/web/src/app/{ => [lang]}/page.tsx | 77 ++++------- .../create-project/project-create-form.tsx | 10 +- apps/web/src/components/dashboard/dashnav.tsx | 14 +- .../dashboard/search-navigation.tsx | 13 +- apps/web/src/components/modals/alertModal.tsx | 28 ++-- apps/web/src/components/navbar.tsx | 20 ++- .../src/components/organization-select.tsx | 11 +- apps/web/src/components/pricing-plans.tsx | 98 +++++++------ 19 files changed, 345 insertions(+), 250 deletions(-) rename apps/web/src/app/{ => [lang]}/dashboard/[name]/create-project/page.tsx (100%) rename apps/web/src/app/{ => [lang]}/dashboard/[name]/layout.tsx (74%) rename apps/web/src/app/{ => [lang]}/dashboard/[name]/overview/page.tsx (60%) rename apps/web/src/app/{ => [lang]}/dashboard/[name]/project/[id]/page.tsx (100%) rename apps/web/src/app/{ => [lang]}/dashboard/[name]/settings/layout.tsx (66%) rename apps/web/src/app/{ => [lang]}/dashboard/[name]/settings/members/page.tsx (78%) rename apps/web/src/app/{ => [lang]}/dashboard/[name]/settings/page.tsx (64%) rename apps/web/src/app/{ => [lang]}/dashboard/error.tsx (100%) rename apps/web/src/app/{ => [lang]}/dashboard/route.ts (79%) rename apps/web/src/app/{ => [lang]}/layout.tsx (66%) rename apps/web/src/app/{ => [lang]}/not-found.tsx (100%) rename apps/web/src/app/{ => [lang]}/page.tsx (70%) diff --git a/apps/web/src/app/dashboard/[name]/create-project/page.tsx b/apps/web/src/app/[lang]/dashboard/[name]/create-project/page.tsx similarity index 100% rename from apps/web/src/app/dashboard/[name]/create-project/page.tsx rename to apps/web/src/app/[lang]/dashboard/[name]/create-project/page.tsx diff --git a/apps/web/src/app/dashboard/[name]/layout.tsx b/apps/web/src/app/[lang]/dashboard/[name]/layout.tsx similarity index 74% rename from apps/web/src/app/dashboard/[name]/layout.tsx rename to apps/web/src/app/[lang]/dashboard/[name]/layout.tsx index b7a76fd7..2fc107d3 100644 --- a/apps/web/src/app/dashboard/[name]/layout.tsx +++ b/apps/web/src/app/[lang]/dashboard/[name]/layout.tsx @@ -2,10 +2,12 @@ import { type PropsWithChildren } from "react"; import Dashnav from "~/components/dashboard/dashnav"; import Navbar from "~/components/navbar"; +import type { SupportedLanguages } from "~/app/i18n/settings"; type DashboardLayoutProps = PropsWithChildren<{ params: { name: string; + lang: SupportedLanguages; }; }>; @@ -16,9 +18,9 @@ export default async function DashboardLayout({ return (
- + - +
{children} diff --git a/apps/web/src/app/dashboard/[name]/overview/page.tsx b/apps/web/src/app/[lang]/dashboard/[name]/overview/page.tsx similarity index 60% rename from apps/web/src/app/dashboard/[name]/overview/page.tsx rename to apps/web/src/app/[lang]/dashboard/[name]/overview/page.tsx index f3fc6acf..0c648475 100644 --- a/apps/web/src/app/dashboard/[name]/overview/page.tsx +++ b/apps/web/src/app/[lang]/dashboard/[name]/overview/page.tsx @@ -1,5 +1,6 @@ import { redirect } from "next/navigation"; import { Suspense } from "react"; +import type { SupportedLanguages } from "~/app/i18n/settings"; import ProjectGrid from "~/components/dashboard/project-grid"; import ProjectsSkeleton from "~/components/dashboard/project-grid-skeleton"; import ProjectPagination from "~/components/dashboard/project-pagination"; @@ -10,11 +11,17 @@ import { getOwnOrganizations } from "~/server/actions/organization"; type DashboardOverviewProps = { params: { name: string; + lang: SupportedLanguages; + }; + searchParams?: { + page?: string; + q?: string; }; }; export default async function DashboardOverviewPage({ - params: { name }, + params, + searchParams, }: DashboardOverviewProps) { const [organizations, err] = await getOwnOrganizations(); @@ -22,13 +29,25 @@ export default async function DashboardOverviewPage({ throw err; } - const organization = organizations.find((org) => name === org.name); + const page = searchParams?.page; + const q = searchParams?.q; + console.log(page, q); + if ((page && isNaN(parseInt(page))) || q === undefined) { + const params = new URLSearchParams(searchParams); + + if (!page || isNaN(parseInt(page))) params.set("page", "1"); + if (q === undefined) params.set("q", ""); + + return redirect("?" + params.toString()); + } + + const organization = organizations.find((org) => params.name === org.name); return organization ? (
- + - +
}> diff --git a/apps/web/src/app/dashboard/[name]/project/[id]/page.tsx b/apps/web/src/app/[lang]/dashboard/[name]/project/[id]/page.tsx similarity index 100% rename from apps/web/src/app/dashboard/[name]/project/[id]/page.tsx rename to apps/web/src/app/[lang]/dashboard/[name]/project/[id]/page.tsx diff --git a/apps/web/src/app/dashboard/[name]/settings/layout.tsx b/apps/web/src/app/[lang]/dashboard/[name]/settings/layout.tsx similarity index 66% rename from apps/web/src/app/dashboard/[name]/settings/layout.tsx rename to apps/web/src/app/[lang]/dashboard/[name]/settings/layout.tsx index 6326876f..dbb72979 100644 --- a/apps/web/src/app/dashboard/[name]/settings/layout.tsx +++ b/apps/web/src/app/[lang]/dashboard/[name]/settings/layout.tsx @@ -24,10 +24,10 @@ export default function Settings({ params, children }: SettingsProps) {
general @@ -36,12 +36,12 @@ export default function Settings({ params, children }: SettingsProps) {
members @@ -49,7 +49,8 @@ export default function Settings({ params, children }: SettingsProps) {
-
{children}
+ + {children}
diff --git a/apps/web/src/app/dashboard/[name]/settings/members/page.tsx b/apps/web/src/app/[lang]/dashboard/[name]/settings/members/page.tsx similarity index 78% rename from apps/web/src/app/dashboard/[name]/settings/members/page.tsx rename to apps/web/src/app/[lang]/dashboard/[name]/settings/members/page.tsx index 578ba378..29d82253 100644 --- a/apps/web/src/app/dashboard/[name]/settings/members/page.tsx +++ b/apps/web/src/app/[lang]/dashboard/[name]/settings/members/page.tsx @@ -5,13 +5,14 @@ import { Select } from "@radix-ui/react-select"; import { useQuery } from "@tanstack/react-query"; import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; +import { useRef, useState } from "react"; import { - FormProvider, type SubmitErrorHandler, type SubmitHandler, useForm, } from "react-hook-form"; +import { useTranslation } from "~/app/i18n/client"; +import type { SupportedLanguages } from "~/app/i18n/settings"; import AlertModal from "~/components/modals/alertModal"; import DialogModal from "~/components/modals/dialogModal"; @@ -20,6 +21,7 @@ import { FormControl, FormField, FormItem, + Form, FormLabel, FormMessage, } from "~/components/ui/form"; @@ -46,6 +48,7 @@ import { type SettingsMembersViewProps = { params: { name: string; + lang: SupportedLanguages; }; }; @@ -55,7 +58,7 @@ type RoleInfo = { role: OrgMemberRole; }; -function SettingsMembersView({ params }: SettingsMembersViewProps) { +export default function SettingsMembersPage({ params }: SettingsMembersViewProps) { const session = useSession(); const formMethods = useForm(); const router = useRouter(); @@ -63,12 +66,12 @@ function SettingsMembersView({ params }: SettingsMembersViewProps) { const [isModalOpen, setIsModalOpen] = useState(false); const [isAlertOpen, setIsAlertOpen] = useState(false); + const { t, i18n } = useTranslation(params.lang, "settings", { + keyPrefix: "members", + }); + const newRole = useRef(null); - const { - data: organization, - refetch: refetchOrg, - isFetched, - } = useQuery({ + const { data: organization, isFetched } = useQuery({ queryKey: ["organization", params.name], queryFn: async () => { const [organizations, err] = await getOwnOrganizationByName(params.name); @@ -98,10 +101,6 @@ function SettingsMembersView({ params }: SettingsMembersViewProps) { ({ usersToOrganizations }) => usersToOrganizations.memberId, ); - useEffect(() => { - refetchOrg(); - }, [organization]); - async function handleRoleChange( newValue: RoleInfo["role"], userId: RoleInfo["memberId"], @@ -115,6 +114,7 @@ function SettingsMembersView({ params }: SettingsMembersViewProps) { }; return; } + try { await updateMemberRole({ memberId: userId, @@ -123,14 +123,14 @@ function SettingsMembersView({ params }: SettingsMembersViewProps) { }); refetchUsers(); toast({ - title: "Success", - description: "User role was updated successfully", + title: i18n.t("success"), + description: t("toast.update_role.success", { role: newValue }), }); } catch (error) { if (error instanceof Error) { toast({ - title: "Error", - description: `${error.message} !`, + title: i18n.t("error"), + description: t("toast.update_role.error", { error: error.message }), }); } } @@ -152,14 +152,14 @@ function SettingsMembersView({ params }: SettingsMembersViewProps) { organizationId: organization!.id, }); toast({ - title: "Success", - description: "User was removed successfully", + title: i18n.t("success"), + description: t("toast.remove_user.success"), }); } catch (error) { if (error instanceof Error) { toast({ - title: "Error", - description: `${error.message} !`, + title: i18n.t("error"), + description: t("toast.remove_user.error", { error: error.message }), }); } } @@ -172,15 +172,15 @@ function SettingsMembersView({ params }: SettingsMembersViewProps) { organizationId: organization!.id, }); toast({ - title: "Success", - description: "You have left the organization", + title: i18n.t("success"), + description: t("toast.leave_org.success"), }); router.push("/dashboard"); } catch (error) { if (error instanceof Error) { toast({ - title: "Error", - description: `${error.message} !`, + title: i18n.t("error"), + description: t("toast.leave_org.error", { error: error.message }), }); } } @@ -202,15 +202,16 @@ function SettingsMembersView({ params }: SettingsMembersViewProps) { }); refetchUsers(); + toast({ - title: "Success", - description: `User ${data.email} was added successfully`, + title: i18n.t("success"), + description: t("toast.invite_member.success", { email: data.email }), }); } catch (error) { if (error instanceof Error) { toast({ - title: "Error", - description: `${error.message} !`, + title: i18n.t("error"), + description: t("toast.invite_member.error", { email: error.message }), }); } } @@ -220,10 +221,6 @@ function SettingsMembersView({ params }: SettingsMembersViewProps) { const onError: SubmitErrorHandler = (error) => { console.log(error); - toast({ - title: "Error", - description: `Failed to send and invitation: ${error.email!.message} !`, - }); }; return ( @@ -231,12 +228,11 @@ function SettingsMembersView({ params }: SettingsMembersViewProps) {
-

Members

-

- All members and administrators with access to the{" "} - {params.name} organization. -

+

{t("title")}

+ +

{t("description")}

+
@@ -252,9 +248,12 @@ function SettingsMembersView({ params }: SettingsMembersViewProps) {
- Member + {t("member_table.columns.member")} +
+ +
+ {t("member_table.columns.role")}
-
Role
{userData?.map(({ user, usersToOrganizations }) => ( @@ -265,6 +264,7 @@ function SettingsMembersView({ params }: SettingsMembersViewProps) {
{user?.name}
+
+
{session.data?.user.id === user?.id ? ( ) : currentUserRole === "owner" || (currentUserRole === "admin" && @@ -324,7 +325,7 @@ function SettingsMembersView({ params }: SettingsMembersViewProps) { variant="ghost" onClick={() => handleRemoveMember(user!.id)} > - Remove + {t('leave_section.remove_button')} ) : null}
@@ -332,25 +333,27 @@ function SettingsMembersView({ params }: SettingsMembersViewProps) { ))}
) : ( - + )}
- + +
setIsModalOpen(open)} - title="Invite member" + onOpenChange={setIsModalOpen} + title={t("invite_modal.title")} onSubmit={form.handleSubmit(onSubmit, onError)} - footerText="Send invitation" + footerText={t("invite_modal.footer")} > ( - Email + {t("invite_modal.email_label")} + - + @@ -358,18 +361,17 @@ function SettingsMembersView({ params }: SettingsMembersViewProps) { )} /> - +
setIsAlertOpen(open)} - title="Are you absolutely sure?" - description="You will transter the ownership of the organization to the selected user. Your role will be changed to admin." + onOpenChange={setIsAlertOpen} + title={t("leave_section.alert.title")} + description={t("leave_section.alert.description")} onCancel={() => setIsAlertOpen(false)} onConfirm={handleAlertAccepted} /> ); } - -export default SettingsMembersView; diff --git a/apps/web/src/app/dashboard/[name]/settings/page.tsx b/apps/web/src/app/[lang]/dashboard/[name]/settings/page.tsx similarity index 64% rename from apps/web/src/app/dashboard/[name]/settings/page.tsx rename to apps/web/src/app/[lang]/dashboard/[name]/settings/page.tsx index 2c1fc8c8..a67786e2 100644 --- a/apps/web/src/app/dashboard/[name]/settings/page.tsx +++ b/apps/web/src/app/[lang]/dashboard/[name]/settings/page.tsx @@ -6,11 +6,10 @@ import { Trash2 } from "lucide-react"; import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { useState } from "react"; -import { - type SubmitErrorHandler, - type SubmitHandler, - useForm, -} from "react-hook-form"; +import { type FieldErrors, useForm } from "react-hook-form"; +import { Trans } from "react-i18next"; +import { useTranslation } from "~/app/i18n/client"; +import type { SupportedLanguages } from "~/app/i18n/settings"; import AlertModal from "~/components/modals/alertModal"; import { Button } from "~/components/ui/button"; @@ -39,10 +38,17 @@ import { type SettingsMembersViewProps = { params: { name: string; + lang: SupportedLanguages; }; }; -function SettingsGeneral({ params }: SettingsMembersViewProps) { +export default function SettingsGeneralPage({ + params, +}: SettingsMembersViewProps) { + const { t, i18n } = useTranslation(params.lang, "settings", { + keyPrefix: "general", + }); + const { toast } = useToast(); const router = useRouter(); const session = useSession(); @@ -52,18 +58,14 @@ function SettingsGeneral({ params }: SettingsMembersViewProps) { data: organization, isLoading, isFetched, + refetch, } = useQuery({ queryKey: ["organization", params.name], queryFn: async () => { const [organization, err] = await getOwnOrganizationByName(params.name); if (err !== null) { - toast({ - title: "Error", - description: `Failed to fetch organization: ${err}`, - }); - router.push("/dashboard"); - return null; + router.push("/404"); } return organization; @@ -84,16 +86,16 @@ function SettingsGeneral({ params }: SettingsMembersViewProps) { mutationFn: updateOrganizationName, onError: (error) => { toast({ - title: "Error", - description: `Failed to update organization name: ${error.message}`, + title: i18n.t("error"), + description: t("toast.update.error", { error: error.message }), }); }, onSuccess: (_, { name }) => { toast({ - title: "Success", - description: `Organization name updated to ${name}`, + title: i18n.t("success"), + description: t("toast.update.success", { name }), }); - router.push(`/dashboard/${name}/settings`); + refetch(); }, }); @@ -101,14 +103,14 @@ function SettingsGeneral({ params }: SettingsMembersViewProps) { mutationFn: deleteOrganization, onError: (error) => { toast({ - title: "Error", - description: `${error.message}`, + title: i18n.t("error"), + description: t("toast.delete.error", { error: error.message }), }); }, onSuccess: () => { toast({ - title: "Success", - description: `Organization deleted successfully`, + title: i18n.t("success"), + description: t("toast.delete.success"), }); router.push("/dashboard"); }, @@ -122,24 +124,31 @@ function SettingsGeneral({ params }: SettingsMembersViewProps) { }, }); - const onSubmit: SubmitHandler = (data) => { + function onSubmit(data: UpdateOrganizationName) { updateOrganizationNameMutation(data); - }; + } - const onError: SubmitErrorHandler = (error) => { + function onError(error: FieldErrors) { console.error(error); - }; + } return organization && !isLoading ? (
-

General

-

- Settings and options for the{" "} - {organization?.name}{" "} +

{t("title")}

+ + + Settings and options for the + organization. -

+
+
- Organization name + {t("name_form.label")} + + )} @@ -173,24 +183,31 @@ function SettingsGeneral({ params }: SettingsMembersViewProps) { type="submit" className="w-fit" > - Save changes + {t("name_form.submit_button")} {!organization?.defaultOrg ? (
-

Delete organization

+

+ {t("delete_section.title")} +

+

- Please note that deleting the{" "} - - {organization?.name}{" "} - - organization is{" "} - permanent and{" "} - cannot be undone - . + + Please note that deleting the + + organization is + and + . +

+
) : (
-

Default organization

+

+ {t("delete_section.default_org.title")} +

+

- The default{" "} - - {organization?.name}{" "} - - organization{" "} - cannot be - deleted. + + The default organization + cannot be + deleted. +

)} + setIsAlertOpen(open)} - title="Are you absolutely sure?" - description={`This will permanently delete the organization and all its data. \n Enter the name of the organization "${organization.name}" to confirm.`} + onOpenChange={setIsAlertOpen} + title={t("delete_section.delete_modal.title")} + description={t("delete_section.delete_modal.description", { + name: organization.name, + })} unlockString={organization.name} onCancel={() => setIsAlertOpen(false)} onConfirm={() => { @@ -234,5 +256,3 @@ function SettingsGeneral({ params }: SettingsMembersViewProps) {
); } - -export default SettingsGeneral; diff --git a/apps/web/src/app/dashboard/error.tsx b/apps/web/src/app/[lang]/dashboard/error.tsx similarity index 100% rename from apps/web/src/app/dashboard/error.tsx rename to apps/web/src/app/[lang]/dashboard/error.tsx diff --git a/apps/web/src/app/dashboard/route.ts b/apps/web/src/app/[lang]/dashboard/route.ts similarity index 79% rename from apps/web/src/app/dashboard/route.ts rename to apps/web/src/app/[lang]/dashboard/route.ts index 76e566d7..ee5c3698 100644 --- a/apps/web/src/app/dashboard/route.ts +++ b/apps/web/src/app/[lang]/dashboard/route.ts @@ -9,7 +9,9 @@ export async function GET(req: NextRequest) { const url = req.nextUrl.clone(); - url.pathname = org ? `/dashboard/${org.organizations?.name}/overview` : "/404"; + url.pathname = org + ? `${req.nextUrl.pathname}/${org.organizations?.name}/overview` + : "/404"; return NextResponse.redirect(url); } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/[lang]/layout.tsx similarity index 66% rename from apps/web/src/app/layout.tsx rename to apps/web/src/app/[lang]/layout.tsx index 7d039a07..a269bccc 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/[lang]/layout.tsx @@ -1,5 +1,6 @@ import { GeistSans } from "geist/font/sans"; import type { PropsWithChildren } from "react"; +import { dir } from "i18next"; import { Toaster } from "~/components/ui/toaster"; import { ReactQueryProvider } from "~/providers/react-query-provider"; @@ -7,6 +8,11 @@ import { SessionProvider } from "~/providers/session-provider"; import { ThemeProvider } from "~/providers/theme-provider"; import { auth } from "~/server/auth"; import "~/styles/globals.css"; +import { LANGUAGES, type SupportedLanguages } from "~/app/i18n/settings"; + +export async function generateStaticParams() { + return LANGUAGES.map((lng) => ({ lng })); +} export const metadata = { title: "Editthing", @@ -14,11 +20,25 @@ export const metadata = { "Welcome to Editthing - your go-to platform for enhanced collaboration between editors and YouTube creators.", }; -export default async function RootLayout({ children }: PropsWithChildren) { +interface RootLayoutProps extends PropsWithChildren { + params: { + lang: SupportedLanguages; + }; +} + +export default async function RootLayout({ + children, + params, +}: RootLayoutProps) { const session = await auth(); return ( - + diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/[lang]/not-found.tsx similarity index 100% rename from apps/web/src/app/not-found.tsx rename to apps/web/src/app/[lang]/not-found.tsx diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/[lang]/page.tsx similarity index 70% rename from apps/web/src/app/page.tsx rename to apps/web/src/app/[lang]/page.tsx index e5118b59..9bec3be6 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/[lang]/page.tsx @@ -8,39 +8,24 @@ import { import Navbar from "~/components/navbar"; import PricingPlans from "~/components/pricing-plans"; +import { translation } from "~/app/i18n"; +import type { SupportedLanguages } from "../i18n/settings"; + +const featureIcons = [GitPullRequestArrow, Lock, RefreshCw, Fingerprint]; + +interface HomePageProps { + params: { + lang: SupportedLanguages; + }; +} + +export default async function HomePage({ params }: HomePageProps) { + const { t } = await translation(params.lang, "home-page"); -const features = [ - { - name: "Merge to upload", - description: - "If you know what git flow is you exactly know what this tool's about. You'll get comfortable real quick.", - icon: GitPullRequestArrow, - }, - { - name: "Secure", - description: - "You finally have a tool to manage access you your channel in a way that gives you security and doesn't restrain your flow.", - icon: Lock, - }, - { - name: "Simple flow", - description: - "We didn't invent anything new, we took something that works and translated it to a new area.", - icon: RefreshCw, - }, - { - name: "We don't track you", - description: - "We store as little about you as necessary, your access keys are encrypted.", - icon: Fingerprint, - }, -] as const; - -export default function HomePage() { return (
- +
@@ -62,13 +47,11 @@ export default function HomePage() {

- Tool to streamline your video production pipeline + {t("hero.title")}

- Giving feedback, suggestions and managing your channel's - access don't get easier than this. Inspired by Git a tool for - iterating on software that actually works. + {t("hero.description")}

@@ -77,7 +60,7 @@ export default function HomePage() { href="#deploy-faster" className="flex items-center gap-2 text-sm font-semibold leading-6 text-gray-900" > - Learn more + {t("hero.learn_more")} @@ -108,35 +91,33 @@ export default function HomePage() { id="deploy-faster" className="text-base font-semibold leading-7 text-fuchsia-900" > - Upload faster + {t("feature_section.subtitle")}

- Everything you need to upload your new video + {t("feature_section.title")}

- Quis tellus eget adipiscing convallis sit sit eget aliquet quis. - Suspendisse eget egestas a elementum pulvinar et feugiat blandit - at. In mi viverra elit nunc. + {t("feature_section.description")}

- {features.map((feature) => ( -
+ {featureIcons.map((Icon, i) => ( +
-
- {feature.name} + {t(`feature_section.features.${i}.name`)}
- {feature.description} + {t(`feature_section.features.${i}.description`)}
))} @@ -145,7 +126,7 @@ export default function HomePage() {
- +

diff --git a/apps/web/src/components/create-project/project-create-form.tsx b/apps/web/src/components/create-project/project-create-form.tsx index 4e96a18d..e39846fd 100644 --- a/apps/web/src/components/create-project/project-create-form.tsx +++ b/apps/web/src/components/create-project/project-create-form.tsx @@ -1,7 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; +import { useForm, type FieldErrors } from "react-hook-form"; import { FILE_INPUT_ACCEPTED_FORMATS, projectFormSchema, @@ -97,12 +97,14 @@ export default function ProjectCreateForm({ mutate(formData); } + function onError(errors: FieldErrors) { + console.error(errors); + } + return (

{ - console.error(err); - })} + onSubmit={form.handleSubmit(onSuccess, onError)} className={cn("flex flex-col gap-8 w-full", { "justify-center items-center flex-1": !showWholeForm, })} diff --git a/apps/web/src/components/dashboard/dashnav.tsx b/apps/web/src/components/dashboard/dashnav.tsx index 9f98fa2e..e5388cad 100644 --- a/apps/web/src/components/dashboard/dashnav.tsx +++ b/apps/web/src/components/dashboard/dashnav.tsx @@ -2,24 +2,28 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useTranslation } from "~/app/i18n/client"; +import type { SupportedLanguages } from "~/app/i18n/settings"; import { cn } from "~/lib/utils"; interface DashnavProps { orgName: string; + lang: SupportedLanguages; } -export default function Dashnav({ orgName }: DashnavProps) { +export default function Dashnav({ orgName, lang }: DashnavProps) { const pathname = usePathname(); + const { t } = useTranslation(lang, "translation", { keyPrefix: "dashnav" }); const links = [ { - path: `/dashboard/${orgName}/overview`, - label: "overview", + path: `/${lang}/dashboard/${orgName}/overview`, + label: t("overview_button"), }, { - path: `/dashboard/${orgName}/settings`, - label: "settings", + path: `/${lang}/dashboard/${orgName}/settings`, + label: t("settings_button"), }, ]; diff --git a/apps/web/src/components/dashboard/search-navigation.tsx b/apps/web/src/components/dashboard/search-navigation.tsx index 9f9bfcac..560cc9ac 100644 --- a/apps/web/src/components/dashboard/search-navigation.tsx +++ b/apps/web/src/components/dashboard/search-navigation.tsx @@ -7,16 +7,21 @@ import { Button } from "~/components/ui/button"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState, type ChangeEvent } from "react"; import { useDebounce } from "@uidotdev/usehooks"; +import type { SupportedLanguages } from "~/app/i18n/settings"; +import { useTranslation } from "~/app/i18n/client"; interface ProjectGridProps { orgName: string; + lang: SupportedLanguages; } -export default function SearchNavigation({ orgName }: ProjectGridProps) { +export default function SearchNavigation({ orgName, lang }: ProjectGridProps) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); + const { t } = useTranslation(lang, "overview"); + const [query, setQuery] = useState(searchParams.get("q") ?? ""); const debouncedQuery = useDebounce(query, 300); @@ -24,7 +29,7 @@ export default function SearchNavigation({ orgName }: ProjectGridProps) { const params = new URLSearchParams(searchParams); params.set("q", debouncedQuery); - router.push(pathname + "?" + params.toString()); + router.push(`${pathname}?${params.toString()}`); }, [debouncedQuery]); function onChange(event: ChangeEvent) { @@ -41,12 +46,12 @@ export default function SearchNavigation({ orgName }: ProjectGridProps) {
- +
); diff --git a/apps/web/src/components/modals/alertModal.tsx b/apps/web/src/components/modals/alertModal.tsx index fe74a356..5589e3f2 100644 --- a/apps/web/src/components/modals/alertModal.tsx +++ b/apps/web/src/components/modals/alertModal.tsx @@ -12,6 +12,8 @@ import { } from "~/components/ui/alert-dialog"; import { Input } from "../ui/input"; +import { useTranslation } from "~/app/i18n/client"; +import type { SupportedLanguages } from "~/app/i18n/settings"; type AlertModalProps = { isOpen: boolean; @@ -21,6 +23,7 @@ type AlertModalProps = { unlockString?: string; onCancel: () => void; onConfirm: () => void; + lang: SupportedLanguages; }; export default function AlertModal({ @@ -31,8 +34,12 @@ export default function AlertModal({ unlockString, onCancel, onConfirm, + lang, }: AlertModalProps) { const [inputValue, setInputValue] = useState(""); + const { t } = useTranslation(lang, "translation", { + keyPrefix: "alert_modal", + }); const isActionDisabled = unlockString !== undefined && unlockString !== inputValue; @@ -46,28 +53,31 @@ export default function AlertModal({ {description} + + {unlockString !== undefined && ( + setInputValue(e.target.value)} + /> + )} + - {unlockString !== undefined && ( - setInputValue(e.target.value)} - /> - )} { onCancel(), setInputValue(""); }} > - Cancel + {t("cancel_button")} + { onConfirm(), setInputValue(""); }} > - Continue + {t("confirm_button")} diff --git a/apps/web/src/components/navbar.tsx b/apps/web/src/components/navbar.tsx index 74fb669a..974fd755 100644 --- a/apps/web/src/components/navbar.tsx +++ b/apps/web/src/components/navbar.tsx @@ -14,13 +14,21 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "./ui/dropdown-menu"; +import type { SupportedLanguages } from "~/app/i18n/settings"; +import { useTranslation } from "~/app/i18n/client"; -export default function Navbar() { +interface NavbarProps { + lang: SupportedLanguages; +} + +export default function Navbar({ lang }: NavbarProps) { const pathname = usePathname(); const isOnDashboard = pathname.includes("/dashboard"); const session = useSession(); + const { t } = useTranslation(lang, "translation", { keyPrefix: "navbar" }); + const shouldShowOrganizationsSelect = session.status === "authenticated" && isOnDashboard; @@ -32,7 +40,7 @@ export default function Navbar() { thing - {shouldShowOrganizationsSelect ? : null} + {shouldShowOrganizationsSelect && }
@@ -51,15 +59,15 @@ export default function Navbar() { className="mr-2 h-4 w-4 hover:cursor-pointer" color="red" /> - Log out + {t("logout_button")} {!isOnDashboard && ( - + )} @@ -71,7 +79,7 @@ export default function Navbar() { signIn(); }} > - Sign in + {t("signin_button")} )}
diff --git a/apps/web/src/components/organization-select.tsx b/apps/web/src/components/organization-select.tsx index 0f6b7e81..6f8773f3 100644 --- a/apps/web/src/components/organization-select.tsx +++ b/apps/web/src/components/organization-select.tsx @@ -45,16 +45,23 @@ import { import { Input } from "./ui/input"; import { Skeleton } from "./ui/skeleton"; import { useToast } from "./ui/toaster/use-toast"; +import { useTranslation } from "~/app/i18n/client"; +import type { SupportedLanguages } from "~/app/i18n/settings"; +interface OrganizationSelectProps { + lang: SupportedLanguages; +} -export default function OrganizationSelect() { +export default function OrganizationSelect({ lang }: OrganizationSelectProps) { const pathname = usePathname(); const organizationFromPathname = decodeURIComponent( // @ts-expect-error Since we are taking something from a pathname there has to be something - pathname.split("/").at(2), + pathname.split("/").at(3), ); const router = useRouter(); const { toast } = useToast(); + const { t } = useTranslation(lang, "translation", { keyPrefix: "navbar" }); + const [isModalOpen, setIsModalOpen] = useState(false); useEffect(() => { diff --git a/apps/web/src/components/pricing-plans.tsx b/apps/web/src/components/pricing-plans.tsx index 56ff6cc2..9259e831 100644 --- a/apps/web/src/components/pricing-plans.tsx +++ b/apps/web/src/components/pricing-plans.tsx @@ -1,7 +1,7 @@ "use client"; import { Check } from "lucide-react"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { cn } from "~/lib/utils"; @@ -13,50 +13,62 @@ import { CardHeader, CardTitle, } from "./ui/card"; +import { useTranslation } from "~/app/i18n/client"; +import type { SupportedLanguages } from "~/app/i18n/settings"; + +interface PricingPlansProps { + lang: SupportedLanguages; +} + +export default function PrincingPlans({ lang }: PricingPlansProps) { + const { t } = useTranslation(lang, "home-page", { + keyPrefix: "pricing_section", + }); -const plans = [ - { - title: "Small channel", - description: - "The essentials for running a small or an up and comming channel.", - price: 50, - buttonText: "Buy plan", - features: ["Full access to our tools", "You + 2 seats", "Up to 1 channel"], - }, - - { - title: "Small network of channels", - description: "Everything you need to manage small organization.", - price: 250, - buttonText: "Buy plan", - features: [ - "Everything in Small channel plan", - "You + 10 seats", - "Up to 3 channels", - ], - }, -]; - -const enterprisePlan = { - title: "Enterprise", - description: "A plan tailored to the exact needs of your company.", - price: "Custom", - buttonText: "Contact sales", - features: [ - "Everything in Small network of channels", - "You + any number of seats you'd like", - "You specify the number of channels", - ], -}; - -export default function PrincingPlans() { const [billingPeriod, setBillingPeriod] = useState<"monthly" | "yearly">( "monthly", ); + const plans = useMemo(() => { + const plans = []; + + for (let i = 0; i < 2; i++) { + const plan = { + title: t(`pricing_plans.${i}.title`), + description: t(`pricing_plans.${i}.description`), + price: t(`pricing_plans.${i}.price`), + buttonText: t(`pricing_plans.${i}.buttonText`), + features: [ + t(`pricing_plans.${i}.features.0`), + t(`pricing_plans.${i}.features.1`), + t(`pricing_plans.${i}.features.2`), + ], + }; + + plans.push(plan); + } + + return plans; + }, [t]); + + const enterprisePlan = useMemo( + () => ({ + title: t("pricing_plans.2.title"), + description: t("pricing_plans.2.description"), + price: t("pricing_plans.2.price"), + buttonText: t("pricing_plans.2.buttonText"), + features: [ + t("pricing_plans.2.features.0"), + t("pricing_plans.2.features.1"), + t("pricing_plans.2.features.2"), + ], + }), + [t], + ); + return (
-
+

setBillingPeriod("monthly")} > - Monthly + {t("monthly")}

setBillingPeriod("yearly")} > - Yearly + {t("yearly")}

@@ -94,7 +106,7 @@ export default function PrincingPlans() {

- ${billingPeriod === "monthly" ? price : price * 10} + ${billingPeriod === "monthly" ? price : +price * 10} /{billingPeriod === "monthly" ? "month" : "year"} @@ -102,7 +114,7 @@ export default function PrincingPlans() {

    @@ -134,7 +146,7 @@ export default function PrincingPlans() {

      From d5b7c1e59ea561db77a987b4119d12c9d66b82ee Mon Sep 17 00:00:00 2001 From: hubcio2115 Date: Thu, 26 Sep 2024 12:31:04 +0200 Subject: [PATCH 11/34] moved i18n folder to src --- .../src/app/[lang]/dashboard/[name]/create-project/page.tsx | 4 +++- apps/web/src/app/[lang]/dashboard/[name]/layout.tsx | 2 +- apps/web/src/app/[lang]/dashboard/[name]/overview/page.tsx | 2 +- .../app/[lang]/dashboard/[name]/settings/members/page.tsx | 4 ++-- apps/web/src/app/[lang]/dashboard/[name]/settings/page.tsx | 4 ++-- apps/web/src/app/[lang]/layout.tsx | 2 +- apps/web/src/app/[lang]/page.tsx | 4 ++-- .../src/components/create-project/project-create-form.tsx | 6 ++++++ apps/web/src/components/dashboard/dashnav.tsx | 4 ++-- apps/web/src/components/dashboard/search-navigation.tsx | 4 ++-- apps/web/src/components/modals/alertModal.tsx | 4 ++-- apps/web/src/components/navbar.tsx | 4 ++-- apps/web/src/components/organization-select.tsx | 4 ++-- apps/web/src/components/pricing-plans.tsx | 4 ++-- apps/web/src/{app => }/i18n/client.ts | 0 apps/web/src/{app => }/i18n/index.ts | 0 apps/web/src/{app => }/i18n/locales/en/create-project.json | 0 apps/web/src/{app => }/i18n/locales/en/home-page.json | 0 apps/web/src/{app => }/i18n/locales/en/overview.json | 0 apps/web/src/{app => }/i18n/locales/en/settings.json | 0 apps/web/src/{app => }/i18n/locales/en/translation.json | 0 apps/web/src/{app => }/i18n/locales/pl/create-project.json | 0 apps/web/src/{app => }/i18n/locales/pl/home-page.json | 0 apps/web/src/{app => }/i18n/locales/pl/overview.json | 0 apps/web/src/{app => }/i18n/locales/pl/settings.json | 0 apps/web/src/{app => }/i18n/locales/pl/translation.json | 0 apps/web/src/{app => }/i18n/settings.ts | 0 apps/web/src/middleware.ts | 2 +- 28 files changed, 31 insertions(+), 23 deletions(-) rename apps/web/src/{app => }/i18n/client.ts (100%) rename apps/web/src/{app => }/i18n/index.ts (100%) rename apps/web/src/{app => }/i18n/locales/en/create-project.json (100%) rename apps/web/src/{app => }/i18n/locales/en/home-page.json (100%) rename apps/web/src/{app => }/i18n/locales/en/overview.json (100%) rename apps/web/src/{app => }/i18n/locales/en/settings.json (100%) rename apps/web/src/{app => }/i18n/locales/en/translation.json (100%) rename apps/web/src/{app => }/i18n/locales/pl/create-project.json (100%) rename apps/web/src/{app => }/i18n/locales/pl/home-page.json (100%) rename apps/web/src/{app => }/i18n/locales/pl/overview.json (100%) rename apps/web/src/{app => }/i18n/locales/pl/settings.json (100%) rename apps/web/src/{app => }/i18n/locales/pl/translation.json (100%) rename apps/web/src/{app => }/i18n/settings.ts (100%) diff --git a/apps/web/src/app/[lang]/dashboard/[name]/create-project/page.tsx b/apps/web/src/app/[lang]/dashboard/[name]/create-project/page.tsx index 39dd204a..0474ab91 100644 --- a/apps/web/src/app/[lang]/dashboard/[name]/create-project/page.tsx +++ b/apps/web/src/app/[lang]/dashboard/[name]/create-project/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useRouter } from "next/navigation"; +import type { SupportedLanguages } from "~/i18n/settings"; import ProjectCreateForm from "~/components/create-project/project-create-form"; import { useCreateProjectMutation } from "~/lib/mutations/useCreateProjectMutation"; import type { InsertProject } from "~/lib/validators/project"; @@ -21,7 +22,7 @@ const defaultValues: InsertProject = { }; interface CreateProjectPageProps { - params: { name: string }; + params: { name: string; lang: SupportedLanguages }; } export default function CreateProjectPage({ params }: CreateProjectPageProps) { @@ -42,6 +43,7 @@ export default function CreateProjectPage({ params }: CreateProjectPageProps) { mutate={mutate} isPending={isPending} defaultValues={defaultValues} + lang={params.lang} />
); diff --git a/apps/web/src/app/[lang]/dashboard/[name]/layout.tsx b/apps/web/src/app/[lang]/dashboard/[name]/layout.tsx index 2fc107d3..1a463335 100644 --- a/apps/web/src/app/[lang]/dashboard/[name]/layout.tsx +++ b/apps/web/src/app/[lang]/dashboard/[name]/layout.tsx @@ -2,7 +2,7 @@ import { type PropsWithChildren } from "react"; import Dashnav from "~/components/dashboard/dashnav"; import Navbar from "~/components/navbar"; -import type { SupportedLanguages } from "~/app/i18n/settings"; +import type { SupportedLanguages } from "~/i18n/settings"; type DashboardLayoutProps = PropsWithChildren<{ params: { diff --git a/apps/web/src/app/[lang]/dashboard/[name]/overview/page.tsx b/apps/web/src/app/[lang]/dashboard/[name]/overview/page.tsx index 0c648475..e0fa08e3 100644 --- a/apps/web/src/app/[lang]/dashboard/[name]/overview/page.tsx +++ b/apps/web/src/app/[lang]/dashboard/[name]/overview/page.tsx @@ -1,6 +1,6 @@ import { redirect } from "next/navigation"; import { Suspense } from "react"; -import type { SupportedLanguages } from "~/app/i18n/settings"; +import type { SupportedLanguages } from "~/i18n/settings"; import ProjectGrid from "~/components/dashboard/project-grid"; import ProjectsSkeleton from "~/components/dashboard/project-grid-skeleton"; import ProjectPagination from "~/components/dashboard/project-pagination"; diff --git a/apps/web/src/app/[lang]/dashboard/[name]/settings/members/page.tsx b/apps/web/src/app/[lang]/dashboard/[name]/settings/members/page.tsx index 29d82253..40af0340 100644 --- a/apps/web/src/app/[lang]/dashboard/[name]/settings/members/page.tsx +++ b/apps/web/src/app/[lang]/dashboard/[name]/settings/members/page.tsx @@ -11,8 +11,8 @@ import { type SubmitHandler, useForm, } from "react-hook-form"; -import { useTranslation } from "~/app/i18n/client"; -import type { SupportedLanguages } from "~/app/i18n/settings"; +import { useTranslation } from "~/i18n/client"; +import type { SupportedLanguages } from "~/i18n/settings"; import AlertModal from "~/components/modals/alertModal"; import DialogModal from "~/components/modals/dialogModal"; diff --git a/apps/web/src/app/[lang]/dashboard/[name]/settings/page.tsx b/apps/web/src/app/[lang]/dashboard/[name]/settings/page.tsx index a67786e2..a2291a3c 100644 --- a/apps/web/src/app/[lang]/dashboard/[name]/settings/page.tsx +++ b/apps/web/src/app/[lang]/dashboard/[name]/settings/page.tsx @@ -8,8 +8,8 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; import { type FieldErrors, useForm } from "react-hook-form"; import { Trans } from "react-i18next"; -import { useTranslation } from "~/app/i18n/client"; -import type { SupportedLanguages } from "~/app/i18n/settings"; +import { useTranslation } from "~/i18n/client"; +import type { SupportedLanguages } from "~/i18n/settings"; import AlertModal from "~/components/modals/alertModal"; import { Button } from "~/components/ui/button"; diff --git a/apps/web/src/app/[lang]/layout.tsx b/apps/web/src/app/[lang]/layout.tsx index a269bccc..322c49ad 100644 --- a/apps/web/src/app/[lang]/layout.tsx +++ b/apps/web/src/app/[lang]/layout.tsx @@ -8,7 +8,7 @@ import { SessionProvider } from "~/providers/session-provider"; import { ThemeProvider } from "~/providers/theme-provider"; import { auth } from "~/server/auth"; import "~/styles/globals.css"; -import { LANGUAGES, type SupportedLanguages } from "~/app/i18n/settings"; +import { LANGUAGES, type SupportedLanguages } from "~/i18n/settings"; export async function generateStaticParams() { return LANGUAGES.map((lng) => ({ lng })); diff --git a/apps/web/src/app/[lang]/page.tsx b/apps/web/src/app/[lang]/page.tsx index 9bec3be6..c279276e 100644 --- a/apps/web/src/app/[lang]/page.tsx +++ b/apps/web/src/app/[lang]/page.tsx @@ -8,8 +8,8 @@ import { import Navbar from "~/components/navbar"; import PricingPlans from "~/components/pricing-plans"; -import { translation } from "~/app/i18n"; -import type { SupportedLanguages } from "../i18n/settings"; +import { translation } from "~/i18n"; +import type { SupportedLanguages } from "../../i18n/settings"; const featureIcons = [GitPullRequestArrow, Lock, RefreshCw, Fingerprint]; diff --git a/apps/web/src/components/create-project/project-create-form.tsx b/apps/web/src/components/create-project/project-create-form.tsx index e39846fd..629fdcbb 100644 --- a/apps/web/src/components/create-project/project-create-form.tsx +++ b/apps/web/src/components/create-project/project-create-form.tsx @@ -44,23 +44,29 @@ import { Checkbox } from "../ui/checkbox"; import { cn } from "~/lib/utils"; import ChannelsSelect from "./channel-select"; import type { UseCreateProjectMutationResult } from "~/lib/mutations/useCreateProjectMutation"; +import { useTranslation } from "~/i18n/client"; +import type { SupportedLanguages } from "~/i18n/settings"; interface ProjectFormProps { defaultValues: InsertProject; mutate: UseCreateProjectMutationResult["mutate"]; isPending?: UseCreateProjectMutationResult["isPending"]; + lang: SupportedLanguages; } export default function ProjectCreateForm({ mutate, isPending = false, defaultValues, + lang, }: ProjectFormProps) { const form = useForm({ resolver: zodResolver(projectFormSchema), defaultValues, }); + const { t } = useTranslation(lang, "create-project", {}); + const [showMore, setShowMore] = useState(false); const video = form.watch("video"); diff --git a/apps/web/src/components/dashboard/dashnav.tsx b/apps/web/src/components/dashboard/dashnav.tsx index e5388cad..bde3dcc9 100644 --- a/apps/web/src/components/dashboard/dashnav.tsx +++ b/apps/web/src/components/dashboard/dashnav.tsx @@ -2,8 +2,8 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; -import { useTranslation } from "~/app/i18n/client"; -import type { SupportedLanguages } from "~/app/i18n/settings"; +import { useTranslation } from "~/i18n/client"; +import type { SupportedLanguages } from "~/i18n/settings"; import { cn } from "~/lib/utils"; diff --git a/apps/web/src/components/dashboard/search-navigation.tsx b/apps/web/src/components/dashboard/search-navigation.tsx index 560cc9ac..82805f43 100644 --- a/apps/web/src/components/dashboard/search-navigation.tsx +++ b/apps/web/src/components/dashboard/search-navigation.tsx @@ -7,8 +7,8 @@ import { Button } from "~/components/ui/button"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState, type ChangeEvent } from "react"; import { useDebounce } from "@uidotdev/usehooks"; -import type { SupportedLanguages } from "~/app/i18n/settings"; -import { useTranslation } from "~/app/i18n/client"; +import type { SupportedLanguages } from "~/i18n/settings"; +import { useTranslation } from "~/i18n/client"; interface ProjectGridProps { orgName: string; diff --git a/apps/web/src/components/modals/alertModal.tsx b/apps/web/src/components/modals/alertModal.tsx index 5589e3f2..635be4a7 100644 --- a/apps/web/src/components/modals/alertModal.tsx +++ b/apps/web/src/components/modals/alertModal.tsx @@ -12,8 +12,8 @@ import { } from "~/components/ui/alert-dialog"; import { Input } from "../ui/input"; -import { useTranslation } from "~/app/i18n/client"; -import type { SupportedLanguages } from "~/app/i18n/settings"; +import { useTranslation } from "~/i18n/client"; +import type { SupportedLanguages } from "~/i18n/settings"; type AlertModalProps = { isOpen: boolean; diff --git a/apps/web/src/components/navbar.tsx b/apps/web/src/components/navbar.tsx index 974fd755..0e122fdd 100644 --- a/apps/web/src/components/navbar.tsx +++ b/apps/web/src/components/navbar.tsx @@ -14,8 +14,8 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "./ui/dropdown-menu"; -import type { SupportedLanguages } from "~/app/i18n/settings"; -import { useTranslation } from "~/app/i18n/client"; +import type { SupportedLanguages } from "~/i18n/settings"; +import { useTranslation } from "~/i18n/client"; interface NavbarProps { lang: SupportedLanguages; diff --git a/apps/web/src/components/organization-select.tsx b/apps/web/src/components/organization-select.tsx index 6f8773f3..36a692be 100644 --- a/apps/web/src/components/organization-select.tsx +++ b/apps/web/src/components/organization-select.tsx @@ -45,8 +45,8 @@ import { import { Input } from "./ui/input"; import { Skeleton } from "./ui/skeleton"; import { useToast } from "./ui/toaster/use-toast"; -import { useTranslation } from "~/app/i18n/client"; -import type { SupportedLanguages } from "~/app/i18n/settings"; +import { useTranslation } from "~/i18n/client"; +import type { SupportedLanguages } from "~/i18n/settings"; interface OrganizationSelectProps { lang: SupportedLanguages; } diff --git a/apps/web/src/components/pricing-plans.tsx b/apps/web/src/components/pricing-plans.tsx index 9259e831..59fc3e83 100644 --- a/apps/web/src/components/pricing-plans.tsx +++ b/apps/web/src/components/pricing-plans.tsx @@ -13,8 +13,8 @@ import { CardHeader, CardTitle, } from "./ui/card"; -import { useTranslation } from "~/app/i18n/client"; -import type { SupportedLanguages } from "~/app/i18n/settings"; +import { useTranslation } from "~/i18n/client"; +import type { SupportedLanguages } from "~/i18n/settings"; interface PricingPlansProps { lang: SupportedLanguages; diff --git a/apps/web/src/app/i18n/client.ts b/apps/web/src/i18n/client.ts similarity index 100% rename from apps/web/src/app/i18n/client.ts rename to apps/web/src/i18n/client.ts diff --git a/apps/web/src/app/i18n/index.ts b/apps/web/src/i18n/index.ts similarity index 100% rename from apps/web/src/app/i18n/index.ts rename to apps/web/src/i18n/index.ts diff --git a/apps/web/src/app/i18n/locales/en/create-project.json b/apps/web/src/i18n/locales/en/create-project.json similarity index 100% rename from apps/web/src/app/i18n/locales/en/create-project.json rename to apps/web/src/i18n/locales/en/create-project.json diff --git a/apps/web/src/app/i18n/locales/en/home-page.json b/apps/web/src/i18n/locales/en/home-page.json similarity index 100% rename from apps/web/src/app/i18n/locales/en/home-page.json rename to apps/web/src/i18n/locales/en/home-page.json diff --git a/apps/web/src/app/i18n/locales/en/overview.json b/apps/web/src/i18n/locales/en/overview.json similarity index 100% rename from apps/web/src/app/i18n/locales/en/overview.json rename to apps/web/src/i18n/locales/en/overview.json diff --git a/apps/web/src/app/i18n/locales/en/settings.json b/apps/web/src/i18n/locales/en/settings.json similarity index 100% rename from apps/web/src/app/i18n/locales/en/settings.json rename to apps/web/src/i18n/locales/en/settings.json diff --git a/apps/web/src/app/i18n/locales/en/translation.json b/apps/web/src/i18n/locales/en/translation.json similarity index 100% rename from apps/web/src/app/i18n/locales/en/translation.json rename to apps/web/src/i18n/locales/en/translation.json diff --git a/apps/web/src/app/i18n/locales/pl/create-project.json b/apps/web/src/i18n/locales/pl/create-project.json similarity index 100% rename from apps/web/src/app/i18n/locales/pl/create-project.json rename to apps/web/src/i18n/locales/pl/create-project.json diff --git a/apps/web/src/app/i18n/locales/pl/home-page.json b/apps/web/src/i18n/locales/pl/home-page.json similarity index 100% rename from apps/web/src/app/i18n/locales/pl/home-page.json rename to apps/web/src/i18n/locales/pl/home-page.json diff --git a/apps/web/src/app/i18n/locales/pl/overview.json b/apps/web/src/i18n/locales/pl/overview.json similarity index 100% rename from apps/web/src/app/i18n/locales/pl/overview.json rename to apps/web/src/i18n/locales/pl/overview.json diff --git a/apps/web/src/app/i18n/locales/pl/settings.json b/apps/web/src/i18n/locales/pl/settings.json similarity index 100% rename from apps/web/src/app/i18n/locales/pl/settings.json rename to apps/web/src/i18n/locales/pl/settings.json diff --git a/apps/web/src/app/i18n/locales/pl/translation.json b/apps/web/src/i18n/locales/pl/translation.json similarity index 100% rename from apps/web/src/app/i18n/locales/pl/translation.json rename to apps/web/src/i18n/locales/pl/translation.json diff --git a/apps/web/src/app/i18n/settings.ts b/apps/web/src/i18n/settings.ts similarity index 100% rename from apps/web/src/app/i18n/settings.ts rename to apps/web/src/i18n/settings.ts diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index 72043862..db90a4a4 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -1,7 +1,7 @@ import { auth } from "./server/auth"; import { NextResponse } from "next/server"; import acceptLanguage from "accept-language"; -import { FALLBACK_LANG, LANGUAGES, COOKIE_NAME } from "./app/i18n/settings"; +import { FALLBACK_LANG, LANGUAGES, COOKIE_NAME } from "./i18n/settings"; const authedPathsRegex = new RegExp(`^/(${LANGUAGES.join("|")})/dashboard.*`); From 720916dd7a5773304ccfb11b4d4d9324888340f7 Mon Sep 17 00:00:00 2001 From: hubcio2115 Date: Thu, 26 Sep 2024 15:11:38 +0200 Subject: [PATCH 12/34] added lang property for fetching categories and languages --- .../web/src/lib/queries/useCategoriesQuery.ts | 16 +++++++------ apps/web/src/lib/queries/useLanguagesQuery.ts | 23 +++++++++++++------ apps/web/src/server/api/utils/project.ts | 15 +++++++----- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/apps/web/src/lib/queries/useCategoriesQuery.ts b/apps/web/src/lib/queries/useCategoriesQuery.ts index 47ce9900..d55705d6 100644 --- a/apps/web/src/lib/queries/useCategoriesQuery.ts +++ b/apps/web/src/lib/queries/useCategoriesQuery.ts @@ -6,25 +6,27 @@ import { type UseSuspenseQueryOptions, } from "@tanstack/react-query"; import ky from "ky"; +import type { SupportedLanguages } from "~/i18n/settings"; type UseCategoriesSuspenseQueryOptions = Omit< UseSuspenseQueryOptions< youtube_v3.Schema$VideoCategory[], Error, youtube_v3.Schema$VideoCategory[], - ["youtubeVideoCategories"] + ["youtubeVideoCategories", SupportedLanguages] >, "queryKey" | "queryFn" >; export function useCategoriesSuspenseQuery( + lang: SupportedLanguages, options: UseCategoriesSuspenseQueryOptions = {}, ) { return useSuspenseQuery({ - queryKey: ["youtubeVideoCategories"], + queryKey: ["youtubeVideoCategories", lang], queryFn: async () => ky - .get(`/api/youtube/categories`) + .get(`/api/youtube/categories/${lang}`) .json(), select: (data) => data.filter((category) => category.snippet?.assignable), staleTime: Number.POSITIVE_INFINITY, @@ -37,17 +39,17 @@ type UseCategoriesQueryOptions = Omit< youtube_v3.Schema$VideoCategory[], Error, youtube_v3.Schema$VideoCategory[], - ["youtubeVideoCategories"] + ["youtubeVideoCategories", SupportedLanguages] >, "queryKey" | "queryFn" >; -export function useCategoriesQuery(options: UseCategoriesQueryOptions = {}) { +export function useCategoriesQuery(lang: SupportedLanguages, options: UseCategoriesQueryOptions = {}) { return useQuery({ - queryKey: ["youtubeVideoCategories"], + queryKey: ["youtubeVideoCategories", lang], queryFn: async () => ky - .get(`/api/youtube/categories`) + .get(`/api/youtube/categories/${lang}`) .json(), select: (data) => data.filter((category) => category.snippet?.assignable), staleTime: Number.POSITIVE_INFINITY, diff --git a/apps/web/src/lib/queries/useLanguagesQuery.ts b/apps/web/src/lib/queries/useLanguagesQuery.ts index d407dc0c..00d5cb09 100644 --- a/apps/web/src/lib/queries/useLanguagesQuery.ts +++ b/apps/web/src/lib/queries/useLanguagesQuery.ts @@ -6,24 +6,28 @@ import { type UseSuspenseQueryOptions, } from "@tanstack/react-query"; import ky from "ky"; +import type { SupportedLanguages } from "~/i18n/settings"; type UseLanguagesSuspenseQueryOptions = Omit< UseSuspenseQueryOptions< youtube_v3.Schema$I18nLanguage[], Error, youtube_v3.Schema$I18nLanguage[], - ["youtubeLanguages"] + ["youtubeLanguages", SupportedLanguages] >, "queryKey" | "queryFn" >; export function useLanguagesSuspenseQuery( + lang: SupportedLanguages, options: UseLanguagesSuspenseQueryOptions = {}, ) { return useSuspenseQuery({ - queryKey: ["youtubeLanguages"], + queryKey: ["youtubeLanguages", lang], queryFn: async () => - ky.get("/api/youtube/languages").json(), + ky + .get(`/api/youtube/languages/${lang}`) + .json(), staleTime: Number.POSITIVE_INFINITY, ...options, }); @@ -34,16 +38,21 @@ type UseLanguagesQueryOptions = Omit< youtube_v3.Schema$I18nLanguage[], Error, youtube_v3.Schema$I18nLanguage[], - ["youtubeLanguages"] + ["youtubeLanguages", SupportedLanguages] >, "queryKey" | "queryFn" >; -export function useLanguagesQuery(options: UseLanguagesQueryOptions = {}) { +export function useLanguagesQuery( + lang: SupportedLanguages, + options: UseLanguagesQueryOptions = {}, +) { return useQuery({ - queryKey: ["youtubeLanguages"], + queryKey: ["youtubeLanguages", lang], queryFn: async () => - ky.get("/api/youtube/languages").json(), + ky + .get(`/api/youtube/languages/${lang}`) + .json(), staleTime: Number.POSITIVE_INFINITY, ...options, }); diff --git a/apps/web/src/server/api/utils/project.ts b/apps/web/src/server/api/utils/project.ts index ed65aead..a029dff8 100644 --- a/apps/web/src/server/api/utils/project.ts +++ b/apps/web/src/server/api/utils/project.ts @@ -4,12 +4,13 @@ import type { Result } from "~/lib/utils"; import type { Organization } from "~/lib/validators/organization"; import { OAuth2Client } from "google-auth-library"; import { getOwnerAccount, getOwnerId } from "./organizations"; +import type { SupportedLanguages } from "~/i18n/settings"; const youtubeClient = youtube("v3"); -export async function getYoutubeCategories(): Promise< - Result -> { +export async function getYoutubeCategories( + lang: SupportedLanguages, +): Promise> { try { const categories: youtube_v3.Schema$VideoCategoryListResponse = (await youtubeClient.videoCategories @@ -17,6 +18,7 @@ export async function getYoutubeCategories(): Promise< part: ["snippet"], regionCode: "PL", key: env.YOUTUBE_DATA_API_KEY, + hl: lang, }) .then((res) => res.data)) as any; @@ -26,15 +28,16 @@ export async function getYoutubeCategories(): Promise< } } -export async function getYoutubeSupportedLanguages(): Promise< - Result -> { +export async function getYoutubeSupportedLanguages( + lang: SupportedLanguages, +): Promise> { try { const languages: youtube_v3.Schema$I18nLanguageListResponse = (await youtubeClient.i18nLanguages .list({ part: ["snippet"], key: env.YOUTUBE_DATA_API_KEY, + hl: lang, }) .then((res) => res.data)) as any; From e450b895ec9a3a7a71be4ee743b6df26ba520950 Mon Sep 17 00:00:00 2001 From: hubcio2115 Date: Thu, 26 Sep 2024 15:12:00 +0200 Subject: [PATCH 13/34] full i18n --- .../dashboard/[name]/project/[id]/page.tsx | 18 ++- .../youtube/categories/{ => [lang]}/route.ts | 10 +- .../youtube/languages/{ => [lang]}/route.ts | 10 +- .../create-project/categories-select.tsx | 23 ++- .../create-project/channel-select.tsx | 21 ++- .../create-project/language-select.tsx | 24 ++- .../create-project/project-create-form.tsx | 148 ++++++++---------- .../src/components/dashboard/project-form.tsx | 145 ++++++++--------- .../src/components/dashboard/project-view.tsx | 78 ++++----- .../src/i18n/locales/en/create-project.json | 1 - .../web/src/i18n/locales/en/project-form.json | 84 ++++++++++ .../web/src/i18n/locales/en/project-page.json | 63 ++++++++ 12 files changed, 394 insertions(+), 231 deletions(-) rename apps/web/src/app/api/youtube/categories/{ => [lang]}/route.ts (59%) rename apps/web/src/app/api/youtube/languages/{ => [lang]}/route.ts (59%) delete mode 100644 apps/web/src/i18n/locales/en/create-project.json create mode 100644 apps/web/src/i18n/locales/en/project-form.json create mode 100644 apps/web/src/i18n/locales/en/project-page.json diff --git a/apps/web/src/app/[lang]/dashboard/[name]/project/[id]/page.tsx b/apps/web/src/app/[lang]/dashboard/[name]/project/[id]/page.tsx index 17d0a573..6ef5ad3e 100644 --- a/apps/web/src/app/[lang]/dashboard/[name]/project/[id]/page.tsx +++ b/apps/web/src/app/[lang]/dashboard/[name]/project/[id]/page.tsx @@ -1,5 +1,6 @@ import { redirect } from "next/navigation"; import ProjectView from "~/components/dashboard/project-view"; +import type { SupportedLanguages } from "~/i18n/settings"; import { getProjectById } from "~/server/actions/project"; import { getChannel, @@ -11,6 +12,7 @@ type ProjectPageProps = { params: { name: string; id: string; + lang: SupportedLanguages; }; }; @@ -35,12 +37,22 @@ export default async function ProjectPage({ params }: ProjectPageProps) { return redirect("/404"); } - const [languages, languagesErr] = await getYoutubeSupportedLanguages(); - const [categories, categoriesErr] = await getYoutubeCategories(); + const [languages, languagesErr] = await getYoutubeSupportedLanguages( + params.lang, + ); + const [categories, categoriesErr] = await getYoutubeCategories(params.lang); if (languagesErr !== null || categoriesErr !== null) { throw new Error("Something went wrong on our end"); } - return ; + return ( + + ); } diff --git a/apps/web/src/app/api/youtube/categories/route.ts b/apps/web/src/app/api/youtube/categories/[lang]/route.ts similarity index 59% rename from apps/web/src/app/api/youtube/categories/route.ts rename to apps/web/src/app/api/youtube/categories/[lang]/route.ts index c0d28e76..5c564c2b 100644 --- a/apps/web/src/app/api/youtube/categories/route.ts +++ b/apps/web/src/app/api/youtube/categories/[lang]/route.ts @@ -1,15 +1,19 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; +import type { SupportedLanguages } from "~/i18n/settings"; import { getYoutubeCategories } from "~/server/api/utils/project"; import { auth } from "~/server/auth"; -export async function GET() { +export async function GET( + _: NextRequest, + { params }: { params: { lang: SupportedLanguages } }, +) { const session = auth(); if (!session) { return NextResponse.json({ message: "UNAUTHORIZED" }, { status: 401 }); } - const [categories, err] = await getYoutubeCategories(); + const [categories, err] = await getYoutubeCategories(params.lang); if (err !== null) { return NextResponse.json( diff --git a/apps/web/src/app/api/youtube/languages/route.ts b/apps/web/src/app/api/youtube/languages/[lang]/route.ts similarity index 59% rename from apps/web/src/app/api/youtube/languages/route.ts rename to apps/web/src/app/api/youtube/languages/[lang]/route.ts index f22edb4a..f7ec5f7f 100644 --- a/apps/web/src/app/api/youtube/languages/route.ts +++ b/apps/web/src/app/api/youtube/languages/[lang]/route.ts @@ -1,15 +1,19 @@ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; +import type { SupportedLanguages } from "~/i18n/settings"; import { getYoutubeSupportedLanguages } from "~/server/api/utils/project"; import { auth } from "~/server/auth"; -export async function GET() { +export async function GET( + _: NextRequest, + { params }: { params: { lang: SupportedLanguages } }, +) { const session = auth(); if (!session) { return NextResponse.json({ message: "UNAUTHORIZED" }, { status: 401 }); } - const [languages, err] = await getYoutubeSupportedLanguages(); + const [languages, err] = await getYoutubeSupportedLanguages(params.lang); if (err !== null) { return NextResponse.json( diff --git a/apps/web/src/components/create-project/categories-select.tsx b/apps/web/src/components/create-project/categories-select.tsx index 8bef1740..8b942162 100644 --- a/apps/web/src/components/create-project/categories-select.tsx +++ b/apps/web/src/components/create-project/categories-select.tsx @@ -15,16 +15,27 @@ import { cn } from "~/lib/utils"; import type { ControllerRenderProps } from "react-hook-form"; import { Button } from "../ui/button"; import { useCategoriesSuspenseQuery } from "~/lib/queries/useCategoriesQuery"; +import { useTranslation } from "~/i18n/client"; +import type { SupportedLanguages } from "~/i18n/settings"; -export default function CategoriesSelect({ +interface CategorySelectProps extends Omit { + lang: SupportedLanguages; +} + +export default function CategorySelect({ value, onChange, disabled, -}: Omit) { - const { data: categories } = useCategoriesSuspenseQuery(); + lang, +}: CategorySelectProps) { + const { data: categories } = useCategoriesSuspenseQuery(lang); const [open, setOpen] = useState(false); + const { t } = useTranslation(lang, "project-form", { + keyPrefix: "category_select", + }); + const displayedValue = categories.find((category) => category.id === value) ?.snippet?.title; @@ -42,7 +53,7 @@ export default function CategoriesSelect({ aria-expanded={open} className="w-full justify-between" > - {displayedValue ?? "Select a category..."} + {displayedValue ?? t("placeholder")} @@ -62,10 +73,10 @@ export default function CategoriesSelect({ : 0; }} > - + - No category found. + {t("not_found")} {categories.map((category) => ( diff --git a/apps/web/src/components/create-project/channel-select.tsx b/apps/web/src/components/create-project/channel-select.tsx index 670bbd72..c3514f18 100644 --- a/apps/web/src/components/create-project/channel-select.tsx +++ b/apps/web/src/components/create-project/channel-select.tsx @@ -20,14 +20,25 @@ import { Button } from "../ui/button"; import { useParams } from "next/navigation"; import Image from "next/image"; import ky from "ky"; +import { useTranslation } from "~/i18n/client"; +import type { SupportedLanguages } from "~/i18n/settings"; -export default function ChannelsSelect({ +interface ChannelSelectProps extends Omit { + lang: SupportedLanguages; +} + +export default function ChannelSelect({ value, onChange, disabled, -}: Omit) { + lang, +}: ChannelSelectProps) { const { name } = useParams(); + const { t } = useTranslation(lang, "project-form", { + keyPrefix: "channel_select", + }); + const [open, setOpen] = useState(false); const { data: channels } = useSuspenseQuery({ @@ -78,7 +89,7 @@ export default function ChannelsSelect({ ); } - return

Select a channel...

; + return

{t("placeholder")}

; })()} @@ -86,10 +97,10 @@ export default function ChannelsSelect({ - + - No channel found. + {t("not_found")} {channels.map((channel) => ( diff --git a/apps/web/src/components/create-project/language-select.tsx b/apps/web/src/components/create-project/language-select.tsx index ae330d9b..faa5a86f 100644 --- a/apps/web/src/components/create-project/language-select.tsx +++ b/apps/web/src/components/create-project/language-select.tsx @@ -15,16 +15,28 @@ import { } from "../ui/command"; import { cn } from "~/lib/utils"; import { useLanguagesSuspenseQuery } from "~/lib/queries/useLanguagesQuery"; +import { useTranslation } from "~/i18n/client"; +import type { SupportedLanguages } from "~/i18n/settings"; -export default function LanguagesSelect({ +interface LanguageSelectProps + extends PropsWithChildren> { + lang: SupportedLanguages; +} + +export default function LanguageSelect({ value, onChange, disabled, -}: PropsWithChildren>) { - const { data: languages } = useLanguagesSuspenseQuery(); + lang, +}: LanguageSelectProps) { + const { data: languages } = useLanguagesSuspenseQuery(lang); const [open, setOpen] = useState(false); + const { t } = useTranslation(lang, "project-form", { + keyPrefix: "language_select", + }); + const displayedValue = languages.find((language) => language.id === value) ?.snippet?.name; @@ -42,7 +54,7 @@ export default function LanguagesSelect({ aria-expanded={open} className="w-full justify-between" > - {displayedValue ?? "Select a language..."} + {displayedValue ?? t("placeholder")} @@ -50,10 +62,10 @@ export default function LanguagesSelect({ - + - No category found. + {t("not_found")} {languages.map((language) => ( diff --git a/apps/web/src/components/create-project/project-create-form.tsx b/apps/web/src/components/create-project/project-create-form.tsx index 629fdcbb..b2c54350 100644 --- a/apps/web/src/components/create-project/project-create-form.tsx +++ b/apps/web/src/components/create-project/project-create-form.tsx @@ -20,9 +20,9 @@ import { import { Input } from "../ui/input"; import { Textarea } from "../ui/textarea"; import { Suspense, useEffect, useState } from "react"; -import CategoriesSelect from "./categories-select"; +import CategorySelect from "./categories-select"; import InputSkeleton from "../ui/skeletons/input-skeleton"; -import LanguagesSelect from "./language-select"; +import LanguageSelect from "./language-select"; import { Tooltip, TooltipContent, @@ -46,6 +46,7 @@ import ChannelsSelect from "./channel-select"; import type { UseCreateProjectMutationResult } from "~/lib/mutations/useCreateProjectMutation"; import { useTranslation } from "~/i18n/client"; import type { SupportedLanguages } from "~/i18n/settings"; +import { Trans } from "react-i18next"; interface ProjectFormProps { defaultValues: InsertProject; @@ -65,7 +66,7 @@ export default function ProjectCreateForm({ defaultValues, }); - const { t } = useTranslation(lang, "create-project", {}); + const { t } = useTranslation(lang, "project-form"); const [showMore, setShowMore] = useState(false); @@ -120,11 +121,11 @@ export default function ProjectCreateForm({ name="channelId" render={({ field: { ref: _ref, ...field } }) => ( - Channel: + {t("channel_select.label")}: }> - + @@ -138,13 +139,13 @@ export default function ProjectCreateForm({ name="video" render={({ field: { value, ...field } }) => ( - Video + {t("video_picker.label")}: { @@ -161,7 +162,7 @@ export default function ProjectCreateForm({ {showWholeForm ? ( <> -

Details

+

{t("details")}

( - Title: (required) + {t("title.label")}: @@ -178,12 +179,7 @@ export default function ProjectCreateForm({ -

- A catchy title can help you to hook viewers. When - you create video titles, it's a good idea to include - keywords that your audience is likely to use when - looking for videos like yours. -

+

{t("title.tooltip")}


@@ -192,7 +188,7 @@ export default function ProjectCreateForm({ target="_blank" className="text-blue-800 underline" > - Learn more + {t("learn_more")}
@@ -204,7 +200,7 @@ export default function ProjectCreateForm({ {...field} type="text" value={field.value ?? ""} - placeholder="Add a title that describes your video (type @ to mention a channel)" + placeholder={t("title.placeholder")} />
@@ -219,7 +215,7 @@ export default function ProjectCreateForm({ render={({ field }) => ( - Description: + {t("description.label")}: @@ -228,12 +224,7 @@ export default function ProjectCreateForm({ -

- Writing descriptions with keywords can help viewers - to find your videos more easily through search. You - can give an overview of your video and place - keywords at the beginning of the description. -

+

{t("description.tooltip")}


@@ -242,7 +233,7 @@ export default function ProjectCreateForm({ target="_blank" className="text-blue-800 underline" > - Learn more + {t("learn_more")}
@@ -254,7 +245,7 @@ export default function ProjectCreateForm({ {...field} className="resize-none h-64" value={field.value ?? ""} - placeholder="Tell viewers about your video (type @ to mention a channel)" + placeholder={t("description.placeholder")} /> @@ -263,45 +254,36 @@ export default function ProjectCreateForm({ )} /> -

Audience

+

{t("audience")}

( - - Is this video 'Made for Kids'? (required) - + {t("made_for_kids.label")} - Regardless of your location, you're legally required to - comply with the Children's Online Privacy Protection Act - (COPPA) and/or other laws. You're required to tell us - whether your videos are 'Made for Kids'.{" "} - - What is 'Made for Kids' content? - + + + - Features like personalised ads and notifications won't be - available on videos 'Made for Kids'. Videos that are set - as 'Made for Kids' by you are more likely to be - recommended alongside other children's videos.{" "} + {t("made_for_kids.alert_description")}{" "} - Learn More + {t("learn_more")} @@ -325,7 +307,7 @@ export default function ProjectCreateForm({ - Yes, it's 'Made for Kids' + {t("made_for_kids.yes_label")}
@@ -333,7 +315,7 @@ export default function ProjectCreateForm({ - No, it's not 'Made for Kids' + {t("made_for_kids.no_label")}
@@ -351,7 +333,7 @@ export default function ProjectCreateForm({ }} className="rounded-full max-w-max" > - {showMore ? "Show less" : "Show more"} + {showMore ? t("show_more") : t("show_less")} {showMore ? ( @@ -364,19 +346,17 @@ export default function ProjectCreateForm({
- Tags + {t("tags.label")} - Tags can be useful if content in your video is - commonly misspelt. Otherwise, tags play a minimal - role in helping viewers to find your video.{" "} + {t("tags.description")}{" "} - Learn more + {t("learn_more")}
@@ -387,8 +367,7 @@ export default function ProjectCreateForm({ - Enter a comma after each tag {value?.length ?? 0} - /500 + {t("tags.counter", { counter: value?.length ?? 0 })} @@ -406,21 +385,22 @@ export default function ProjectCreateForm({
- Language: + {t("language_select.label")} - Select your video's language + {t("language_select.description")}
}> - @@ -440,18 +420,16 @@ export default function ProjectCreateForm({
- License + {t("license_select.label")} - Learn about{" "} - - license types - - . + + +
@@ -469,11 +447,11 @@ export default function ProjectCreateForm({ - Standard YouTube License + {t("license_select.youtube")} - Creative Commons - Attribution + {t("license_select.creative_commons")} @@ -492,19 +470,18 @@ export default function ProjectCreateForm({
- Category: + {t("category_select.label")}: - Add your video to a category so that viewers can - find it more easily + {t("category_select.description")}
) : ( -

Paid promotion, tags and more

+

{t("paid_promotion_and_more")}

)} ) : null} diff --git a/apps/web/src/components/dashboard/project-form.tsx b/apps/web/src/components/dashboard/project-form.tsx index 6c8da26f..abab1f8e 100644 --- a/apps/web/src/components/dashboard/project-form.tsx +++ b/apps/web/src/components/dashboard/project-form.tsx @@ -39,25 +39,32 @@ import { } from "../ui/select"; import { Checkbox } from "../ui/checkbox"; import type { UseCreateProjectMutationResult } from "~/lib/mutations/useCreateProjectMutation"; -import CategoriesSelect from "../create-project/categories-select"; -import LanguagesSelect from "../create-project/language-select"; +import CategorySelect from "../create-project/categories-select"; +import LanguageSelect from "../create-project/language-select"; +import type { SupportedLanguages } from "~/i18n/settings"; +import { useTranslation } from "~/i18n/client"; +import { Trans } from "react-i18next"; interface ProjectFormProps { defaultValues: InsertProject; mutate: UseCreateProjectMutationResult["mutate"]; isPending?: UseCreateProjectMutationResult["isPending"]; + lang: SupportedLanguages; } export default function ProjectForm({ mutate, isPending = false, defaultValues, + lang, }: ProjectFormProps) { const form = useForm({ resolver: zodResolver(projectFormSchema), defaultValues, }); + const { t } = useTranslation(lang, "project-form"); + const formValues = form.watch(); useEffect(() => { @@ -82,13 +89,11 @@ export default function ProjectForm({ - Currently it is impossible to change the target channel or an - uploaded video. Please create a new project if you need to change - the target channel or video. + {t("cant_change_channel_and_video_info")} -

Details

+

{t("details")}

( - Title: (required) + {t("title.label")} @@ -105,12 +110,7 @@ export default function ProjectForm({ -

- A catchy title can help you to hook viewers. When you - create video titles, it's a good idea to include - keywords that your audience is likely to use when - looking for videos like yours. -

+

{t("title.tooltip")}


@@ -119,7 +119,7 @@ export default function ProjectForm({ target="_blank" className="text-blue-800 underline" > - Learn more + {t("learn_more")}
@@ -131,7 +131,7 @@ export default function ProjectForm({ {...field} type="text" value={field.value ?? ""} - placeholder="Add a title that describes your video (type @ to mention a channel)" + placeholder={t("title.placeholder")} /> @@ -146,7 +146,7 @@ export default function ProjectForm({ render={({ field }) => ( - Description: + {t("description.label")}: @@ -155,12 +155,7 @@ export default function ProjectForm({ -

- Writing descriptions with keywords can help viewers to - find your videos more easily through search. You can - give an overview of your video and place keywords at the - beginning of the description. -

+

{t("description.tooltip")}


@@ -169,7 +164,7 @@ export default function ProjectForm({ target="_blank" className="text-blue-800 underline" > - Learn more + {t("learn_more")}
@@ -181,7 +176,7 @@ export default function ProjectForm({ {...field} className="resize-none h-64" value={field.value ?? ""} - placeholder="Tell viewers about your video (type @ to mention a channel)" + placeholder={t("description.placeholder")} /> @@ -190,43 +185,36 @@ export default function ProjectForm({ )} /> -

Audience

+

{t("audience")}

( - Is this video 'Made for Kids'? (required) + {t("made_for_kids.label")} - Regardless of your location, you're legally required to comply - with the Children's Online Privacy Protection Act (COPPA) and/or - other laws. You're required to tell us whether your videos are - 'Made for Kids'.{" "} - - What is 'Made for Kids' content? - + + + - Features like personalised ads and notifications won't be - available on videos 'Made for Kids'. Videos that are set as - 'Made for Kids' by you are more likely to be recommended - alongside other children's videos.{" "} + {t("made_for_kids.alert_description")}{" "} - Learn More + {t("learn_more")} @@ -250,7 +238,7 @@ export default function ProjectForm({ - Yes, it's 'Made for Kids' + {t("made_for_kids.yes_label")}
@@ -258,7 +246,7 @@ export default function ProjectForm({ - No, it's not 'Made for Kids' + {t("made_for_kids.no_label")} @@ -276,7 +264,7 @@ export default function ProjectForm({ }} className="rounded-full max-w-max" > - {showMore ? "Show less" : "Show more"} + {showMore ? t("show_more") : t("show_less")} {showMore ? ( @@ -288,18 +276,18 @@ export default function ProjectForm({ render={({ field: { value, ...field } }) => (
- Tags + + {t("tags.label")} + - Tags can be useful if content in your video is commonly - misspelt. Otherwise, tags play a minimal role in helping - viewers to find your video.{" "} + {t("tags.description")}{" "} - Learn more + {t("learn_more")}
@@ -310,8 +298,7 @@ export default function ProjectForm({ - Enter a comma after each tag {value?.length ?? 0} - /500 + {t("tags.counter", { counter: value?.length ?? 0 })} @@ -329,18 +316,19 @@ export default function ProjectForm({
- Language: + {t("language_select.label")} - Select your video's language + {t("language_select.description")}
}> -
- License + {t("license_select.label")} - Learn about{" "} - - license types - - . + + +
@@ -392,11 +378,11 @@ export default function ProjectForm({ - Standard YouTube License + {t("license_select.youtube")} - Creative Commons - Attribution + {t("license_select.creative_commons")} @@ -415,19 +401,18 @@ export default function ProjectForm({
- Category: + {t("category_select.label")}: - Add your video to a category so that viewers can find it - more easily + {t("category_select.description")}
) : ( -

Paid promotion, tags and more

+

{t("paid_promotion_and_more")}

)} diff --git a/apps/web/src/components/dashboard/project-view.tsx b/apps/web/src/components/dashboard/project-view.tsx index e2809c41..22cd15fc 100644 --- a/apps/web/src/components/dashboard/project-view.tsx +++ b/apps/web/src/components/dashboard/project-view.tsx @@ -11,22 +11,29 @@ import { Badge } from "../ui/badge"; import { GitPullRequestClosed, Merge } from "lucide-react"; import { useLanguagesQuery } from "~/lib/queries/useLanguagesQuery"; import { useCategoriesQuery } from "~/lib/queries/useCategoriesQuery"; +import type { SupportedLanguages } from "~/i18n/settings"; +import { useTranslation } from "~/i18n/client"; interface ProjectDisplayProps { project: Project; channel: youtube_v3.Schema$Channel; + lang: SupportedLanguages; } -function ProjectDisplay({ channel, project }: ProjectDisplayProps) { +function ProjectDisplay({ channel, project, lang }: ProjectDisplayProps) { const [showWholeDescription, setShowWholeDescription] = useState(false); const descriptionLines = project.description.split("\n"); - const { data: languages } = useLanguagesQuery(); + const { t } = useTranslation(lang, "project-page", { + keyPrefix: "project_display", + }); + + const { data: languages } = useLanguagesQuery(lang); const language = languages?.find( (language) => language.id === project.defaultLanguage, ); - const { data: categories } = useCategoriesQuery(); + const { data: categories } = useCategoriesQuery(lang); const category = categories?.find( (category) => category.id === project.categoryId, ); @@ -55,7 +62,7 @@ function ProjectDisplay({ channel, project }: ProjectDisplayProps) { disabled className="rounded-full my-auto disabled:opacity-100" > - Subscribe + {t("subscribe")}
@@ -70,57 +77,52 @@ function ProjectDisplay({ channel, project }: ProjectDisplayProps) { className="rounded-full w-max" onClick={() => setShowWholeDescription((prev) => !prev)} > - {showWholeDescription ? "Show less" : "Show more"} + {showWholeDescription ? t("show_more") : t("show_less")}
-

Tags:

-

{project.tags || "None"}

+

{t("tags.label")}:

+

{project.tags || t("tags.empty")}

-

License:

-

- {(() => { - switch (project.license) { - case "youtube": - return "Standard YouTube License"; - case "creativeCommon": - return "Creative Commons - Attribution"; - } - })()} -

+

{t("license.label")}:

+

{t(`license.${project.license}`)}

-

Embeddable:

-

{project.embeddable ? "Yes" : "No"}

+

{t("embeddable.label")}:

+

{project.embeddable ? t("yes") : t("no")}

-

Language:

+

{t("language.label")}:

{language?.snippet?.name}

-

Stats:

-

{project.publicStatsViewable ? "Public" : "Private"}

+

{t("stats.label")}:

+

+ {project.publicStatsViewable + ? t("stats.public") + : t("stats.private")} +

-

'Made for Kids':

-

{project.selfDeclaredMadeForKids ? "Yes" : "No"}

+

{t("made_for_kids.label")}:

+

{project.selfDeclaredMadeForKids ? t("yes") : t("no")}

-

Notify Subscribers:

-

{project.notifySubscribers ? "Yes" : "No"}

+

{t("notify_subscribers.label")}:

+

{project.notifySubscribers ? t("yes") : t("no")}

-

Category

+

{t("category.label")}

{category?.snippet?.title}

@@ -128,12 +130,12 @@ function ProjectDisplay({ channel, project }: ProjectDisplayProps) {
@@ -145,6 +147,7 @@ interface ProjectViewProps { channel: youtube_v3.Schema$Channel; languages: youtube_v3.Schema$I18nLanguage[]; categories: youtube_v3.Schema$VideoCategory[]; + lang: SupportedLanguages; } export default function ProjectView({ @@ -152,11 +155,14 @@ export default function ProjectView({ channel, languages, categories, + lang, }: ProjectViewProps) { const [isEdititng, setIsEditing] = useState(false); const queryClient = useQueryClient(); + const { t } = useTranslation(lang, "project-page"); + useEffect(() => { if (!queryClient.getQueryData(["youtubeVideoCategories"])) { queryClient.setQueryData(["youtubeVideoCategories"], categories); @@ -173,8 +179,8 @@ export default function ProjectView({
-

Status:

- Unlisted +

{t("status.label")}:

+ {t("status.unlisted")}