diff --git a/loop/plan.md b/loop/plan.md
index 88517f54..dfad8a7c 100644
--- a/loop/plan.md
+++ b/loop/plan.md
@@ -358,7 +358,7 @@
- [x] **A11Y-02** — Add keyboard navigation testing. Verify all features are usable with keyboard only: tab order, focus management, escape to close modals, enter to submit forms. Fix any gaps. **Acceptance**: Complete user journey possible with keyboard only.
-- [ ] **A11Y-03** — Set up i18n infrastructure. Install `vue-i18n`. Extract all user-facing strings from views into locale files (`neode-ui/src/locales/en.json`). Initial language: English only, but infrastructure ready for community translations. **Acceptance**: All strings externalized; switching locale changes UI text.
+- [x] **A11Y-03** — Set up i18n infrastructure. Install `vue-i18n`. Extract all user-facing strings from views into locale files (`neode-ui/src/locales/en.json`). Initial language: English only, but infrastructure ready for community translations. **Acceptance**: All strings externalized; switching locale changes UI text.
### Q2 2028 (June -- August): Penetration Testing, Final QA
diff --git a/neode-ui/package-lock.json b/neode-ui/package-lock.json
index 96c3a52a..cb242df9 100644
--- a/neode-ui/package-lock.json
+++ b/neode-ui/package-lock.json
@@ -13,12 +13,15 @@
"fuse.js": "^7.1.0",
"pinia": "^3.0.4",
"vue": "^3.5.24",
+ "vue-i18n": "^11.3.0",
"vue-router": "^4.6.3"
},
"devDependencies": {
+ "@playwright/test": "^1.58.2",
"@types/node": "^24.10.0",
"@vite-pwa/assets-generator": "^1.0.2",
"@vitejs/plugin-vue": "^6.0.1",
+ "@vitest/coverage-v8": "^3.2.4",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.8.1",
"autoprefixer": "^10.4.22",
@@ -50,6 +53,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/@apideck/better-ajv-errors": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz",
@@ -487,12 +504,12 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
- "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.28.6"
+ "@babel/types": "^7.29.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -1673,9 +1690,9 @@
"license": "MIT"
},
"node_modules/@babel/types": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
- "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@@ -1691,6 +1708,16 @@
"integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==",
"license": "Apache-2.0"
},
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@canvas/image-data": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@canvas/image-data/-/image-data-1.1.0.tgz",
@@ -2697,6 +2724,67 @@
"url": "https://opencollective.com/libvips"
}
},
+ "node_modules/@intlify/core-base": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.3.0.tgz",
+ "integrity": "sha512-NNX5jIwF4TJBe7RtSKDMOA6JD9mp2mRcBHAwt2X+Q8PvnZub0yj5YYXlFu2AcESdgQpEv/5Yx2uOCV/yh7YkZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@intlify/devtools-types": "11.3.0",
+ "@intlify/message-compiler": "11.3.0",
+ "@intlify/shared": "11.3.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ }
+ },
+ "node_modules/@intlify/devtools-types": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.3.0.tgz",
+ "integrity": "sha512-G9CNL4WpANWVdUjubOIIS7/D2j/0j+1KJmhBJxHilWNKr9mmt3IjFV3Hq4JoBP23uOoC5ynxz/FHZ42M+YxfGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@intlify/core-base": "11.3.0",
+ "@intlify/shared": "11.3.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ }
+ },
+ "node_modules/@intlify/message-compiler": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.3.0.tgz",
+ "integrity": "sha512-RAJp3TMsqohg/Wa7bVF3cChRhecSYBLrTCQSj7j0UtWVFLP+6iEJoE2zb7GU5fp+fmG5kCbUdzhmlAUCWXiUJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@intlify/shared": "11.3.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ }
+ },
+ "node_modules/@intlify/shared": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.3.0.tgz",
+ "integrity": "sha512-LC6P/uay7rXL5zZ5+5iRJfLs/iUN8apu9tm8YqQVmW3Uq3X4A0dOFUIDuAmB7gAC29wTHOS3EiN/IosNSz0eNQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ }
+ },
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
@@ -2738,6 +2826,16 @@
"node": ">=12"
}
},
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -2864,6 +2962,22 @@
"node": ">=14"
}
},
+ "node_modules/@playwright/test": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
+ "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.58.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -3495,6 +3609,65 @@
"vue": "^3.2.25"
}
},
+ "node_modules/@vitest/coverage-v8": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz",
+ "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.3.0",
+ "@bcoe/v8-coverage": "^1.0.2",
+ "ast-v8-to-istanbul": "^0.3.3",
+ "debug": "^4.4.1",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-lib-source-maps": "^5.0.6",
+ "istanbul-reports": "^3.1.7",
+ "magic-string": "^0.30.17",
+ "magicast": "^0.3.5",
+ "std-env": "^3.9.0",
+ "test-exclude": "^7.0.1",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "3.2.4",
+ "vitest": "3.2.4"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/coverage-v8/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/coverage-v8/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@vitest/expect": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
@@ -4034,6 +4207,35 @@
"node": ">=12"
}
},
+ "node_modules/ast-v8-to-istanbul": {
+ "version": "0.3.12",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz",
+ "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^10.0.0"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
+ "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@@ -6295,6 +6497,13 @@
"node": ">=18"
}
},
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@@ -6973,6 +7182,98 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
+ "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.23",
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/istanbul-lib-source-maps/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/jackspeak": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
@@ -7365,6 +7666,47 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/magicast": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
+ "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.25.4",
+ "@babel/types": "^7.25.4",
+ "source-map-js": "^1.2.0"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -7892,6 +8234,53 @@
"node": ">= 6"
}
},
+ "node_modules/playwright": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
+ "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.58.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
+ "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -8435,6 +8824,7 @@
"integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -9486,7 +9876,6 @@
"integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
"dev": true,
"license": "BSD-2-Clause",
- "peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@@ -9507,6 +9896,138 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/test-exclude": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz",
+ "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^10.4.1",
+ "minimatch": "^10.2.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/test-exclude/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/test-exclude/node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/test-exclude/node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/test-exclude/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/test-exclude/node_modules/minimatch": {
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/test-exclude/node_modules/minimatch/node_modules/brace-expansion": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
+ "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/test-exclude/node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@@ -10288,6 +10809,7 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@@ -10429,6 +10951,33 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/vue-i18n": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.0.tgz",
+ "integrity": "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@intlify/core-base": "11.3.0",
+ "@intlify/devtools-types": "11.3.0",
+ "@intlify/shared": "11.3.0",
+ "@vue/devtools-api": "^6.5.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ },
+ "peerDependencies": {
+ "vue": "^3.0.0"
+ }
+ },
+ "node_modules/vue-i18n/node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
"node_modules/vue-router": {
"version": "4.6.4",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
diff --git a/neode-ui/package.json b/neode-ui/package.json
index 84a37daa..83557256 100644
--- a/neode-ui/package.json
+++ b/neode-ui/package.json
@@ -28,26 +28,29 @@
"fuse.js": "^7.1.0",
"pinia": "^3.0.4",
"vue": "^3.5.24",
+ "vue-i18n": "^11.3.0",
"vue-router": "^4.6.3"
},
"devDependencies": {
+ "@playwright/test": "^1.58.2",
"@types/node": "^24.10.0",
- "@vue/test-utils": "^2.4.6",
- "jsdom": "^25.0.1",
- "vitest": "^3.1.1",
"@vite-pwa/assets-generator": "^1.0.2",
"@vitejs/plugin-vue": "^6.0.1",
+ "@vitest/coverage-v8": "^3.2.4",
+ "@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.8.1",
"autoprefixer": "^10.4.22",
"concurrently": "^9.1.2",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"express": "^4.21.2",
+ "jsdom": "^25.0.1",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.18",
"typescript": "~5.9.3",
"vite": "^7.2.2",
"vite-plugin-pwa": "^1.2.0",
+ "vitest": "^3.1.1",
"vue-tsc": "^3.1.3",
"ws": "^8.18.0"
}
diff --git a/neode-ui/src/i18n.ts b/neode-ui/src/i18n.ts
new file mode 100644
index 00000000..16dd50b2
--- /dev/null
+++ b/neode-ui/src/i18n.ts
@@ -0,0 +1,15 @@
+import { createI18n } from 'vue-i18n'
+import en from './locales/en.json'
+
+export type MessageSchema = typeof en
+
+const i18n = createI18n<[MessageSchema], 'en'>({
+ legacy: false,
+ locale: 'en',
+ fallbackLocale: 'en',
+ messages: {
+ en,
+ },
+})
+
+export default i18n
diff --git a/neode-ui/src/locales/en.json b/neode-ui/src/locales/en.json
new file mode 100644
index 00000000..8ee9753b
--- /dev/null
+++ b/neode-ui/src/locales/en.json
@@ -0,0 +1,696 @@
+{
+ "common": {
+ "cancel": "Cancel",
+ "save": "Save",
+ "close": "Close",
+ "copy": "Copy",
+ "copied": "Copied",
+ "copiedBang": "Copied!",
+ "loading": "Loading...",
+ "retry": "Retry",
+ "refresh": "Refresh",
+ "install": "Install",
+ "installing": "Installing...",
+ "uninstall": "Uninstall",
+ "start": "Start",
+ "stop": "Stop",
+ "restart": "Restart",
+ "launch": "Launch",
+ "starting": "Starting...",
+ "stopping": "Stopping...",
+ "send": "Send",
+ "sending": "Sending...",
+ "back": "Back",
+ "done": "Done",
+ "manage": "Manage",
+ "connect": "Connect",
+ "connecting": "Connecting...",
+ "disconnect": "Disconnect",
+ "running": "running",
+ "stopped": "stopped",
+ "exited": "exited",
+ "healthy": "Healthy",
+ "elevated": "Elevated",
+ "critical": "Critical",
+ "connected": "Connected",
+ "disconnected": "Disconnected",
+ "active": "Active",
+ "inactive": "Inactive",
+ "synced": "Synced",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "dismiss": "Dismiss",
+ "apply": "Apply",
+ "configure": "Configure",
+ "export": "Export",
+ "delete": "Delete",
+ "remove": "Remove",
+ "error": "Error",
+ "version": "Version",
+ "status": "Status",
+ "category": "Category",
+ "developer": "Developer",
+ "license": "License",
+ "never": "Never",
+ "notAvailable": "Not Available",
+ "goBack": "Go back",
+ "skipToContent": "Skip to main content",
+ "continue": "Continue",
+ "verify": "Verify",
+ "create": "Create",
+ "restore": "Restore",
+ "disabling": "Disabling...",
+ "creating": "Creating...",
+ "restoring": "Restoring...",
+ "manageUpdates": "Manage Updates",
+ "enableAll": "Enable All",
+ "networkDiagnostics": "Network Diagnostics",
+ "network": "Network",
+ "saveConfiguration": "Save Configuration",
+ "sendTest": "Send Test"
+ },
+ "login": {
+ "title": "Welcome to Archipelago",
+ "setupTitle": "Set Up Your Node",
+ "twoFactorTitle": "Two-Factor Authentication",
+ "password": "Password",
+ "confirmPassword": "Confirm Password",
+ "enterPasswordPlaceholder": "Enter your password",
+ "enterPasswordSetup": "Enter a password (min 8 characters)",
+ "confirmPasswordPlaceholder": "Confirm your password",
+ "setupButton": "Set Up Node",
+ "settingUp": "Setting up...",
+ "loginButton": "Login",
+ "loggingIn": "Logging in...",
+ "verifyButton": "Verify",
+ "verifying": "Verifying...",
+ "useAuthCode": "Use authenticator code",
+ "useBackupCode": "Use a backup code instead",
+ "totpInstruction": "Enter the 6-digit code from your authenticator app",
+ "totpPlaceholder": "000000",
+ "backupCodePlaceholder": "XXXX-XXXX",
+ "serverStarting": "Server starting up...",
+ "replayIntro": "Replay Intro",
+ "onboarding": "Onboarding",
+ "resetting": "Resetting...",
+ "recoveryNote": "Password recovery requires SSH access to the server.",
+ "errorMinLength": "Password must be at least 8 characters",
+ "errorMismatch": "Passwords do not match",
+ "errorServerStarting": "Server is starting up. Please try again in a moment.",
+ "errorSetupFailed": "Setup failed. Please try again.",
+ "errorLoginFailed": "Login failed. Please check your password.",
+ "errorInvalidCode": "Invalid code. Please try again.",
+ "totpLabel": "Two-factor authentication code"
+ },
+ "home": {
+ "title": "Welcome Noderunner",
+ "subtitle": "Here's an overview of your sovereign life",
+ "dashboardTab": "Dashboard",
+ "setupTab": "Setup",
+ "myApps": "My Apps",
+ "myAppsDesc": "Manage your installed applications",
+ "cloud": "Cloud",
+ "cloudDesc": "Cloud services and storage",
+ "network": "Network",
+ "networkDesc": "Network infrastructure and Web3 services",
+ "web5": "Web5",
+ "web5Desc": "Decentralized identity and data protocols",
+ "system": "System",
+ "quickStartGoals": "Quick Start Goals",
+ "quickStartDesc": "Not sure where to start? Try a guided setup.",
+ "installed": "Installed",
+ "runningLabel": "Running",
+ "storageUsed": "Storage Used",
+ "folders": "Folders",
+ "servicesStatus": "Services Status",
+ "connectivity": "Connectivity",
+ "runningApps": "Running Apps",
+ "didStatus": "DID Status",
+ "dwnSync": "DWN Sync",
+ "credentials": "Credentials",
+ "cpu": "CPU",
+ "ram": "RAM",
+ "disk": "Disk",
+ "browseStore": "Browse Store",
+ "manageApps": "Manage Apps",
+ "viewFolders": "View Folders",
+ "uploadFiles": "Upload Files",
+ "manageNetwork": "Manage Network",
+ "manageWeb5": "Manage Web5",
+ "openAI": "Open AI Assistant",
+ "noApps": "No Apps",
+ "allRunning": "All Running",
+ "systemMonitoring": "System monitoring",
+ "updateAvailable": "Update Available: v{version}",
+ "updateNow": "Update Now",
+ "goToApps": "Go to Apps",
+ "goToCloud": "Go to Cloud",
+ "goToNetwork": "Go to Network",
+ "goToWeb5": "Go to Web5",
+ "goToSettings": "Go to Settings"
+ },
+ "apps": {
+ "title": "My Apps",
+ "subtitle": "Manage your installed applications",
+ "searchPlaceholder": "Search installed apps...",
+ "noAppsTitle": "No Apps Installed",
+ "noAppsMessage": "Get started by browsing the app store",
+ "browseAppStore": "Browse App Store",
+ "noResults": "No apps matching \"{query}\"",
+ "uninstallTitle": "Uninstall App?",
+ "uninstallConfirm": "Are you sure you want to uninstall {name}? This will remove the app and stop its container.",
+ "dismissError": "Dismiss error",
+ "searchLabel": "Search installed apps"
+ },
+ "settings": {
+ "title": "Settings",
+ "subtitle": "Configure your Archipelago experience",
+ "account": "Account",
+ "interfaceMode": "Interface Mode",
+ "claudeAuth": "Claude Authentication",
+ "aiDataAccess": "AI Data Access",
+ "serverName": "Server Name",
+ "sessionStatus": "Session Status",
+ "yourDid": "Your DID",
+ "onionAddress": "Node .onion Address",
+ "loggedIn": "Currently logged in",
+ "didHelper": "Decentralized identifier for passwordless auth",
+ "onionHelper": "Onion address for node interface and peer discovery over Tor",
+ "changePassword": "Change Password",
+ "enable2fa": "Enable 2FA",
+ "disable2fa": "Disable 2FA",
+ "logout": "Logout",
+ "loggingOut": "Logging out...",
+ "twoFactorAuth": "Two-Factor Authentication",
+ "twoFaProtect": "Protect your account with an authenticator app",
+ "changePasswordTitle": "Change Password",
+ "changePasswordDesc": "Updates both web login and SSH access. Use a strong password (12+ chars, upper, lower, digit, special).",
+ "currentPassword": "Current Password",
+ "newPassword": "New Password",
+ "confirmNewPassword": "Confirm New Password",
+ "passwordPlaceholder": "12+ chars, upper, lower, digit, special",
+ "updateSshCheckbox": "Also update SSH password (recommended)",
+ "updatePassword": "Update Password",
+ "updatingPassword": "Updating...",
+ "setup2faTitle": "Two-Factor Authentication",
+ "setup2faPasswordPrompt": "Enter your password to begin setup.",
+ "scanQrCode": "Scan QR Code",
+ "scanQrInstruction": "Scan this QR code with your authenticator app (Google Authenticator, Authy, etc.), then enter the 6-digit code.",
+ "manualEntryKey": "Manual entry key:",
+ "verifyAndEnable": "Verify & Enable",
+ "saveBackupCodes": "Save Your Backup Codes",
+ "backupCodesInstruction": "Store these codes safely. Each can be used once if you lose access to your authenticator app.",
+ "copyAllCodes": "Copy All Codes",
+ "disable2faTitle": "Disable Two-Factor Authentication",
+ "disable2faDesc": "Enter your password and a current TOTP code to disable 2FA.",
+ "authenticatorCode": "Authenticator Code",
+ "webhooks": "Webhooks",
+ "webhooksDesc": "Get notified when important events happen on your node",
+ "webhookUrl": "Webhook URL",
+ "webhookUrlPlaceholder": "https://example.com/webhook",
+ "webhookSecret": "Secret (for HMAC signing)",
+ "webhookSecretPlaceholder": "Optional shared secret",
+ "webhookEvents": "Events",
+ "containerCrash": "Container Crash",
+ "updateAvailableEvent": "Update Available",
+ "diskWarning": "Disk Warning",
+ "backupComplete": "Backup Complete",
+ "saveWebhook": "Save",
+ "savingWebhook": "Saving...",
+ "testWebhook": "Test",
+ "testingWebhook": "Testing...",
+ "webhookSaved": "Webhook configuration saved",
+ "webhookTestSent": "Test webhook sent successfully",
+ "systemUpdates": "System Updates",
+ "backup": "Backup & Restore",
+ "backupDesc": "Back up your node data to external storage",
+ "createBackup": "Create Backup",
+ "creatingBackup": "Creating...",
+ "restoreBackup": "Restore Backup",
+ "deleteBackup": "Delete backup",
+ "backupCreated": "Backup created successfully",
+ "sendMessage": "Send Message",
+ "sendMessageTitle": "Send Broadcast Message",
+ "messagePlaceholder": "Enter your message...",
+ "messageSent": "Message sent",
+ "claudeConnected": "Connected to Claude",
+ "claudeDisconnected": "Not connected",
+ "claudeApiKey": "API Key",
+ "claudeApiKeyPlaceholder": "Enter your Anthropic API key",
+ "claudeSave": "Save Key",
+ "advancedMode": "Advanced Mode",
+ "beginnerMode": "Beginner Mode",
+ "advancedModeDesc": "Show all system controls and developer tools",
+ "beginnerModeDesc": "Simplified interface with guided experience",
+ "networkSettings": "Network Settings",
+ "torEnabled": "Tor Enabled",
+ "torAddress": "Tor Address",
+ "interfaceModeDesc": "Choose how you want to interact with your node.",
+ "claudeAuthDesc": "Connect your Claude Max account to enable AI chat features.",
+ "connectionStatus": "Connection Status",
+ "notConnected": "Not connected",
+ "reAuthenticate": "Re-authenticate",
+ "loginWithClaude": "Login with Claude",
+ "aiDataAccessDesc": "Control what data the AI assistant can see. All categories are off by default.",
+ "enableAllDesc": "Grant access to all data categories at once",
+ "systemUpdatesDesc": "Check for and install software updates",
+ "webhookNotifications": "Webhook Notifications",
+ "webhookNotificationsDesc": "Get push notifications for critical events via webhook",
+ "enableWebhooks": "Enable webhooks",
+ "disableWebhooks": "Disable webhooks",
+ "webhookUrlLabel": "Webhook URL",
+ "webhookSecretLabel": "Secret (optional, for HMAC-SHA256 signing)",
+ "eventsToNotify": "Events to notify",
+ "containerCrashDesc": "A running container stops unexpectedly",
+ "updateAvailableDesc": "A new system or app update is ready",
+ "diskWarningDesc": "Disk usage exceeds warning threshold",
+ "backupCompleteDesc": "A scheduled or manual backup finishes",
+ "backupRestoreDesc": "Encrypted backups of your identity, settings, and data",
+ "loadingBackups": "Loading backups...",
+ "noBackups": "No backups yet. Create one to protect your node data.",
+ "systemBackup": "System Backup",
+ "createEncryptedBackup": "Create Encrypted Backup",
+ "encryptionPassphrase": "Encryption Passphrase",
+ "enterPassphrase": "Enter a strong passphrase",
+ "descriptionOptional": "Description (optional)",
+ "descriptionPlaceholder": "e.g. Before update",
+ "restoreBackupTitle": "Restore Backup",
+ "restoreWarning": "This will overwrite current node data. Make sure you have the correct passphrase.",
+ "enterBackupPassphrase": "Enter backup passphrase",
+ "networkDesc": "Network connectivity, UPnP, and diagnostics",
+ "webhookSecretPlaceholderFull": "Shared secret for payload signing",
+ "backupCreatedSuccess": "Backup created successfully",
+ "backupCreateFailed": "Failed to create backup",
+ "backupVerifiedOk": "Backup verified — integrity OK",
+ "backupVerifyFailed": "Verification failed: {error}",
+ "backupVerifyRequestFailed": "Verification request failed",
+ "backupRestored": "Backup restored. Restart may be needed.",
+ "backupRestoreFailed": "Restore failed — check passphrase",
+ "backupDeleted": "Backup deleted",
+ "backupDeleteFailed": "Failed to delete backup",
+ "noUsbDrives": "No mounted USB drives found. Insert and mount a USB drive first.",
+ "backupCopiedToUsb": "Backup copied to {path}",
+ "backupUsbFailed": "Failed to copy backup to USB",
+ "deleteBackupConfirm": "Delete this backup permanently?",
+ "verifyPassphrasePrompt": "Enter backup passphrase to verify:",
+ "webhookSaveFailed": "Failed to save webhook configuration",
+ "webhookTestFailed": "Test failed: webhook not sent",
+ "webhookSendFailed": "Failed to send test webhook",
+ "passwordAllFieldsRequired": "All fields are required",
+ "passwordMismatch": "New passwords do not match",
+ "passwordUpdatedSuccess": "Password updated successfully. Use the new password for login and SSH.",
+ "passwordChangeFailed": "Failed to change password",
+ "passwordMinLength": "Password must be at least 12 characters",
+ "passwordNeedUppercase": "Password must contain at least one uppercase letter",
+ "passwordNeedLowercase": "Password must contain at least one lowercase letter",
+ "passwordNeedDigit": "Password must contain at least one digit",
+ "passwordNeedSpecial": "Password must contain at least one special character (!@#$%^&* etc.)",
+ "setupFailed": "Setup failed",
+ "verificationFailed": "Verification failed",
+ "disableFailed": "Failed to disable 2FA",
+ "copyToUsb": "Copy to USB",
+ "diskSpaceWarning": "Disk Space Warning",
+ "modeEasy": "Easy",
+ "modeEasyDesc": "Goal-based interface. Choose what you want to do, and the system handles the rest.",
+ "modePro": "Pro",
+ "modeProDesc": "Full control over all services. Configure everything manually with all technical details.",
+ "modeChat": "Chat",
+ "modeChatDesc": "Conversational AI interface. Manage your node through natural language. Coming soon."
+ },
+ "marketplace": {
+ "title": "App Store",
+ "subtitle": "Discover and install apps for your new sovereign life",
+ "curatedTab": "Curated",
+ "communityTab": "Community",
+ "filterByCategory": "Filter by Category",
+ "searchPlaceholder": "Search apps...",
+ "downloading": "Downloading...",
+ "alreadyInstalled": "Already Installed",
+ "queryingRelays": "Querying Nostr relays for apps...",
+ "noCommunityApps": "No community apps discovered yet.",
+ "noResults": "No apps found in {category} matching \"{query}\"",
+ "noResultsCategory": "No apps found in {category}",
+ "noResultsSearch": "No apps matching \"{query}\"",
+ "all": "All",
+ "community": "Community",
+ "commerce": "Commerce",
+ "money": "Money",
+ "data": "Data",
+ "homeCategory": "Home",
+ "auto": "Auto",
+ "networking": "Networking",
+ "other": "Other",
+ "searchApps": "Search apps",
+ "percentComplete": "{percent}% complete"
+ },
+ "dashboard": {
+ "mainNav": "Main navigation",
+ "mobileNav": "Mobile navigation"
+ },
+ "chat": {
+ "close": "Close",
+ "aiuiConnected": "AIUI connected",
+ "closeAssistant": "Close AI Assistant",
+ "loadingAssistant": "Loading AI assistant...",
+ "aiAssistant": "AI Assistant",
+ "notConfigured": "AI Assistant is not yet configured on this node.",
+ "deployCta": "Deploy the AIUI app from the App Store to enable this feature."
+ },
+ "web5": {
+ "title": "Web5",
+ "subtitle": "Decentralized identity and data protocols",
+ "profitsHelper": "Earn networking profits by hosting decentralized services",
+ "networkingProfits": "Networking Profits",
+ "didStatus": "DID Status",
+ "walletConnection": "Wallet Connection",
+ "wallet": "Wallet",
+ "walletSubtitle": "On-chain, Lightning & Ecash",
+ "nostrRelays": "Nostr Relays",
+ "connectedNodes": "Connected Nodes",
+ "bitcoinDomains": "Bitcoin Domain Names",
+ "domainsSubtitle": "NIP-05 verified identities",
+ "copyDid": "Copy DID",
+ "viewDidDocument": "View DID Document",
+ "createDid": "Create DID",
+ "creatingDid": "Creating...",
+ "manageDomains": "Manage Domains",
+ "relaysConnected": "{count} connected",
+ "peersKnown": "{count} peer(s) known",
+ "sendMessage": "Send Message",
+ "sendMessageTitle": "Send Message (over Tor)",
+ "to": "To",
+ "selectPeer": "Select a peer...",
+ "message": "Message",
+ "messagePlaceholder": "Type your message...",
+ "didDocument": "DID Document",
+ "addContent": "Add Content",
+ "addContentTitle": "Add Content",
+ "createIdentity": "Create Identity",
+ "createIdentityTitle": "Create Identity",
+ "deleteIdentity": "Delete Identity",
+ "deleteIdentityTitle": "Delete Identity",
+ "sendBitcoin": "Send Bitcoin",
+ "sendBitcoinTitle": "Send Bitcoin",
+ "receiveBitcoin": "Receive Bitcoin",
+ "receiveBitcoinTitle": "Receive Bitcoin",
+ "domains": "Domains",
+ "domainsTitle": "Domains",
+ "relays": "Relays",
+ "relaysTitle": "Relays",
+ "totalEarned": "Total Earned",
+ "monthlyAvg": "Monthly Avg",
+ "ecashBalance": "Ecash Balance",
+ "onChain": "On-chain",
+ "lightning": "Lightning",
+ "ecash": "Ecash",
+ "identityName": "Identity Name",
+ "identityNamePlaceholder": "Enter identity name",
+ "contentTitle": "Title",
+ "contentTitlePlaceholder": "Enter content title",
+ "amount": "Amount",
+ "amountPlaceholder": "Enter amount in sats",
+ "address": "Address",
+ "addressPlaceholder": "Enter Bitcoin address",
+ "deleteIdentityConfirm": "Are you sure you want to delete this identity? This action cannot be undone.",
+ "confirm": "Confirm",
+ "noRelays": "No relays connected",
+ "noDomains": "No domains configured",
+ "addRelay": "Add Relay",
+ "addDomain": "Add Domain",
+ "relayUrl": "Relay URL",
+ "relayUrlPlaceholder": "wss://relay.example.com",
+ "domainName": "Domain Name",
+ "domainNamePlaceholder": "user{'@'}example.com",
+ "peerNodesDescription": "Peer nodes discovered via Nostr. Messages sent over Tor.",
+ "nodeVisibility": "Node Visibility",
+ "nodeVisibilityDesc": "Control how other nodes can discover you",
+ "yourTorAddress": "Your Tor address",
+ "discoverableWarning": "Making your node discoverable lets other Archipelago users find and connect with you.",
+ "noPeers": "No peers yet. Add a peer manually or use Discover to find nodes on Nostr.",
+ "noMessages": "No messages yet. Messages from peers will appear here.",
+ "noRequests": "No pending connection requests.",
+ "accept": "Accept",
+ "reject": "Reject",
+ "discovering": "Discovering...",
+ "discoverNodes": "Discover Nodes on Nostr",
+ "refreshMessages": "Refresh Messages",
+ "refreshRequests": "Refresh Requests",
+ "torServices": "Tor Services",
+ "torServicesDesc": "Hidden services exposing your apps over Tor",
+ "noTorServices": "No Tor hidden services configured.",
+ "content": "Content",
+ "contentDesc": "Share and browse content with peers over Tor",
+ "noSharedContent": "No shared content",
+ "addFilesToShare": "Add files to share with connected peers.",
+ "browse": "Browse",
+ "connectingToPeer": "Connecting to peer over Tor...",
+ "selectPeerToBrowse": "Select a peer to browse",
+ "choosePeerDesc": "Choose a connected peer to see their shared content.",
+ "peerNoContent": "This peer has no shared content.",
+ "identities": "Identities",
+ "identitiesDesc": "Sovereign digital identities (DID:key)",
+ "noIdentities": "No identities yet",
+ "createFirstIdentity": "Create your first sovereign digital identity.",
+ "deleting": "Deleting...",
+ "decentralizedWebNode": "Decentralized Web Node",
+ "dwnDescription": "Personal data store with DID-based access control",
+ "manageDwn": "Manage DWN",
+ "syncing": "Syncing...",
+ "syncNow": "Sync Now",
+ "verifiableCredentials": "Verifiable Credentials",
+ "verifiableCredentialsDesc": "Issue and manage W3C Verifiable Credentials",
+ "noCredentials": "No credentials issued yet",
+ "messageSent": "Message sent over Tor!",
+ "failedToSend": "Failed to send",
+ "pasteInvoice": "Paste a Lightning invoice (BOLT11)",
+ "enterBitcoinAddress": "Enter a Bitcoin address",
+ "sendFailed": "Send failed",
+ "broadcastViaHwWallet": "Broadcast via hardware wallet",
+ "broadcastFailed": "Broadcast failed",
+ "psbtCopied": "PSBT copied!",
+ "enterAmount": "Enter an amount",
+ "pasteEcashToken": "Paste an ecash token",
+ "receiveFailed": "Receive failed",
+ "ecashTokenCopied": "Ecash token copied",
+ "contentAdded": "Content added",
+ "failedToAddContent": "Failed to add content",
+ "contentRemoved": "Content removed",
+ "failedToRemoveContent": "Failed to remove content",
+ "failedToUpdatePricing": "Failed to update pricing",
+ "failedToUpdatePrice": "Failed to update price",
+ "failedToConnectPeer": "Failed to connect to peer",
+ "onionAddressCopied": "Onion address copied",
+ "streamUrlCopied": "Stream URL copied",
+ "playerError": "Unable to load media. The content may only be accessible over Tor.",
+ "connectionAccepted": "Connection accepted",
+ "failedToAcceptRequest": "Failed to accept request",
+ "requestRejected": "Request rejected",
+ "failedToRejectRequest": "Failed to reject request",
+ "visibilitySetTo": "Visibility set to {level}",
+ "failedToUpdateVisibility": "Failed to update visibility",
+ "didCopied": "DID copied to clipboard",
+ "defaultIdentityUpdated": "Default identity updated",
+ "failedToSetDefault": "Failed to set default",
+ "identityCreated": "Identity created",
+ "failedToCreateIdentity": "Failed to create identity",
+ "identityDeleted": "Identity deleted",
+ "failedToDeleteIdentity": "Failed to delete identity",
+ "registrationFailed": "Registration failed",
+ "removeFailed": "Remove failed",
+ "failedToAddRelay": "Failed to add relay",
+ "failedToRemoveRelay": "Failed to remove relay",
+ "failedToToggleRelay": "Failed to toggle relay",
+ "downloadUrlCopied": "Download URL copied",
+ "hardwareWalletDetected": "Hardware Wallet Detected",
+ "namesRegistered": "Names Registered",
+ "expiringSoon": "Expiring Soon",
+ "nostrRelaysDesc": "Decentralized social networking relays",
+ "relaysConnectedLabel": "Relays Connected",
+ "totalRelays": "Total Relays",
+ "freeAccessDesc": "Available to all peers for free",
+ "peersOnlyAccessDesc": "Available only to connected peers",
+ "signWithHwWallet": "Sign with Hardware Wallet",
+ "createsPsbt": "Creates a PSBT for external signing",
+ "generateFreshAddress": "Generate a fresh Bitcoin address",
+ "registerNewName": "Register New Name",
+ "verifyNip05": "Verify NIP-05",
+ "peers": "Peers",
+ "messages": "Messages",
+ "requests": "Requests",
+ "myContent": "My Content",
+ "browsePeers": "Browse Peers",
+ "verified": "Verified",
+ "invalid": "Invalid",
+ "stream": "Stream",
+ "download": "Download"
+ },
+ "appDetails": {
+ "backToApps": "Back to My Apps",
+ "backToStore": "Back to App Store",
+ "screenshots": "Screenshots",
+ "screenshotPlaceholder": "Screenshot placeholders - images coming soon",
+ "about": "About {name}",
+ "features": "Features",
+ "information": "Information",
+ "requirements": "Requirements",
+ "ram": "RAM",
+ "ramDesc": "Minimum 512MB",
+ "storage": "Storage",
+ "storageDesc": "~100MB",
+ "links": "Links",
+ "website": "Website",
+ "sourceCode": "Source Code",
+ "documentation": "Documentation",
+ "services": "Services",
+ "guardian": "Guardian",
+ "gateway": "Gateway",
+ "access": "Access",
+ "lan": "LAN",
+ "tor": "Tor",
+ "requiresTor": "Requires Tor Browser",
+ "channels": "Channels",
+ "uninstallTitle": "Uninstall App?",
+ "uninstallConfirm": "Are you sure you want to uninstall {name}? This will remove the app and stop its container.",
+ "notFoundTitle": "App Not Found",
+ "notFoundMessage": "The requested application could not be found",
+ "installed": "Installed",
+ "channels": "Channels"
+ },
+ "containerDetails": {
+ "back": "Back",
+ "subtitle": "Container details and management",
+ "containerInfo": "Container Information",
+ "actions": "Actions",
+ "logs": "Logs",
+ "containerId": "Container ID",
+ "image": "Image",
+ "state": "State",
+ "created": "Created",
+ "startContainer": "Start Container",
+ "stopContainer": "Stop Container",
+ "loadingLogs": "Loading logs...",
+ "noLogs": "No logs available"
+ },
+ "marketplaceDetails": {
+ "backToStore": "Back to App Store",
+ "screenshots": "Screenshots",
+ "screenshotPlaceholder": "Screenshot placeholders - images coming soon",
+ "about": "About {name}",
+ "features": "Features",
+ "information": "Information",
+ "requirements": "Requirements",
+ "noRequirements": "No additional dependencies required",
+ "installRequirements": "Install Requirements",
+ "links": "Links",
+ "downloadPackage": "Download Package",
+ "installed": "Installed",
+ "notInstalled": "Not Installed",
+ "open": "Open",
+ "loadingDetails": "Loading app details...",
+ "notFoundTitle": "App Not Found",
+ "notFoundMessage": "The requested application could not be found in the marketplace",
+ "installFailed": "Installation Failed",
+ "depRunning": "Running",
+ "depStopped": "Installed but stopped",
+ "depNotInstalled": "Not installed"
+ },
+ "goalDetail": {
+ "backToGoals": "Back to Goals",
+ "notFound": "Goal not found.",
+ "stepOf": "Step {current} of {total}",
+ "notStarted": "Not Started",
+ "inProgress": "In Progress",
+ "completed": "Completed",
+ "syncTitle": "Sovereignty takes a little patience",
+ "syncMessage": "Your Bitcoin node is syncing the entire blockchain so you don't have to trust anyone else. This takes 2-3 days on first run. Meanwhile, you can explore your node, set up your identity, or back up your keys.",
+ "installApp": "Install {name}",
+ "openAndConfigure": "Open & Configure",
+ "iveDoneThis": "I've Done This",
+ "complete": "Complete",
+ "allSet": "All Set!",
+ "goalReady": "{title} is ready to go.",
+ "viewMyServices": "View My Services"
+ },
+ "monitoring": {
+ "title": "Monitoring",
+ "subtitle": "Real-time system metrics and container resource usage",
+ "cpuUsage": "CPU Usage (%)",
+ "memoryUsage": "Memory Usage (%)",
+ "networkIo": "Network I/O (bytes)",
+ "rpcLatency": "RPC Latency (ms)",
+ "alertHistory": "Alert History",
+ "hideConfig": "Hide Config",
+ "noAlerts": "No alerts fired",
+ "containerResources": "Container Resources",
+ "noContainerMetrics": "No container metrics available",
+ "systemHealth": "System Health",
+ "load": "Load:",
+ "exportCsv": "Export CSV",
+ "exportJson": "Export JSON",
+ "diskUsage": "Disk Usage",
+ "ramUsage": "RAM Usage",
+ "containerCrash": "Container Crash",
+ "rpcLatencySpike": "RPC Latency Spike",
+ "sslCertExpiry": "SSL Cert Expiry",
+ "refreshFooter": "Refreshing every 5 seconds",
+ "wsConnections": "WS connections: {count}",
+ "cpu": "CPU",
+ "memory": "Memory",
+ "network": "Network"
+ },
+ "systemUpdate": {
+ "title": "System Update",
+ "subtitle": "Manage software updates for your Archipelago node",
+ "currentSystem": "Current System",
+ "updateAvailable": "Update Available",
+ "upToDate": "System is up to date",
+ "downloading": "Downloading Update...",
+ "applying": "Applying Update...",
+ "updateSchedule": "Update Schedule",
+ "actions": "Actions",
+ "lastChecked": "Last Checked",
+ "new": "New",
+ "changelog": "Changelog",
+ "componentsToUpdate": "{count} component(s) to update",
+ "manualOnly": "Manual Only",
+ "manualOnlyDesc": "Never check automatically. You control when to check and install updates.",
+ "dailyCheck": "Daily Check",
+ "dailyCheckDesc": "Check for updates once per day. You decide when to install.",
+ "autoApply": "Auto-Apply",
+ "autoApplyDesc": "Check daily and automatically install updates at 3 AM. Service restarts as needed.",
+ "downloadUpdate": "Download Update",
+ "applyUpdate": "Apply Update",
+ "checkForUpdates": "Check for Updates",
+ "checking": "Checking...",
+ "rollback": "Rollback to Previous",
+ "backToSettings": "Back to Settings",
+ "percentComplete": "{percent}% complete",
+ "applyWarning": "Installing components and restarting services. Do not power off.",
+ "applyTitle": "Apply Update?",
+ "applyMessage": "The backend service will restart. This may take a moment.",
+ "rollbackTitle": "Rollback Version?",
+ "rollbackMessage": "This will restore the previous version. The backend service will restart.",
+ "applyNow": "Apply Now",
+ "rollbackButton": "Rollback",
+ "upToDateMessage": "Your system is up to date. No updates available. Your system is running the latest version.",
+ "checkFailed": "Failed to check for updates. Check your internet connection.",
+ "downloadSuccess": "Downloaded {count} component(s) ({size}MB)",
+ "downloadFailed": "Download failed. Please try again.",
+ "applySuccess": "Update applied. The service will restart momentarily.",
+ "applyFailed": "Failed to apply update. You can try again or rollback.",
+ "rollbackSuccess": "Rolled back to previous version. Service will restart.",
+ "rollbackFailed": "Rollback failed."
+ },
+ "kioskRecovery": {
+ "title": "Archipelago Recovery",
+ "subtitle": "Kiosk failsafe — no authentication required",
+ "serverAddress": "Server Address",
+ "webUi": "Web UI: http://{address}",
+ "scanForMobile": "Scan for mobile access",
+ "backend": "Backend",
+ "unreachable": "Unreachable",
+ "containers": "Containers",
+ "goToLogin": "Go to Login",
+ "lastChecked": "Last checked: {time}"
+ }
+}
diff --git a/neode-ui/src/main.ts b/neode-ui/src/main.ts
index 31fbfe87..fe3a3d34 100644
--- a/neode-ui/src/main.ts
+++ b/neode-ui/src/main.ts
@@ -3,11 +3,13 @@ import { createPinia } from 'pinia'
import './style.css'
import App from './App.vue'
import router from './router'
+import i18n from './i18n'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
+app.use(i18n)
app.mount('#app')
diff --git a/neode-ui/src/views/AppDetails.vue b/neode-ui/src/views/AppDetails.vue
index c9789b2b..70540812 100644
--- a/neode-ui/src/views/AppDetails.vue
+++ b/neode-ui/src/views/AppDetails.vue
@@ -60,7 +60,7 @@
- Channels
+ {{ t('appDetails.channels') }}
Screenshot placeholders - images coming soon
+{{ t('appDetails.screenshotPlaceholder') }}
{{ pkg.manifest.description.long }}
@@ -231,7 +231,7 @@Guardian
+{{ t('appDetails.guardian') }}
{{ pkg.state }}
Gateway
+{{ t('appDetails.gateway') }}
{{ gatewayState }}
RAM
-Minimum 512MB
+{{ t('appDetails.ram') }}
+{{ t('appDetails.ramDesc') }}
Storage
-~100MB
+{{ t('appDetails.storage') }}
+{{ t('appDetails.storageDesc') }}
The requested application could not be found
+{{ t('appDetails.notFoundMessage') }}
- Are you sure you want to uninstall {{ uninstallModal.appTitle }}? - This will remove the app and stop its container. + {{ t('appDetails.uninstallConfirm', { name: uninstallModal.appTitle }) }}
Two-Factor Authentication
-Enter the 6-digit code from your authenticator app
+{{ t('login.twoFactorTitle') }}
+{{ t('login.totpInstruction') }}
Decentralized identity and data protocols
-Earn networking profits by hosting decentralized services
+{{ t('web5.subtitle') }}
+{{ t('web5.profitsHelper') }}
Networking Profits
+{{ t('web5.networkingProfits') }}
{{ networkingProfitsDisplay }}
DID Status
+{{ t('web5.didStatus') }}
{{ userDid }}
{{ didStatus }}
Wallet
-{{ walletConnected ? 'Connected' : 'Disconnected' }}
+{{ t('web5.wallet') }}
+{{ walletConnected ? t('common.connected') : t('common.disconnected') }}
Nostr Relays
-{{ nostrRelayStats?.connected_count ?? 0 }} connected
+{{ t('web5.nostrRelays') }}
+{{ t('web5.relaysConnected', { count: nostrRelayStats?.connected_count ?? 0 }) }}
Connected Nodes
-{{ connectedNodesCount }} peer{{ connectedNodesCount !== 1 ? 's' : '' }} known
+{{ t('web5.connectedNodes') }}
+{{ t('web5.peersKnown', { count: connectedNodesCount }) }}
Hardware Wallet Detected
+{{ t('web5.hardwareWalletDetected') }}
{{ detectedHwWallets.map(d => `${d.type}${d.product ? ' (' + d.product + ')' : ''}`).join(', ') }}
@@ -146,29 +146,29 @@{{ didDocumentFormatted }}
Messages are sent over the Tor network to the selected peer.
{{ sendMessageError }}
@@ -236,8 +236,8 @@NIP-05 verified identities
+{{ t('web5.domainsSubtitle') }}
On-chain, Lightning & Ecash
+{{ t('web5.walletSubtitle') }}
Decentralized social networking relays
+{{ t('web5.nostrRelaysDesc') }}
Control how other nodes can discover you
+{{ t('web5.nodeVisibilityDesc') }}
Your Tor address
+{{ t('web5.yourTorAddress') }}
{{ nodeOnionAddress }}
Peer nodes discovered via Nostr. Messages sent over Tor.
+{{ t('web5.peerNodesDescription') }}
Hidden services exposing your apps over Tor
+{{ t('web5.torServicesDesc') }}
{{ browsePeerError }}
@@ -851,7 +851,7 @@Connecting to peer over Tor...
+{{ t('web5.connectingToPeer') }}
@@ -859,13 +859,13 @@Select a peer to browse
-Choose a connected peer to see their shared content.
+{{ t('web5.selectPeerToBrowse') }}
+{{ t('web5.choosePeerDesc') }}
This peer has no shared content.
+{{ t('web5.peerNoContent') }}
{{ addContentError }}
Sovereign digital identities (DID:key)
+{{ t('web5.identitiesDesc') }}
Loading identities...
+{{ t('common.loading') }}
No identities yet
-Create your first sovereign digital identity.
+{{ t('web5.noIdentities') }}
+{{ t('web5.createFirstIdentity') }}
{{ createIdentityError }}
This will permanently delete "{{ deleteIdentityTarget.name }}" and its keypair.
+{{ t('web5.deleteIdentityConfirm') }}
Sign with Hardware Wallet
-Creates a PSBT for external signing
+{{ t('web5.signWithHwWallet') }}
+{{ t('web5.createsPsbt') }}
Generate a fresh Bitcoin address
+{{ t('web5.generateFreshAddress') }}
Personal data store with DID-based access control
+{{ t('web5.dwnDescription') }}
Issue and manage W3C Verifiable Credentials
+{{ t('web5.verifiableCredentialsDesc') }}