diff --git a/pkg/embed/locales/en_US/backend.mo b/pkg/embed/locales/en_US/backend.mo
deleted file mode 100644
index 0530ef0d..00000000
Binary files a/pkg/embed/locales/en_US/backend.mo and /dev/null differ
diff --git a/pkg/embed/locales/en_US/cli.mo b/pkg/embed/locales/en_US/cli.mo
deleted file mode 100644
index 677059b3..00000000
--- a/pkg/embed/locales/en_US/cli.mo
+++ /dev/null
@@ -1,3 +0,0 @@
->[«jQh®k'¦[«j[6¦[«jYõ0ƒUêìŠ‰õÐ*'µéíO*^µìmþ™ZŠw!j»¶ë_ð*'µéíN¶§±÷«w(v)àñ¸­-©à¹¨zu\g§z¶­¢¼`¢×±µp«£bœúè�ç-­«ijw¥\*èÁØ§>º#yËH¾üë�
-º0v)Ëjx.jž�p«£bœX¥{ùšŠé’Þ™·�þZjW¬ýÉb¦‹W
-º0v)ÅŠWˆÝ¶>º#yËHuW«²*'­«ijw¥-©à¹¨Mæ¦x%ŠÈO9¯ŠÈ¨œ6­{m6çN5×mw
\ No newline at end of file
diff --git a/pkg/embed/locales/en_US/web.mo b/pkg/embed/locales/en_US/web.mo
deleted file mode 100644
index a601acd0..00000000
--- a/pkg/embed/locales/en_US/web.mo
+++ /dev/null
@@ -1,3 +0,0 @@
->[«jQh®k'¦[«j[6¦[«jYõ0ƒUêìŠ‰õÐ*'µéíO*^µìmþ™ZŠw!j»¶ë_ð*'µéíN¶§±÷«w(v)àñ¸­-©à¹¨zu\g§z¶­¢¼`¢×±µp«£bœúè�ç-­«ijw¥\*èÁØ§>º#yËH¾üë�
-º0v)Ëjx.jž�p«£bœX¥{ùšŠé’Þ™·�þZjW¬ÿ›¦‹W
-º0v)ÅŠWˆÝ¸>º#yËHuW«²*'­«ijw¥-©à¹¨Mæ¦x%ŠÈO9¯ŠÈ¨œ6­{m6çN5×mw
\ No newline at end of file
diff --git a/pkg/embed/locales/zh_CN/cli.mo b/pkg/embed/locales/zh_CN/cli.mo
deleted file mode 100644
index ba0da4f5..00000000
--- a/pkg/embed/locales/zh_CN/cli.mo
+++ /dev/null
@@ -1,4 +0,0 @@
->[«jQh®k'¦[«j[5¦[«j] Áz»"¢}t
-‰íz{SÊ—­{¦V¢�ÈZ®Ç­º×ü
-‰íz{S­©ì}êÄ�ÊŠx<n+Kjx.j³„#WéÞ­«h¯(µìm\*èÁØ§>º#yËkjÚZ�éW
-º0v)Ï®ˆÞrÒï¿:ãEÂ®ŒŠrÚžš�ìáÕÂ®ŒŠqb•ïæj)ÿ¦H?zfÞwùhq©^³÷%Šš-\*èÁØ§)^ ?vØúè�ç-!Õ^®È¨ž¶­¥©Þ”¶§‚æ y7š˜(b�ëJ)©–'âyÓÎEëâ²*'
«^ÛM¹Ó�uÛ]ý
\ No newline at end of file
diff --git a/pkg/embed/locales/zh_CN/web.mo b/pkg/embed/locales/zh_CN/web.mo
deleted file mode 100644
index b22539c0..00000000
--- a/pkg/embed/locales/zh_CN/web.mo
+++ /dev/null
@@ -1,4 +0,0 @@
->[«jQh®k'¦[«j[5¦[«j] Áz»"¢}t
-‰íz{SÊ—­{¦V¢�ÈZ®Ç­º×ü
-‰íz{S­©ì}êÄ�ÊŠx<n+Kjx.j³„#WéÞ­«h¯(µìm\*èÁØ§>º#yËkjÚZ�éW
-º0v)Ï®ˆÞrÒï¿:ãEÂ®ŒŠrÚžš�ìáÕÂ®ŒŠqb•ïæj)ÿ¦H?zfÞwùhq©^³ünš-\*èÁØ§)^ ?vàúè�ç-!Õ^®È¨ž¶­¥©Þ”¶§‚æ y7š˜(b�ëJ)©–'âyÓÎEëâ²*'
«^ÛM¹Ó�uÛ]ý
\ No newline at end of file
diff --git a/pkg/embed/locales/zh_TW/cli.mo b/pkg/embed/locales/zh_TW/cli.mo
deleted file mode 100644
index e3d82147..00000000
--- a/pkg/embed/locales/zh_TW/cli.mo
+++ /dev/null
@@ -1,4 +0,0 @@
->[«jQh®k'¦[«j[5¦[«j] Áz»"¢}t
-‰íz{SÊ—­{¦V¢�ÈZ®Ç­º×ü
-‰íz{S­©ì}êÄ�ÊŠx<n+Kjx.j³…5—éÞ­«h¯(µìm\*èÁØ§>º#yËkjÚZ�éW
-º0v)Ï®ˆÞrÒï¿:ãEÂ®ŒŠrÚžš�ìáMeÂ®ŒŠqb•ïæj)ÿ¦H?zfÞwùhq©^³÷%Šš-\*èÁØ§)^ ?vØúè�ç-!Õ^®È¨ž¶­¥©Þ”¶§‚æ y7š˜(b�ëN¶�ŠØ¨�©O9¯ŠÈ¨œ6­{m6çN5×mw
\ No newline at end of file
diff --git a/pkg/embed/locales/zh_TW/web.mo b/pkg/embed/locales/zh_TW/web.mo
deleted file mode 100644
index 3f2fd092..00000000
--- a/pkg/embed/locales/zh_TW/web.mo
+++ /dev/null
@@ -1,4 +0,0 @@
->[«jQh®k'¦[«j[5¦[«j] Áz»"¢}t
-‰íz{SÊ—­{¦V¢�ÈZ®Ç­º×ü
-‰íz{S­©ì}êÄ�ÊŠx<n+Kjx.j³…5—éÞ­«h¯(µìm\*èÁØ§>º#yËkjÚZ�éW
-º0v)Ï®ˆÞrÒï¿:ãEÂ®ŒŠrÚžš�ìáMeÂ®ŒŠqb•ïæj)ÿ¦H?zfÞwùhq©^³ünš-\*èÁØ§)^ ?vàúè�ç-!Õ^®È¨ž¶­¥©Þ”¶§‚æ y7š˜(b�ëN¶�ŠØ¨�©O9¯ŠÈ¨œ6­{m6çN5×mw
\ No newline at end of file
diff --git a/web/gettext.config.js b/web/gettext.config.js
new file mode 100644
index 00000000..613d9257
--- /dev/null
+++ b/web/gettext.config.js
@@ -0,0 +1,7 @@
+module.exports = {
+  output: {
+    path: './src/locales',
+    potPath: './frontend.pot',
+    locales: ['en', 'zh_CN', 'zh_TW']
+  }
+}
diff --git a/web/package.json b/web/package.json
index e9c67534..fe9ed94e 100644
--- a/web/package.json
+++ b/web/package.json
@@ -20,7 +20,9 @@
     "lint-only": "eslint . --fix",
     "format": "prettier --write src/",
     "copy": "cpx \"dist/**/*\" \"../pkg/embed/frontend\" -C",
-    "gen-auto-import": "tsx gen-auto-import.ts"
+    "gen-auto-import": "tsx gen-auto-import.ts",
+    "gettext:extract": "vue-gettext-extract",
+    "gettext:compile": "vue-gettext-compile"
   },
   "dependencies": {
     "@alova/adapter-xhr": "^2.1.1",
@@ -52,7 +54,8 @@
     "vue": "^3.5.13",
     "vue-echarts": "^7.0.3",
     "vue-i18n": "^11.0.1",
-    "vue-router": "^4.5.0"
+    "vue-router": "^4.5.0",
+    "vue3-gettext": "4.0.0-alpha.8"
   },
   "devDependencies": {
     "@iconify/json": "^2.2.290",
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index d691eaa6..1efdfe9c 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -98,6 +98,9 @@ importers:
       vue-router:
         specifier: ^4.5.0
         version: 4.5.0(vue@3.5.13(typescript@5.8.3))
+      vue3-gettext:
+        specifier: 4.0.0-alpha.8
+        version: 4.0.0-alpha.8(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.8.3))
     devDependencies:
       '@iconify/json':
         specifier: ^2.2.290
@@ -1434,6 +1437,10 @@ packages:
   argparse@2.0.1:
     resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
 
+  array-back@6.2.2:
+    resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==}
+    engines: {node: '>=12.17'}
+
   array-buffer-byte-length@1.0.2:
     resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
     engines: {node: '>= 0.4'}
@@ -1538,6 +1545,10 @@ packages:
     resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
     engines: {node: '>=10'}
 
+  chalk@5.4.1:
+    resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
+    engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+
   chokidar@3.6.0:
     resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
     engines: {node: '>= 8.10.0'}
@@ -1569,6 +1580,15 @@ packages:
   colorette@2.0.20:
     resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
 
+  command-line-args@6.0.1:
+    resolution: {integrity: sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==}
+    engines: {node: '>=12.20'}
+    peerDependencies:
+      '@75lb/nature': latest
+    peerDependenciesMeta:
+      '@75lb/nature':
+        optional: true
+
   commander@13.1.0:
     resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
     engines: {node: '>=18'}
@@ -1985,6 +2005,15 @@ packages:
   find-index@0.1.1:
     resolution: {integrity: sha512-uJ5vWrfBKMcE6y2Z8834dwEZj9mNGxYa3t3I53OwFeuZ8D9oc2E5zcsrkuhX6h4iYrjhiv0T3szQmxlAV9uxDg==}
 
+  find-replace@5.0.2:
+    resolution: {integrity: sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==}
+    engines: {node: '>=14'}
+    peerDependencies:
+      '@75lb/nature': latest
+    peerDependenciesMeta:
+      '@75lb/nature':
+        optional: true
+
   find-up@5.0.0:
     resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
     engines: {node: '>=10'}
@@ -2399,6 +2428,10 @@ packages:
     resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
     engines: {node: '>= 0.8.0'}
 
+  lilconfig@3.1.3:
+    resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
+    engines: {node: '>=14'}
+
   linkify-it@5.0.0:
     resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
 
@@ -2417,6 +2450,9 @@ packages:
   lodash-es@4.17.21:
     resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
 
+  lodash.camelcase@4.3.0:
+    resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
+
   lodash.merge@4.6.2:
     resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
 
@@ -2763,6 +2799,9 @@ packages:
   pkg-types@2.1.0:
     resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==}
 
+  pofile@1.1.4:
+    resolution: {integrity: sha512-r6Q21sKsY1AjTVVjOuU02VYKVNQGJNQHjTIvs4dEbeuuYfxgYk/DGD2mqqq4RDaVkwdSq0VEtmQUOPe/wH8X3g==}
+
   possible-typed-array-names@1.1.0:
     resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
     engines: {node: '>= 0.4'}
@@ -3143,6 +3182,10 @@ packages:
     engines: {node: '>=14.17'}
     hasBin: true
 
+  typical@7.3.0:
+    resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==}
+    engines: {node: '>=12.17'}
+
   uc.micro@2.1.0:
     resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
 
@@ -3424,6 +3467,14 @@ packages:
     peerDependencies:
       typescript: '>=5.0.0'
 
+  vue3-gettext@4.0.0-alpha.8:
+    resolution: {integrity: sha512-kz8hdgg0LuweeAUDFFH+L9GghfJ46CkmbyVY86xOm/eYi9/pUi5lWtpzDH25B80peUeoNF/uQdcli0wWaajS8g==}
+    engines: {node: '>= 20.0.0'}
+    hasBin: true
+    peerDependencies:
+      '@vue/compiler-sfc': '>=3.0.0'
+      vue: '>=3.0.0'
+
   vue@3.5.13:
     resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
     peerDependencies:
@@ -5009,6 +5060,8 @@ snapshots:
 
   argparse@2.0.1: {}
 
+  array-back@6.2.2: {}
+
   array-buffer-byte-length@1.0.2:
     dependencies:
       call-bound: 1.0.4
@@ -5125,6 +5178,8 @@ snapshots:
       ansi-styles: 4.3.0
       supports-color: 7.2.0
 
+  chalk@5.4.1: {}
+
   chokidar@3.6.0:
     dependencies:
       anymatch: 3.1.3
@@ -5169,6 +5224,13 @@ snapshots:
 
   colorette@2.0.20: {}
 
+  command-line-args@6.0.1:
+    dependencies:
+      array-back: 6.2.2
+      find-replace: 5.0.2
+      lodash.camelcase: 4.3.0
+      typical: 7.3.0
+
   commander@13.1.0: {}
 
   commander@2.20.3: {}
@@ -5673,6 +5735,8 @@ snapshots:
 
   find-index@0.1.1: {}
 
+  find-replace@5.0.2: {}
+
   find-up@5.0.0:
     dependencies:
       locate-path: 6.0.0
@@ -6075,6 +6139,8 @@ snapshots:
       prelude-ls: 1.2.1
       type-check: 0.4.0
 
+  lilconfig@3.1.3: {}
+
   linkify-it@5.0.0:
     dependencies:
       uc.micro: 2.1.0
@@ -6093,6 +6159,8 @@ snapshots:
 
   lodash-es@4.17.21: {}
 
+  lodash.camelcase@4.3.0: {}
+
   lodash.merge@4.6.2: {}
 
   lodash@4.17.21: {}
@@ -6442,6 +6510,8 @@ snapshots:
       exsolve: 1.0.4
       pathe: 2.0.3
 
+  pofile@1.1.4: {}
+
   possible-typed-array-names@1.1.0: {}
 
   postcss-selector-parser@6.1.2:
@@ -6881,6 +6951,8 @@ snapshots:
 
   typescript@5.8.3: {}
 
+  typical@7.3.0: {}
+
   uc.micro@2.1.0: {}
 
   ufo@1.5.4: {}
@@ -7201,6 +7273,18 @@ snapshots:
       '@vue/language-core': 2.2.8(typescript@5.8.3)
       typescript: 5.8.3
 
+  vue3-gettext@4.0.0-alpha.8(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.8.3)):
+    dependencies:
+      '@vue/compiler-sfc': 3.5.13
+      chalk: 5.4.1
+      command-line-args: 6.0.1
+      glob: 11.0.1
+      lilconfig: 3.1.3
+      pofile: 1.1.4
+      vue: 3.5.13(typescript@5.8.3)
+    transitivePeerDependencies:
+      - '@75lb/nature'
+
   vue@3.5.13(typescript@5.8.3):
     dependencies:
       '@vue/compiler-dom': 3.5.13
diff --git a/web/src/main.ts b/web/src/main.ts
index d96baabc..b4ff1b94 100644
--- a/web/src/main.ts
+++ b/web/src/main.ts
@@ -7,9 +7,9 @@ import { createApp } from 'vue'
 import App from './App.vue'
 
 import { setupI18n } from '@/i18n/i18n'
-import { setupRouter } from './router'
-import { setupStore, useThemeStore } from './store'
-import { setupNaiveDiscreteApi } from './utils'
+import { setupRouter } from '@/router'
+import { setupStore, useThemeStore } from '@/store'
+import { setupGettext, setupNaiveDiscreteApi } from '@/utils'
 
 import { install as VueMonacoEditorPlugin } from '@guolao/vue-monaco-editor'
 
@@ -30,6 +30,7 @@ async function setupApp() {
   await setupStore(app)
   await setupNaiveDiscreteApi()
   await setupPanel().then(() => {
+    setupGettext(app)
     setupI18n(app)
   })
   await setupRouter(app)
diff --git a/web/src/utils/gettext/index.ts b/web/src/utils/gettext/index.ts
new file mode 100644
index 00000000..8eb61e04
--- /dev/null
+++ b/web/src/utils/gettext/index.ts
@@ -0,0 +1,16 @@
+import type { App } from 'vue'
+import { createGettext } from 'vue3-gettext'
+
+let gettext: ReturnType<typeof createGettext>
+
+export function setupGettext(app: App) {
+  gettext = createGettext({
+    availableLanguages: {
+      en: 'English',
+      zh_CN: 'ç®€ä½“ä¸­æ–‡',
+      zh_TW: 'ç¹�é«”ä¸­æ–‡'
+    },
+    defaultLanguage: 'zh_CN'
+  })
+  app.use(gettext)
+}
diff --git a/web/src/utils/index.ts b/web/src/utils/index.ts
index 9ae0b5ee..de8d05d6 100644
--- a/web/src/utils/index.ts
+++ b/web/src/utils/index.ts
@@ -1,5 +1,6 @@
 export * from './auth'
 export * from './common'
 export * from './file'
+export * from './gettext'
 export * from './http'
 export * from './storage'
