初步集成next-intl支持多语言

This commit is contained in:
xintaofei
2026-03-07 10:08:05 +08:00
parent efd87dbd9c
commit 934f689b08
20 changed files with 1186 additions and 89 deletions

View File

@@ -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)

View File

@@ -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",

406
pnpm-lock.yaml generated
View File

@@ -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

View File

@@ -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<SystemProxySettings, String> {
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<SystemLanguageSettings, String> {
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::<SystemLanguageSettings>(&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<SystemLanguageSettings, String> {
load_system_language_settings(&db.conn).await
}
#[tauri::command]
pub async fn update_system_language_settings(
settings: SystemLanguageSettings,
db: State<'_, AppDatabase>,
) -> Result<SystemLanguageSettings, String> {
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)
}

View File

@@ -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,

View File

@@ -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};

View File

@@ -5,3 +5,27 @@ pub struct SystemProxySettings {
pub enabled: bool,
pub proxy_url: Option<String>,
}
#[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,
}

View File

@@ -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 (
<html lang="en" className={jetbrainsMono.variable} suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
<NextIntlClientProvider locale="en" messages={enMessages}>
<AppI18nProvider>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</AppI18nProvider>
</NextIntlClientProvider>
</body>
</html>
)

View File

@@ -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 (
<Suspense
fallback={
<div className="h-full flex items-center justify-center text-sm text-muted-foreground">
Agent ...
{t("agentsLoading")}
</div>
}
>

View File

@@ -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<AppLocale, AbstractIntlMessages> = {
en: enMessages,
zh_cn: zhCNMessages,
zh_tw: zhTWMessages,
}
const AppI18nContext = createContext<AppI18nContextValue | null>(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<SystemLanguageSettings>(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<AppI18nContextValue>(
() => ({
appLocale,
languageSettings,
languageSettingsLoaded,
setLanguageSettings,
}),
[appLocale, languageSettings, languageSettingsLoaded, setLanguageSettings]
)
return (
<AppI18nContext.Provider value={contextValue}>
<NextIntlClientProvider
locale={intlLocale}
messages={MESSAGES_BY_LOCALE[appLocale]}
>
{children}
</NextIntlClientProvider>
</AppI18nContext.Provider>
)
}

View File

@@ -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 (
<div className="h-full overflow-auto">
@@ -27,41 +29,41 @@ export function AppearanceSettings() {
<section className="rounded-xl border bg-card p-4 space-y-4">
<div className="flex items-center gap-2">
<Sun className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold"></h2>
<h2 className="text-sm font-semibold">{t("sectionTitle")}</h2>
</div>
<p className="text-xs text-muted-foreground leading-5">
{t("sectionDescription")}
</p>
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">
{t("themeMode")}
</label>
<Select
value={theme ?? "system"}
onValueChange={(value) => setTheme(value as ThemeMode)}
>
<SelectTrigger className="w-56">
<SelectValue placeholder="请选择主题模式" />
<SelectValue placeholder={t("placeholder")} />
</SelectTrigger>
<SelectContent align="start">
<SelectItem value="system">
<span className="inline-flex items-center gap-2">
<Monitor className="h-3.5 w-3.5" />
{t("system")}
</span>
</SelectItem>
<SelectItem value="light">
<span className="inline-flex items-center gap-2">
<Sun className="h-3.5 w-3.5" />
{t("light")}
</span>
</SelectItem>
<SelectItem value="dark">
<span className="inline-flex items-center gap-2">
<Moon className="h-3.5 w-3.5" />
{t("dark")}
</span>
</SelectItem>
</SelectContent>
@@ -70,7 +72,7 @@ export function AppearanceSettings() {
className="text-[11px] text-muted-foreground"
suppressHydrationWarning
>
{resolvedThemeLabel}
{t("currentTheme", { theme: resolvedThemeLabel })}
</p>
</div>
</section>

View File

@@ -9,6 +9,7 @@ import {
PlugZap,
Settings,
} from "lucide-react"
import { useTranslations } from "next-intl"
import { usePathname } from "next/navigation"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
@@ -18,39 +19,39 @@ import { AppTitleBar } from "@/components/layout/app-title-bar"
interface SettingsNavItem {
href: string
label: string
labelKey: string
icon: ComponentType<{ className?: string }>
}
const SETTINGS_NAV_ITEMS: SettingsNavItem[] = [
{
href: "/settings/appearance",
label: "Appearance",
labelKey: "appearance",
icon: Palette,
},
{
href: "/settings/agents",
label: "Agents",
labelKey: "agents",
icon: Bot,
},
{
href: "/settings/mcp",
label: "MCP",
labelKey: "mcp",
icon: PlugZap,
},
{
href: "/settings/skills",
label: "Skills",
labelKey: "skills",
icon: BookOpenText,
},
{
href: "/settings/shortcuts",
label: "Shortcuts",
labelKey: "shortcuts",
icon: Keyboard,
},
{
href: "/settings/system",
label: "System",
labelKey: "system",
icon: Settings,
},
]
@@ -73,6 +74,7 @@ function isWindowsRuntime(): boolean {
}
export function SettingsShell({ children }: SettingsShellProps) {
const t = useTranslations("SettingsShell")
const pathname = usePathname()
const router = useRouter()
const normalizedPathname = normalizePath(pathname)
@@ -101,14 +103,14 @@ export function SettingsShell({ children }: SettingsShellProps) {
<div className="h-screen flex flex-col overflow-hidden bg-background text-foreground">
<AppTitleBar
center={
<div className="text-sm font-bold tracking-tight">Settings</div>
<div className="text-sm font-bold tracking-tight">{t("title")}</div>
}
/>
<div className="flex-1 min-h-0 flex">
<aside className="w-56 shrink-0 border-r p-3">
<div className="px-1 pb-2 text-[11px] font-medium text-muted-foreground">
Preferences
{t("preferences")}
</div>
<nav className="space-y-1">
{SETTINGS_NAV_ITEMS.map((item) => {
@@ -128,7 +130,7 @@ export function SettingsShell({ children }: SettingsShellProps) {
>
<span className="inline-flex items-center gap-1">
<Icon className="h-3.5 w-3.5" />
{item.label}
{t(`nav.${item.labelKey}`)}
</span>
</Button>
)

View File

@@ -1,12 +1,33 @@
"use client"
import { useCallback, useEffect, useState } from "react"
import { ArrowUpCircle, Loader2, RefreshCw, Save, Wifi } from "lucide-react"
import { useCallback, useEffect, useMemo, useState } from "react"
import {
ArrowUpCircle,
Languages,
Loader2,
RefreshCw,
Save,
Wifi,
} from "lucide-react"
import type { Update } from "@tauri-apps/plugin-updater"
import { useLocale, useTranslations } from "next-intl"
import { toast } from "sonner"
import { useAppI18n } from "@/components/i18n-provider"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { getSystemProxySettings, updateSystemProxySettings } from "@/lib/tauri"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
getSystemProxySettings,
updateSystemLanguageSettings,
updateSystemProxySettings,
} from "@/lib/tauri"
import type { AppLocale } from "@/lib/types"
import {
checkAppUpdate,
closeAppUpdate,
@@ -16,10 +37,24 @@ import {
} from "@/lib/updater"
const PROXY_EXAMPLE = "http://127.0.0.1:7890"
const APP_LANGUAGE_VALUES = ["en", "zh_cn", "zh_tw"] as const
type LanguageSelectValue = "system" | AppLocale
function isAppLocale(value: string): value is AppLocale {
return APP_LANGUAGE_VALUES.includes(value as AppLocale)
}
export function SystemNetworkSettings() {
const t = useTranslations("SystemSettings")
const tLanguage = useTranslations("Language")
const locale = useLocale()
const { languageSettings, languageSettingsLoaded, setLanguageSettings } =
useAppI18n()
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [savingLanguage, setSavingLanguage] = useState(false)
const [enabled, setEnabled] = useState(false)
const [proxyUrl, setProxyUrl] = useState("")
const [loadError, setLoadError] = useState<string | null>(null)
@@ -30,39 +65,60 @@ export function SystemNetworkSettings() {
const [updateError, setUpdateError] = useState<string | null>(null)
const [lastCheckedAt, setLastCheckedAt] = useState<Date | null>(null)
const [appLanguage, setAppLanguage] = useState<LanguageSelectValue>(
languageSettings.mode === "system" ? "system" : languageSettings.language
)
useEffect(() => {
setAppLanguage(
languageSettings.mode === "system" ? "system" : languageSettings.language
)
}, [languageSettings])
const languageLabels = useMemo(
() => ({
en: tLanguage("english"),
zh_cn: tLanguage("simplifiedChinese"),
zh_tw: tLanguage("traditionalChinese"),
}),
[tLanguage]
)
const formattedLastCheckedAt = useMemo(() => {
if (!lastCheckedAt) return null
return new Intl.DateTimeFormat(locale, {
dateStyle: "medium",
timeStyle: "short",
}).format(lastCheckedAt)
}, [lastCheckedAt, locale])
const loadSettings = useCallback(async () => {
setLoading(true)
setLoadError(null)
try {
const settings = await getSystemProxySettings()
setEnabled(settings.enabled)
setProxyUrl(settings.proxy_url ?? "")
const [proxySettings, version] = await Promise.all([
getSystemProxySettings(),
getCurrentAppVersion(),
])
setEnabled(proxySettings.enabled)
setProxyUrl(proxySettings.proxy_url ?? "")
setCurrentVersion(version)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
setLoadError(message)
console.error("[Settings] load system settings failed:", err)
} finally {
setLoading(false)
}
}, [])
const loadAppVersion = useCallback(async () => {
try {
const version = await getCurrentAppVersion()
setCurrentVersion(version)
} catch (err) {
console.error("[Settings] load app version failed:", err)
}
}, [])
useEffect(() => {
loadSettings().catch((err) => {
console.error("[Settings] load system proxy settings failed:", err)
console.error("[Settings] load system settings failed:", err)
})
loadAppVersion().catch((err) => {
console.error("[Settings] load app version failed:", err)
})
}, [loadSettings, loadAppVersion])
}, [loadSettings])
useEffect(() => {
return () => {
@@ -75,7 +131,7 @@ export function SystemNetworkSettings() {
const saveSettings = useCallback(async () => {
if (enabled && !proxyUrl.trim()) {
toast.error("启用代理时必须填写代理地址")
toast.error(t("proxyRequired"))
return
}
@@ -87,14 +143,34 @@ export function SystemNetworkSettings() {
})
setEnabled(next.enabled)
setProxyUrl(next.proxy_url ?? "")
toast.success("系统代理设置已保存")
toast.success(t("saveSuccess"))
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(`保存失败:${message}`)
toast.error(t("saveFailed", { message }))
} finally {
setSaving(false)
}
}, [enabled, proxyUrl])
}, [enabled, proxyUrl, t])
const saveLanguage = useCallback(async () => {
setSavingLanguage(true)
try {
const next = await updateSystemLanguageSettings({
mode: appLanguage === "system" ? "system" : "manual",
language:
appLanguage === "system" ? languageSettings.language : appLanguage,
})
setLanguageSettings(next)
toast.success(t("languageSaveSuccess"))
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("languageSaveFailed", { message }))
} finally {
setSavingLanguage(false)
}
}, [appLanguage, languageSettings.language, setLanguageSettings, t])
const checkForUpdates = useCallback(async () => {
setCheckingUpdate(true)
@@ -108,10 +184,10 @@ export function SystemNetworkSettings() {
if (result.update) {
setAvailableUpdate(result.update)
toast.success(`发现新版本 v${result.update.version}`)
toast.success(t("foundUpdate", { version: result.update.version }))
} else {
setAvailableUpdate(null)
toast.success("当前已经是最新版本")
toast.success(t("alreadyLatest"))
}
if (previousUpdate && previousUpdate !== result.update) {
@@ -120,11 +196,11 @@ export function SystemNetworkSettings() {
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
setUpdateError(message)
toast.error(`检查更新失败:${message}`)
toast.error(t("checkUpdateFailed", { message }))
} finally {
setCheckingUpdate(false)
}
}, [availableUpdate])
}, [availableUpdate, t])
const installUpdate = useCallback(async () => {
if (!availableUpdate) return
@@ -134,22 +210,22 @@ export function SystemNetworkSettings() {
try {
await installAppUpdate(availableUpdate)
toast.success("升级包已安装,正在重启应用")
toast.success(t("installSuccess"))
await relaunchApp()
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
setUpdateError(message)
toast.error(`升级失败:${message}`)
toast.error(t("installFailed", { message }))
} finally {
setInstallingUpdate(false)
}
}, [availableUpdate])
}, [availableUpdate, t])
if (loading) {
return (
<div className="h-full flex items-center justify-center text-sm text-muted-foreground gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
...
{t("loading")}
</div>
)
}
@@ -158,26 +234,25 @@ export function SystemNetworkSettings() {
<div className="h-full overflow-auto">
<div className="w-full space-y-4">
<section className="space-y-1">
<h1 className="text-sm font-semibold"></h1>
<h1 className="text-sm font-semibold">{t("sectionTitle")}</h1>
<p className="text-xs text-muted-foreground">
{t("sectionDescription")}
</p>
</section>
<section className="rounded-xl border bg-card p-4 space-y-4">
<div className="flex items-center gap-2">
<Wifi className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold"></h2>
<h2 className="text-sm font-semibold">{t("proxyTitle")}</h2>
</div>
<p className="text-xs text-muted-foreground leading-5">
ACP Agent Git
{t("proxyDescription")}
</p>
{loadError && (
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-3 py-2 text-xs text-red-400">
{loadError}
{t("loadFailed", { message: loadError })}
</div>
)}
@@ -187,12 +262,12 @@ export function SystemNetworkSettings() {
checked={enabled}
onChange={(event) => setEnabled(event.target.checked)}
/>
{t("enableProxy")}
</label>
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">
{t("proxyAddress")}
</label>
<Input
value={proxyUrl}
@@ -200,8 +275,7 @@ export function SystemNetworkSettings() {
placeholder={PROXY_EXAMPLE}
/>
<p className="text-[11px] text-muted-foreground">
http(s)/socks5{PROXY_EXAMPLE}
{t("proxyHint", { example: PROXY_EXAMPLE })}
</p>
</div>
@@ -210,12 +284,69 @@ export function SystemNetworkSettings() {
{saving ? (
<>
<Loader2 className="h-3.5 w-3.5 animate-spin" />
...
{t("saving")}
</>
) : (
<>
<Save className="h-3.5 w-3.5" />
{t("save")}
</>
)}
</Button>
</div>
</section>
<section className="rounded-xl border bg-card p-4 space-y-4">
<div className="flex items-center gap-2">
<Languages className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">{t("languageTitle")}</h2>
</div>
<p className="text-xs text-muted-foreground leading-5">
{t("languageDescription")}
</p>
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">
{t("appLanguage")}
</label>
<Select
value={appLanguage}
onValueChange={(value) => {
if (value === "system") {
setAppLanguage("system")
return
}
if (!isAppLocale(value)) return
setAppLanguage(value)
}}
disabled={savingLanguage || !languageSettingsLoaded}
>
<SelectTrigger className="w-full sm:w-56">
<SelectValue />
</SelectTrigger>
<SelectContent align="start">
<SelectItem value="system">
{tLanguage("followSystem")}
</SelectItem>
<SelectItem value="en">{languageLabels.en}</SelectItem>
<SelectItem value="zh_cn">{languageLabels.zh_cn}</SelectItem>
<SelectItem value="zh_tw">{languageLabels.zh_tw}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex justify-end">
<Button size="sm" onClick={saveLanguage} disabled={savingLanguage}>
{savingLanguage ? (
<>
<Loader2 className="h-3.5 w-3.5 animate-spin" />
{t("saving")}
</>
) : (
<>
<Save className="h-3.5 w-3.5" />
{t("save")}
</>
)}
</Button>
@@ -225,37 +356,39 @@ export function SystemNetworkSettings() {
<section className="rounded-xl border bg-card p-4 space-y-4">
<div className="flex items-center gap-2">
<RefreshCw className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold"></h2>
<h2 className="text-sm font-semibold">{t("updateTitle")}</h2>
</div>
<p className="text-xs text-muted-foreground leading-5">
{t("updateDescription")}
</p>
<div className="grid grid-cols-1 gap-2 text-xs sm:grid-cols-2">
<div className="rounded-md border bg-muted/20 px-3 py-2">
<div className="text-muted-foreground"></div>
<div className="text-muted-foreground">{t("currentVersion")}</div>
<div className="mt-1 font-medium">
{currentVersion ? `v${currentVersion}` : "-"}
</div>
</div>
<div className="rounded-md border bg-muted/20 px-3 py-2">
<div className="text-muted-foreground"></div>
<div className="text-muted-foreground">
{t("upgradableVersion")}
</div>
<div className="mt-1 font-medium">
{availableUpdate ? `v${availableUpdate.version}` : "暂无"}
{availableUpdate ? `v${availableUpdate.version}` : t("none")}
</div>
</div>
</div>
{lastCheckedAt && (
{formattedLastCheckedAt && (
<p className="text-[11px] text-muted-foreground">
{lastCheckedAt.toLocaleString()}
{t("lastChecked", { time: formattedLastCheckedAt })}
</p>
)}
{updateError && (
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-3 py-2 text-xs text-red-400">
{updateError}
{t("updateError", { message: updateError })}
</div>
)}
@@ -269,12 +402,12 @@ export function SystemNetworkSettings() {
{checkingUpdate ? (
<>
<Loader2 className="h-3.5 w-3.5 animate-spin" />
...
{t("checking")}
</>
) : (
<>
<RefreshCw className="h-3.5 w-3.5" />
{t("checkUpdate")}
</>
)}
</Button>
@@ -288,12 +421,12 @@ export function SystemNetworkSettings() {
{installingUpdate ? (
<>
<Loader2 className="h-3.5 w-3.5 animate-spin" />
...
{t("updating")}
</>
) : (
<>
<ArrowUpCircle className="h-3.5 w-3.5" />
v{availableUpdate.version}
{t("upgradeTo", { version: availableUpdate.version })}
</>
)}
</Button>

75
src/i18n/messages/en.json Normal file
View File

@@ -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..."
}
}

View File

@@ -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 设置中..."
}
}

View File

@@ -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 設定中..."
}
}

7
src/i18n/request.ts Normal file
View File

@@ -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,
}))

84
src/lib/i18n.ts Normal file
View File

@@ -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<AppLocale, string> = {
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<SystemLanguageSettings> | 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
}

View File

@@ -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<SystemLanguageSettings> {
return invoke("get_system_language_settings")
}
export async function updateSystemLanguageSettings(
settings: SystemLanguageSettings
): Promise<SystemLanguageSettings> {
return invoke("update_system_language_settings", { settings })
}
export async function mcpScanLocal(): Promise<LocalMcpServer[]> {
return invoke("mcp_scan_local")
}

View File

@@ -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 {