diff --git a/next.config.ts b/next.config.ts index 260c89d..1d73d67 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,9 @@ import type { NextConfig } from "next" +import createNextIntlPlugin from "next-intl/plugin" const isProd = process.env.NODE_ENV === "production" const internalHost = process.env.TAURI_DEV_HOST || "localhost" +const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts") const nextConfig: NextConfig = { output: "export", @@ -11,4 +13,4 @@ const nextConfig: NextConfig = { assetPrefix: isProd ? undefined : `http://${internalHost}:3000`, } -export default nextConfig +export default withNextIntl(nextConfig) diff --git a/package.json b/package.json index cb239e8..0d8417b 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "monaco-editor": "^0.55.1", "motion": "^12.34.0", "next": "^16", + "next-intl": "^4.8.3", "next-themes": "^0.4.6", "postcss": "^8.5.6", "radix-ui": "^1.4.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 719c020..e8d3be0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,6 +95,9 @@ importers: next: specifier: ^16 version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next-intl: + specifier: ^4.8.3 + version: 4.8.3(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.8.3) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -618,6 +621,21 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@formatjs/ecma402-abstract@3.1.1': + resolution: {integrity: sha512-jhZbTwda+2tcNrs4kKvxrPLPjx8QsBCLCUgrrJ/S+G9YrGHWLhAyFMMBHJBnBoOwuLHd7L14FgYudviKaxkO2Q==} + + '@formatjs/fast-memoize@3.1.0': + resolution: {integrity: sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg==} + + '@formatjs/icu-messageformat-parser@3.5.1': + resolution: {integrity: sha512-sSDmSvmmoVQ92XqWb499KrIhv/vLisJU8ITFrx7T7NZHUmMY7EL9xgRowAosaljhqnj/5iufG24QrdzB6X3ItA==} + + '@formatjs/icu-skeleton-parser@2.1.1': + resolution: {integrity: sha512-PSFABlcNefjI6yyk8f7nyX1DC7NHmq6WaCHZLySEXBrXuLOB2f935YsnzuPjlz+ibhb9yWTdPeVX1OVcj24w2Q==} + + '@formatjs/intl-localematcher@0.8.1': + resolution: {integrity: sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==} + '@giscus/react@3.1.0': resolution: {integrity: sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg==} peerDependencies: @@ -1038,6 +1056,94 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + '@pierre/diffs@1.0.11': resolution: {integrity: sha512-j6zIEoyImQy1HfcJqbrDwP0O5I7V2VNXAaw53FqQ+SykRfaNwABeZHs9uibXO4supaXPmTx6LEH9Lffr03e1Tw==} peerDependencies: @@ -2076,6 +2182,9 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@schummar/icu-type-parser@1.21.5': + resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==} + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -2156,9 +2265,88 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 + '@swc/core-darwin-arm64@1.15.18': + resolution: {integrity: sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.18': + resolution: {integrity: sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.18': + resolution: {integrity: sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.18': + resolution: {integrity: sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-arm64-musl@1.15.18': + resolution: {integrity: sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@swc/core-linux-x64-gnu@1.15.18': + resolution: {integrity: sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-musl@1.15.18': + resolution: {integrity: sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@swc/core-win32-arm64-msvc@1.15.18': + resolution: {integrity: sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.18': + resolution: {integrity: sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.18': + resolution: {integrity: sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.18': + resolution: {integrity: sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/types@0.1.25': + resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} + '@tailwindcss/node@4.1.18': resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} @@ -3307,6 +3495,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} @@ -4032,6 +4223,9 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + icu-minify@4.8.3: + resolution: {integrity: sha512-65Av7FLosNk7bPbmQx5z5XG2Y3T2GFppcjiXh4z1idHeVgQxlDpAmkGoYI0eFzAvrOnjpWTL5FmPDhsdfRMPEA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -4072,6 +4266,9 @@ packages: resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} deprecated: The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019. + intl-messageformat@11.1.2: + resolution: {integrity: sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==} + ip-address@10.0.1: resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} engines: {node: '>= 12'} @@ -4895,6 +5092,19 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + next-intl-swc-plugin-extractor@4.8.3: + resolution: {integrity: sha512-YcaT+R9z69XkGhpDarVFWUprrCMbxgIQYPUaXoE6LGVnLjGdo8hu3gL6bramDVjNKViYY8a/pXPy7Bna0mXORg==} + + next-intl@4.8.3: + resolution: {integrity: sha512-PvdBDWg+Leh7BR7GJUQbCDVVaBRn37GwDBWc9sv0rVQOJDQ5JU1rVzx9EEGuOGYo0DHAl70++9LQ7HxTawdL7w==} + peerDependencies: + next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -4922,6 +5132,9 @@ packages: sass: optional: true + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -5110,6 +5323,9 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + po-parser@2.1.1: + resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==} + points-on-curve@0.2.0: resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} @@ -6062,6 +6278,11 @@ packages: '@types/react': optional: true + use-intl@4.8.3: + resolution: {integrity: sha512-nLxlC/RH+le6g3amA508Itnn/00mE+J22ui21QhOWo5V9hCEC43+WtnRAITbJW0ztVZphev5X9gvOf2/Dk9PLA==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + use-merge-value@1.2.0: resolution: {integrity: sha512-DXgG0kkgJN45TcyoXL49vJnn55LehnrmoHc7MbKi+QDBvr8dsesqws8UlyIWGHMR+JXgxc1nvY+jDGMlycsUcw==} peerDependencies: @@ -6848,6 +7069,33 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@formatjs/ecma402-abstract@3.1.1': + dependencies: + '@formatjs/fast-memoize': 3.1.0 + '@formatjs/intl-localematcher': 0.8.1 + decimal.js: 10.6.0 + tslib: 2.8.1 + + '@formatjs/fast-memoize@3.1.0': + dependencies: + tslib: 2.8.1 + + '@formatjs/icu-messageformat-parser@3.5.1': + dependencies: + '@formatjs/ecma402-abstract': 3.1.1 + '@formatjs/icu-skeleton-parser': 2.1.1 + tslib: 2.8.1 + + '@formatjs/icu-skeleton-parser@2.1.1': + dependencies: + '@formatjs/ecma402-abstract': 3.1.1 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.8.1': + dependencies: + '@formatjs/fast-memoize': 3.1.0 + tslib: 2.8.1 + '@giscus/react@3.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: giscus: 1.6.0 @@ -7304,6 +7552,66 @@ snapshots: '@opentelemetry/api@1.9.0': {} + '@parcel/watcher-android-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + + '@parcel/watcher-win32-x64@2.5.6': + optional: true + + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.3 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + '@pierre/diffs@1.0.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@shikijs/core': 3.23.0 @@ -8463,6 +8771,8 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@schummar/icu-type-parser@1.21.5': {} + '@sec-ant/readable-stream@0.4.1': {} '@shikijs/core@3.22.0': @@ -8578,10 +8888,62 @@ snapshots: mermaid: 11.12.2 react: 19.2.4 + '@swc/core-darwin-arm64@1.15.18': + optional: true + + '@swc/core-darwin-x64@1.15.18': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.18': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.18': + optional: true + + '@swc/core-linux-arm64-musl@1.15.18': + optional: true + + '@swc/core-linux-x64-gnu@1.15.18': + optional: true + + '@swc/core-linux-x64-musl@1.15.18': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.18': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.18': + optional: true + + '@swc/core-win32-x64-msvc@1.15.18': + optional: true + + '@swc/core@1.15.18': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.25 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.18 + '@swc/core-darwin-x64': 1.15.18 + '@swc/core-linux-arm-gnueabihf': 1.15.18 + '@swc/core-linux-arm64-gnu': 1.15.18 + '@swc/core-linux-arm64-musl': 1.15.18 + '@swc/core-linux-x64-gnu': 1.15.18 + '@swc/core-linux-x64-musl': 1.15.18 + '@swc/core-win32-arm64-msvc': 1.15.18 + '@swc/core-win32-ia32-msvc': 1.15.18 + '@swc/core-win32-x64-msvc': 1.15.18 + + '@swc/counter@0.1.3': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 + '@swc/types@0.1.25': + dependencies: + '@swc/counter': 0.1.3 + '@tailwindcss/node@4.1.18': dependencies: '@jridgewell/remapping': 2.3.5 @@ -9789,6 +10151,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 @@ -10779,6 +11143,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + icu-minify@4.8.3: + dependencies: + '@formatjs/icu-messageformat-parser': 3.5.1 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -10808,6 +11176,13 @@ snapshots: intersection-observer@0.12.2: {} + intl-messageformat@11.1.2: + dependencies: + '@formatjs/ecma402-abstract': 3.1.1 + '@formatjs/fast-memoize': 3.1.0 + '@formatjs/icu-messageformat-parser': 3.5.1 + tslib: 2.8.1 + ip-address@10.0.1: {} ipaddr.js@1.9.1: {} @@ -11911,6 +12286,25 @@ snapshots: negotiator@1.0.0: {} + next-intl-swc-plugin-extractor@4.8.3: {} + + next-intl@4.8.3(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.8.3): + dependencies: + '@formatjs/intl-localematcher': 0.8.1 + '@parcel/watcher': 2.5.6 + '@swc/core': 1.15.18 + icu-minify: 4.8.3 + negotiator: 1.0.0 + next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next-intl-swc-plugin-extractor: 4.8.3 + po-parser: 2.1.1 + react: 19.2.4 + use-intl: 4.8.3(react@19.2.4) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - '@swc/helpers' + next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: react: 19.2.4 @@ -11941,6 +12335,8 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-addon-api@7.1.1: {} + node-domexception@1.0.0: {} node-fetch@3.3.2: @@ -12143,6 +12539,8 @@ snapshots: mlly: 1.8.0 pathe: 2.0.3 + po-parser@2.1.1: {} + points-on-curve@0.2.0: {} points-on-path@0.2.1: @@ -13429,6 +13827,14 @@ snapshots: optionalDependencies: '@types/react': 19.2.13 + use-intl@4.8.3(react@19.2.4): + dependencies: + '@formatjs/fast-memoize': 3.1.0 + '@schummar/icu-type-parser': 1.21.5 + icu-minify: 4.8.3 + intl-messageformat: 11.1.2 + react: 19.2.4 + use-merge-value@1.2.0(react@19.2.4): dependencies: react: 19.2.4 diff --git a/src-tauri/src/commands/system_settings.rs b/src-tauri/src/commands/system_settings.rs index dc8cb95..8c0f16e 100644 --- a/src-tauri/src/commands/system_settings.rs +++ b/src-tauri/src/commands/system_settings.rs @@ -3,10 +3,11 @@ use tauri::State; use crate::db::service::app_metadata_service; use crate::db::AppDatabase; -use crate::models::SystemProxySettings; +use crate::models::{SystemLanguageSettings, SystemProxySettings}; use crate::network::proxy; const SYSTEM_PROXY_SETTINGS_KEY: &str = "system_proxy_settings"; +const SYSTEM_LANGUAGE_SETTINGS_KEY: &str = "system_language_settings"; fn normalize_proxy_settings(settings: SystemProxySettings) -> Result { if !settings.enabled { @@ -54,6 +55,21 @@ pub(crate) async fn load_system_proxy_settings( normalize_proxy_settings(parsed) } +pub(crate) async fn load_system_language_settings( + conn: &DatabaseConnection, +) -> Result { + let raw = app_metadata_service::get_value(conn, SYSTEM_LANGUAGE_SETTINGS_KEY) + .await + .map_err(|e| e.to_string())?; + + let Some(raw) = raw else { + return Ok(SystemLanguageSettings::default()); + }; + + serde_json::from_str::(&raw) + .map_err(|e| format!("failed to parse stored language settings: {e}")) +} + #[tauri::command] pub async fn get_system_proxy_settings( db: State<'_, AppDatabase>, @@ -76,3 +92,24 @@ pub async fn update_system_proxy_settings( proxy::apply_system_proxy_settings(&normalized)?; Ok(normalized) } + +#[tauri::command] +pub async fn get_system_language_settings( + db: State<'_, AppDatabase>, +) -> Result { + load_system_language_settings(&db.conn).await +} + +#[tauri::command] +pub async fn update_system_language_settings( + settings: SystemLanguageSettings, + db: State<'_, AppDatabase>, +) -> Result { + let serialized = serde_json::to_string(&settings).map_err(|e| e.to_string())?; + + app_metadata_service::upsert_value(&db.conn, SYSTEM_LANGUAGE_SETTINGS_KEY, &serialized) + .await + .map_err(|e| e.to_string())?; + + Ok(settings) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dcdc1d5..1d6c12b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -220,6 +220,8 @@ pub fn run() { windows::focus_folder_window, system_settings::get_system_proxy_settings, system_settings::update_system_proxy_settings, + system_settings::get_system_language_settings, + system_settings::update_system_language_settings, acp_commands::acp_preflight, acp_commands::acp_connect, acp_commands::acp_prompt, diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 3c53771..11b349e 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -12,4 +12,4 @@ pub use conversation::{ }; pub use folder::{FolderCommandInfo, FolderDetail, FolderHistoryEntry, OpenedConversation}; pub use message::{ContentBlock, MessageRole, MessageTurn, TurnRole, TurnUsage, UnifiedMessage}; -pub use system::SystemProxySettings; +pub use system::{SystemLanguageSettings, SystemProxySettings}; diff --git a/src-tauri/src/models/system.rs b/src-tauri/src/models/system.rs index 4523bb9..8b5a9e6 100644 --- a/src-tauri/src/models/system.rs +++ b/src-tauri/src/models/system.rs @@ -5,3 +5,27 @@ pub struct SystemProxySettings { pub enabled: bool, pub proxy_url: Option, } + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum AppLocale { + #[default] + En, + ZhCn, + ZhTw, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum LanguageMode { + #[default] + System, + Manual, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(default)] +pub struct SystemLanguageSettings { + pub mode: LanguageMode, + pub language: AppLocale, +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 714a08e..4195e01 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,10 @@ import type { Metadata } from "next" import "./globals.css" import { JetBrains_Mono } from "next/font/google" +import { NextIntlClientProvider } from "next-intl" +import { AppI18nProvider } from "@/components/i18n-provider" import { ThemeProvider } from "@/components/theme-provider" +import enMessages from "@/i18n/messages/en.json" const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], @@ -21,14 +24,18 @@ export default function RootLayout({ return ( - - {children} - + + + + {children} + + + ) diff --git a/src/app/settings/agents/page.tsx b/src/app/settings/agents/page.tsx index ac06352..eed094a 100644 --- a/src/app/settings/agents/page.tsx +++ b/src/app/settings/agents/page.tsx @@ -1,12 +1,17 @@ +"use client" + import { Suspense } from "react" +import { useTranslations } from "next-intl" import { AcpAgentSettings } from "@/components/settings/acp-agent-settings" export default function SettingsAgentsPage() { + const t = useTranslations("SettingsPages") + return ( - 加载 Agent 设置中... + {t("agentsLoading")} } > diff --git a/src/components/i18n-provider.tsx b/src/components/i18n-provider.tsx new file mode 100644 index 0000000..9ba982d --- /dev/null +++ b/src/components/i18n-provider.tsx @@ -0,0 +1,141 @@ +"use client" + +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + useSyncExternalStore, +} from "react" +import { NextIntlClientProvider, type AbstractIntlMessages } from "next-intl" +import enMessages from "@/i18n/messages/en.json" +import zhCNMessages from "@/i18n/messages/zh-CN.json" +import zhTWMessages from "@/i18n/messages/zh-TW.json" +import { + APP_LOCALE_TO_INTL_LOCALE, + DEFAULT_LANGUAGE_SETTINGS, + getSystemLocaleCandidates, + normalizeLanguageSettings, + resolveAppLocale, +} from "@/lib/i18n" +import { getSystemLanguageSettings } from "@/lib/tauri" +import type { AppLocale, SystemLanguageSettings } from "@/lib/types" + +interface AppI18nContextValue { + appLocale: AppLocale + languageSettings: SystemLanguageSettings + languageSettingsLoaded: boolean + setLanguageSettings: (settings: SystemLanguageSettings) => void +} + +const MESSAGES_BY_LOCALE: Record = { + en: enMessages, + zh_cn: zhCNMessages, + zh_tw: zhTWMessages, +} + +const AppI18nContext = createContext(null) + +function subscribeSystemLocale(onStoreChange: () => void) { + if (typeof window === "undefined") return () => {} + + window.addEventListener("languagechange", onStoreChange) + return () => { + window.removeEventListener("languagechange", onStoreChange) + } +} + +function getSystemLocaleSnapshot(): string { + return getSystemLocaleCandidates().join("|") +} + +function getSystemLocaleServerSnapshot(): string { + return "" +} + +export function useAppI18n() { + const context = useContext(AppI18nContext) + if (!context) { + throw new Error("useAppI18n must be used within AppI18nProvider") + } + return context +} + +export function AppI18nProvider({ children }: { children: React.ReactNode }) { + const [languageSettings, setLanguageSettingsState] = + useState(DEFAULT_LANGUAGE_SETTINGS) + const [languageSettingsLoaded, setLanguageSettingsLoaded] = useState(false) + + const systemLocaleSnapshot = useSyncExternalStore( + subscribeSystemLocale, + getSystemLocaleSnapshot, + getSystemLocaleServerSnapshot + ) + const systemLocaleCandidates = useMemo( + () => (systemLocaleSnapshot ? systemLocaleSnapshot.split("|") : []), + [systemLocaleSnapshot] + ) + + const setLanguageSettings = useCallback( + (settings: SystemLanguageSettings) => { + setLanguageSettingsState(normalizeLanguageSettings(settings)) + }, + [] + ) + + useEffect(() => { + let cancelled = false + + getSystemLanguageSettings() + .then((settings) => { + if (cancelled) return + setLanguageSettings(settings) + }) + .catch((err) => { + console.error("[i18n] load language settings failed:", err) + }) + .finally(() => { + if (!cancelled) { + setLanguageSettingsLoaded(true) + } + }) + + return () => { + cancelled = true + } + }, [setLanguageSettings]) + + const appLocale = useMemo( + () => resolveAppLocale(languageSettings, systemLocaleCandidates), + [languageSettings, systemLocaleCandidates] + ) + + const intlLocale = APP_LOCALE_TO_INTL_LOCALE[appLocale] + + useEffect(() => { + document.documentElement.lang = intlLocale + }, [intlLocale]) + + const contextValue = useMemo( + () => ({ + appLocale, + languageSettings, + languageSettingsLoaded, + setLanguageSettings, + }), + [appLocale, languageSettings, languageSettingsLoaded, setLanguageSettings] + ) + + return ( + + + {children} + + + ) +} diff --git a/src/components/settings/appearance-settings.tsx b/src/components/settings/appearance-settings.tsx index 11b0b43..22c18e1 100644 --- a/src/components/settings/appearance-settings.tsx +++ b/src/components/settings/appearance-settings.tsx @@ -1,6 +1,7 @@ "use client" import { Monitor, Moon, Sun } from "lucide-react" +import { useTranslations } from "next-intl" import { useTheme } from "next-themes" import { Select, @@ -13,13 +14,14 @@ import { type ThemeMode = "system" | "light" | "dark" export function AppearanceSettings() { + const t = useTranslations("AppearanceSettings") const { theme, resolvedTheme, setTheme } = useTheme() const resolvedThemeLabel = resolvedTheme === "dark" - ? "深色" + ? t("resolvedTheme.dark") : resolvedTheme === "light" - ? "浅色" - : "--" + ? t("resolvedTheme.light") + : t("resolvedTheme.unknown") return (
@@ -27,41 +29,41 @@ export function AppearanceSettings() {
-

主题外观

+

{t("sectionTitle")}

- 选择浅色、深色或跟随系统主题,设置会自动保存。 + {t("sectionDescription")}

- 支持 http(s)/socks5,示例:{PROXY_EXAMPLE} - 。仅在启用系统代理时生效。 + {t("proxyHint", { example: PROXY_EXAMPLE })}

@@ -210,12 +284,69 @@ export function SystemNetworkSettings() { {saving ? ( <> - 保存中... + {t("saving")} ) : ( <> - 保存 + {t("save")} + + )} + +
+ + +
+
+ +

{t("languageTitle")}

+
+ +

+ {t("languageDescription")} +

+ +
+ + +
+ +
+ @@ -225,37 +356,39 @@ export function SystemNetworkSettings() {
-

应用升级

+

{t("updateTitle")}

- 点击检查后会从配置的发布源拉取最新版本信息,有新版本时可直接下载并安装。 + {t("updateDescription")}

-
当前版本
+
{t("currentVersion")}
{currentVersion ? `v${currentVersion}` : "-"}
-
可升级版本
+
+ {t("upgradableVersion")} +
- {availableUpdate ? `v${availableUpdate.version}` : "暂无"} + {availableUpdate ? `v${availableUpdate.version}` : t("none")}
- {lastCheckedAt && ( + {formattedLastCheckedAt && (

- 上次检查:{lastCheckedAt.toLocaleString()} + {t("lastChecked", { time: formattedLastCheckedAt })}

)} {updateError && (
- 更新异常:{updateError} + {t("updateError", { message: updateError })}
)} @@ -269,12 +402,12 @@ export function SystemNetworkSettings() { {checkingUpdate ? ( <> - 检查中... + {t("checking")} ) : ( <> - 检查更新 + {t("checkUpdate")} )} @@ -288,12 +421,12 @@ export function SystemNetworkSettings() { {installingUpdate ? ( <> - 升级中... + {t("updating")} ) : ( <> - 升级到 v{availableUpdate.version} + {t("upgradeTo", { version: availableUpdate.version })} )} diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json new file mode 100644 index 0000000..d1e30fe --- /dev/null +++ b/src/i18n/messages/en.json @@ -0,0 +1,75 @@ +{ + "Language": { + "followSystem": "Follow System", + "english": "English", + "simplifiedChinese": "Simplified Chinese", + "traditionalChinese": "Traditional Chinese" + }, + "SettingsShell": { + "title": "Settings", + "preferences": "Preferences", + "nav": { + "appearance": "Appearance", + "agents": "Agents", + "mcp": "MCP", + "skills": "Skills", + "shortcuts": "Shortcuts", + "system": "System" + } + }, + "AppearanceSettings": { + "sectionTitle": "Theme Appearance", + "sectionDescription": "Choose light, dark, or follow system. Settings are saved automatically.", + "themeMode": "Theme mode", + "placeholder": "Select theme mode", + "system": "Follow system", + "light": "Light", + "dark": "Dark", + "currentTheme": "Current effective theme: {theme}", + "resolvedTheme": { + "light": "Light", + "dark": "Dark", + "unknown": "--" + } + }, + "SystemSettings": { + "loading": "Loading...", + "sectionTitle": "System Management", + "sectionDescription": "Manage network proxy, app updates and language preferences.", + "proxyTitle": "Network Proxy", + "proxyDescription": "When enabled, subsequent network requests prefer this proxy (including ACP chat, agent installation and Git remote operations).", + "loadFailed": "Load failed: {message}", + "enableProxy": "Enable system proxy", + "proxyAddress": "Proxy address", + "proxyHint": "Supports http(s)/socks5, example: {example}. Only effective when system proxy is enabled.", + "save": "Save", + "saving": "Saving...", + "proxyRequired": "Proxy URL is required when proxy is enabled", + "saveSuccess": "System proxy settings saved", + "saveFailed": "Save failed: {message}", + "languageTitle": "Language", + "languageDescription": "Set app language. When following system locale, unsupported languages fall back to English.", + "appLanguage": "App language", + "languageSaveSuccess": "Language settings saved", + "languageSaveFailed": "Failed to save language settings: {message}", + "updateTitle": "App Update", + "updateDescription": "Check the configured release source for newer versions and install directly when available.", + "currentVersion": "Current version", + "upgradableVersion": "Available version", + "none": "None", + "lastChecked": "Last checked: {time}", + "updateError": "Update error: {message}", + "checking": "Checking...", + "checkUpdate": "Check for updates", + "updating": "Installing...", + "upgradeTo": "Upgrade to v{version}", + "foundUpdate": "New version v{version} found", + "alreadyLatest": "You're on the latest version", + "checkUpdateFailed": "Failed to check for updates: {message}", + "installSuccess": "Update installed. Relaunching app.", + "installFailed": "Update failed: {message}" + }, + "SettingsPages": { + "agentsLoading": "Loading agent settings..." + } +} diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json new file mode 100644 index 0000000..b5f6b99 --- /dev/null +++ b/src/i18n/messages/zh-CN.json @@ -0,0 +1,75 @@ +{ + "Language": { + "followSystem": "跟随系统", + "english": "English", + "simplifiedChinese": "简体中文", + "traditionalChinese": "繁體中文" + }, + "SettingsShell": { + "title": "设置", + "preferences": "偏好设置", + "nav": { + "appearance": "外观", + "agents": "Agents", + "mcp": "MCP", + "skills": "Skills", + "shortcuts": "快捷键", + "system": "系统" + } + }, + "AppearanceSettings": { + "sectionTitle": "主题外观", + "sectionDescription": "选择浅色、深色或跟随系统主题,设置会自动保存。", + "themeMode": "主题模式", + "placeholder": "请选择主题模式", + "system": "跟随系统", + "light": "浅色", + "dark": "深色", + "currentTheme": "当前生效主题:{theme}", + "resolvedTheme": { + "light": "浅色", + "dark": "深色", + "unknown": "--" + } + }, + "SystemSettings": { + "loading": "加载中...", + "sectionTitle": "系统管理", + "sectionDescription": "管理网络代理、应用升级与语言偏好。", + "proxyTitle": "网络代理", + "proxyDescription": "开启后,后续网络请求将优先走该代理(包括 ACP 对话、Agent 安装、Git 远程操作等)。", + "loadFailed": "加载失败:{message}", + "enableProxy": "启用系统代理", + "proxyAddress": "代理地址", + "proxyHint": "支持 http(s)/socks5,示例:{example}。仅在启用系统代理时生效。", + "save": "保存", + "saving": "保存中...", + "proxyRequired": "启用代理时必须填写代理地址", + "saveSuccess": "系统代理设置已保存", + "saveFailed": "保存失败:{message}", + "languageTitle": "语言", + "languageDescription": "设置应用语言。跟随系统时,若系统语言不受支持将回退为英文。", + "appLanguage": "应用语言", + "languageSaveSuccess": "语言设置已保存", + "languageSaveFailed": "语言设置保存失败:{message}", + "updateTitle": "应用升级", + "updateDescription": "点击检查后会从配置的发布源拉取最新版本信息,有新版本时可直接下载并安装。", + "currentVersion": "当前版本", + "upgradableVersion": "可升级版本", + "none": "暂无", + "lastChecked": "上次检查:{time}", + "updateError": "更新异常:{message}", + "checking": "检查中...", + "checkUpdate": "检查更新", + "updating": "升级中...", + "upgradeTo": "升级到 v{version}", + "foundUpdate": "发现新版本 v{version}", + "alreadyLatest": "当前已经是最新版本", + "checkUpdateFailed": "检查更新失败:{message}", + "installSuccess": "升级包已安装,正在重启应用", + "installFailed": "升级失败:{message}" + }, + "SettingsPages": { + "agentsLoading": "加载 Agent 设置中..." + } +} diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json new file mode 100644 index 0000000..44992b5 --- /dev/null +++ b/src/i18n/messages/zh-TW.json @@ -0,0 +1,75 @@ +{ + "Language": { + "followSystem": "跟隨系統", + "english": "English", + "simplifiedChinese": "简体中文", + "traditionalChinese": "繁體中文" + }, + "SettingsShell": { + "title": "設定", + "preferences": "偏好設定", + "nav": { + "appearance": "外觀", + "agents": "Agents", + "mcp": "MCP", + "skills": "Skills", + "shortcuts": "快捷鍵", + "system": "系統" + } + }, + "AppearanceSettings": { + "sectionTitle": "主題外觀", + "sectionDescription": "選擇淺色、深色或跟隨系統主題,設定會自動儲存。", + "themeMode": "主題模式", + "placeholder": "請選擇主題模式", + "system": "跟隨系統", + "light": "淺色", + "dark": "深色", + "currentTheme": "目前生效主題:{theme}", + "resolvedTheme": { + "light": "淺色", + "dark": "深色", + "unknown": "--" + } + }, + "SystemSettings": { + "loading": "載入中...", + "sectionTitle": "系統管理", + "sectionDescription": "管理網路代理、應用升級與語言偏好。", + "proxyTitle": "網路代理", + "proxyDescription": "啟用後,後續網路請求將優先走該代理(包含 ACP 對話、Agent 安裝、Git 遠端操作等)。", + "loadFailed": "載入失敗:{message}", + "enableProxy": "啟用系統代理", + "proxyAddress": "代理位址", + "proxyHint": "支援 http(s)/socks5,範例:{example}。僅在啟用系統代理時生效。", + "save": "儲存", + "saving": "儲存中...", + "proxyRequired": "啟用代理時必須填寫代理位址", + "saveSuccess": "系統代理設定已儲存", + "saveFailed": "儲存失敗:{message}", + "languageTitle": "語言", + "languageDescription": "設定應用語言。跟隨系統時,若系統語言不受支援將回退為英文。", + "appLanguage": "應用語言", + "languageSaveSuccess": "語言設定已儲存", + "languageSaveFailed": "語言設定儲存失敗:{message}", + "updateTitle": "應用升級", + "updateDescription": "點擊檢查後會從設定的發佈來源拉取最新版本資訊,有新版本時可直接下載並安裝。", + "currentVersion": "目前版本", + "upgradableVersion": "可升級版本", + "none": "暫無", + "lastChecked": "上次檢查:{time}", + "updateError": "更新異常:{message}", + "checking": "檢查中...", + "checkUpdate": "檢查更新", + "updating": "升級中...", + "upgradeTo": "升級到 v{version}", + "foundUpdate": "發現新版本 v{version}", + "alreadyLatest": "目前已是最新版本", + "checkUpdateFailed": "檢查更新失敗:{message}", + "installSuccess": "升級包已安裝,正在重新啟動應用", + "installFailed": "升級失敗:{message}" + }, + "SettingsPages": { + "agentsLoading": "載入 Agent 設定中..." + } +} diff --git a/src/i18n/request.ts b/src/i18n/request.ts new file mode 100644 index 0000000..431baed --- /dev/null +++ b/src/i18n/request.ts @@ -0,0 +1,7 @@ +import { getRequestConfig } from "next-intl/server" +import enMessages from "@/i18n/messages/en.json" + +export default getRequestConfig(async () => ({ + locale: "en", + messages: enMessages, +})) diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts new file mode 100644 index 0000000..474aa02 --- /dev/null +++ b/src/lib/i18n.ts @@ -0,0 +1,84 @@ +import type { AppLocale, SystemLanguageSettings } from "@/lib/types" + +export const APP_LOCALES: readonly AppLocale[] = ["en", "zh_cn", "zh_tw"] +const FALLBACK_APP_LOCALE: AppLocale = "en" + +export const DEFAULT_LANGUAGE_SETTINGS: SystemLanguageSettings = { + mode: "system", + language: FALLBACK_APP_LOCALE, +} + +export const APP_LOCALE_TO_INTL_LOCALE: Record = { + en: "en", + zh_cn: "zh-CN", + zh_tw: "zh-TW", +} + +export function isAppLocale(value: unknown): value is AppLocale { + return APP_LOCALES.includes(value as AppLocale) +} + +export function normalizeLanguageSettings( + settings: Partial | null | undefined +): SystemLanguageSettings { + const mode = settings?.mode === "manual" ? "manual" : "system" + const language = isAppLocale(settings?.language) + ? settings.language + : FALLBACK_APP_LOCALE + + return { + mode, + language, + } +} + +function mapSystemLocaleToAppLocale(localeTag: string): AppLocale | null { + const normalized = localeTag.trim().toLowerCase().replace(/_/g, "-") + + if (!normalized) return null + if (normalized.startsWith("en")) return "en" + + if ( + normalized.startsWith("zh-hant") || + normalized.endsWith("-tw") || + normalized.endsWith("-hk") || + normalized.endsWith("-mo") + ) { + return "zh_tw" + } + + if (normalized.startsWith("zh")) return "zh_cn" + + return null +} + +export function getSystemLocaleCandidates(): string[] { + if (typeof navigator === "undefined") return [] + + const candidates = [ + ...(navigator.languages ?? []), + navigator.language, + ].filter((value): value is string => Boolean(value)) + + return [...new Set(candidates)] +} + +export function resolveSystemLocale(candidates: string[]): AppLocale | null { + for (const candidate of candidates) { + const resolved = mapSystemLocaleToAppLocale(candidate) + if (resolved) return resolved + } + + return null +} + +export function resolveAppLocale( + settings: SystemLanguageSettings, + systemLocaleCandidates: string[] +): AppLocale { + if (settings.mode === "manual") { + return settings.language + } + + return resolveSystemLocale(systemLocaleCandidates) ?? FALLBACK_APP_LOCALE +} diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index 4351a38..afdc345 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -34,6 +34,7 @@ import type { FileEditContent, FileSaveResult, GitLogEntry, + SystemLanguageSettings, SystemProxySettings, McpAppType, LocalMcpServer, @@ -277,6 +278,16 @@ export async function updateSystemProxySettings( return invoke("update_system_proxy_settings", { settings }) } +export async function getSystemLanguageSettings(): Promise { + return invoke("get_system_language_settings") +} + +export async function updateSystemLanguageSettings( + settings: SystemLanguageSettings +): Promise { + return invoke("update_system_language_settings", { settings }) +} + export async function mcpScanLocal(): Promise { return invoke("mcp_scan_local") } diff --git a/src/lib/types.ts b/src/lib/types.ts index 5265186..4d46711 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -490,6 +490,14 @@ export interface SystemProxySettings { proxy_url: string | null } +export type AppLocale = "en" | "zh_cn" | "zh_tw" +export type LanguageMode = "system" | "manual" + +export interface SystemLanguageSettings { + mode: LanguageMode + language: AppLocale +} + export type McpAppType = "claude_code" | "codex" | "open_code" export interface LocalMcpServer {