mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-24 06:39:12 -05:00
Compare commits
349 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48414dcae4 | ||
|
|
151548f6f7 | ||
|
|
fd5c8096ad | ||
|
|
09cfee2888 | ||
|
|
74cb2eae7d | ||
|
|
35b8f0abae | ||
|
|
08517e3469 | ||
|
|
f3dabc3a39 | ||
|
|
d98f047963 | ||
|
|
599966996e | ||
|
|
952cfd9a28 | ||
|
|
81a5155734 | ||
|
|
3a953ec7c8 | ||
|
|
392dbd626c | ||
|
|
b6d3f9e70f | ||
|
|
c2f2511f6a | ||
|
|
ce2e21900f | ||
|
|
660b286ee9 | ||
|
|
133037dcd8 | ||
|
|
03b65a63ba | ||
|
|
f7a8189b86 | ||
|
|
38973de6f1 | ||
|
|
9ddd00bfa4 | ||
|
|
88013161d1 | ||
|
|
b0da0d8590 | ||
|
|
7dcfd6bfd1 | ||
|
|
586b0a3495 | ||
|
|
30a009c5c4 | ||
|
|
7d73222ee1 | ||
|
|
6d191a1bd5 | ||
|
|
e5c68c6c6e | ||
|
|
58c39815e4 | ||
|
|
4b706f466f | ||
|
|
19f72b1386 | ||
|
|
b4d883dbf0 | ||
|
|
86f8f4ebdf | ||
|
|
b5df1ed8dd | ||
|
|
b2c25db5d9 | ||
|
|
c0c876c694 | ||
|
|
b832d19e0e | ||
|
|
68214becad | ||
|
|
0971922518 | ||
|
|
1e9767b0bb | ||
|
|
3f12bdad9d | ||
|
|
0ee17cc0ee | ||
|
|
c7448f7e99 | ||
|
|
835b350d53 | ||
|
|
b7cbecc61d | ||
|
|
5e2f950b7e | ||
|
|
9a97a904fb | ||
|
|
56b6753320 | ||
|
|
f7675c0279 | ||
|
|
961d237d42 | ||
|
|
47c2ae1e56 | ||
|
|
9658a40c76 | ||
|
|
752ddaea9c | ||
|
|
5efc277316 | ||
|
|
88b32efa97 | ||
|
|
03f692a62f | ||
|
|
bca8ffe676 | ||
|
|
d2590f4222 | ||
|
|
ef245b2566 | ||
|
|
9ae92962d3 | ||
|
|
e52cd927a5 | ||
|
|
582f7c2ebc | ||
|
|
ce5e5df644 | ||
|
|
6a2e663c57 | ||
|
|
f6adb93518 | ||
|
|
077a4fb3ee | ||
|
|
dc4fa1b487 | ||
|
|
949b51defd | ||
|
|
c2b824c31e | ||
|
|
cc846830fe | ||
|
|
f6ab23fa03 | ||
|
|
44d84187c8 | ||
|
|
fe78524e41 | ||
|
|
adc0e8227f | ||
|
|
55cb24be68 | ||
|
|
8efc021bd7 | ||
|
|
b649bdeb2e | ||
|
|
af4ca2e018 | ||
|
|
1fa9606491 | ||
|
|
7620fa8186 | ||
|
|
4a5d42d65b | ||
|
|
af0f582090 | ||
|
|
4f91ae7f1c | ||
|
|
67c4b55cbb | ||
|
|
7ff608b08c | ||
|
|
4ebbea7825 | ||
|
|
1260e94199 | ||
|
|
3b8d0d3a8a | ||
|
|
2725646a6a | ||
|
|
89cddcc626 | ||
|
|
f7d9d2a47c | ||
|
|
60833efcda | ||
|
|
70208eb81a | ||
|
|
ae6e734dc9 | ||
|
|
f1fc2a5f96 | ||
|
|
b62621c9c6 | ||
|
|
a372348dbf | ||
|
|
779d2a6b43 | ||
|
|
9510c0232f | ||
|
|
1e97960eab | ||
|
|
c756156e0d | ||
|
|
af98a252c8 | ||
|
|
a7f016d73f | ||
|
|
3a287ebc77 | ||
|
|
65c1a60447 | ||
|
|
c6906c8caf | ||
|
|
ace1bd7b0f | ||
|
|
56e82cd046 | ||
|
|
58d6b4c67c | ||
|
|
7e4a0f6e07 | ||
|
|
b543696fa9 | ||
|
|
e669738e38 | ||
|
|
961977c9e2 | ||
|
|
e3d2bec203 | ||
|
|
75d9249577 | ||
|
|
016a7e7559 | ||
|
|
b6e7a2e77a | ||
|
|
fd9e62591e | ||
|
|
fd485b979c | ||
|
|
410e845811 | ||
|
|
b5207d97fb | ||
|
|
3122dc4807 | ||
|
|
e010f0f57b | ||
|
|
864a7630d5 | ||
|
|
b603a177e2 | ||
|
|
ee2fd9f9ae | ||
|
|
a14066c43f | ||
|
|
1bcd088782 | ||
|
|
4ff937feec | ||
|
|
77d49c52f0 | ||
|
|
f09cfecb13 | ||
|
|
8655f15731 | ||
|
|
d629ffb6e5 | ||
|
|
21e0ad5017 | ||
|
|
279a1f2ab2 | ||
|
|
957be55927 | ||
|
|
63a8be657c | ||
|
|
7559f0aff4 | ||
|
|
c89afa613f | ||
|
|
7f449694c8 | ||
|
|
8797b3b360 | ||
|
|
4af333e22d | ||
|
|
17e8b6c16c | ||
|
|
694f1d5e8f | ||
|
|
6f32692342 | ||
|
|
358d838f3b | ||
|
|
2e47486195 | ||
|
|
6936d4da3b | ||
|
|
17a7a57136 | ||
|
|
a3552471af | ||
|
|
886208460b | ||
|
|
a6fea3a60a | ||
|
|
fb9c2e1494 | ||
|
|
2b259eee0c | ||
|
|
d9a8e671a1 | ||
|
|
f9a9cb83c4 | ||
|
|
3eae4b478f | ||
|
|
06dc2eadae | ||
|
|
2fa11dab67 | ||
|
|
c73e3a489c | ||
|
|
2b19d27902 | ||
|
|
812302b9bc | ||
|
|
4581dc8fd9 | ||
|
|
42ba9d2869 | ||
|
|
773e6569c2 | ||
|
|
c24671ffb1 | ||
|
|
cd87692588 | ||
|
|
15dc89ac07 | ||
|
|
a95757e982 | ||
|
|
6061511d3c | ||
|
|
cc873fd483 | ||
|
|
8caa69e130 | ||
|
|
c45d0c8f56 | ||
|
|
6c0fc44a66 | ||
|
|
3b88cb5b50 | ||
|
|
7314dc3d1d | ||
|
|
2c98b81111 | ||
|
|
fe7da551a4 | ||
|
|
c4c29b11f3 | ||
|
|
ab740c093f | ||
|
|
056f8e97e9 | ||
|
|
819924c6e2 | ||
|
|
c6203b9e19 | ||
|
|
347a72e55d | ||
|
|
30a2b1326c | ||
|
|
4d66ea9694 | ||
|
|
1cf28c43fb | ||
|
|
6a75e56123 | ||
|
|
ef72abceb4 | ||
|
|
19406cf58d | ||
|
|
9fda76a5ff | ||
|
|
610d1b4654 | ||
|
|
602d59d268 | ||
|
|
edae632025 | ||
|
|
2c3d2379ee | ||
|
|
70ed03e1b3 | ||
|
|
bf1a235dd2 | ||
|
|
2bb7f0a742 | ||
|
|
8cd5118749 | ||
|
|
2fccb162e6 | ||
|
|
ad3c0323b9 | ||
|
|
9e859f6dc0 | ||
|
|
5f70912b7a | ||
|
|
dcc45eb5b6 | ||
|
|
340d3943a2 | ||
|
|
64a879f72d | ||
|
|
0f8e1f7e15 | ||
|
|
f86400fa50 | ||
|
|
047b0723b3 | ||
|
|
f785063065 | ||
|
|
3720ad1961 | ||
|
|
fe617fc024 | ||
|
|
1138b16daa | ||
|
|
108a6855c2 | ||
|
|
fb002e54b7 | ||
|
|
58ae63c74b | ||
|
|
51287c85dc | ||
|
|
b638e3375d | ||
|
|
5d827bb7ac | ||
|
|
666b3ccada | ||
|
|
87a62000d3 | ||
|
|
54c6e94751 | ||
|
|
54a5584baf | ||
|
|
ff48f1882f | ||
|
|
0b95203aac | ||
|
|
3f5328ab3c | ||
|
|
f913d84557 | ||
|
|
9a9752c557 | ||
|
|
82458f74e3 | ||
|
|
71633b166e | ||
|
|
3305958e60 | ||
|
|
4ae1f6ec35 | ||
|
|
4498833b4e | ||
|
|
7054593c07 | ||
|
|
6d197fe870 | ||
|
|
d70eb0a447 | ||
|
|
aecb52de3c | ||
|
|
cd6ea06430 | ||
|
|
0d13440821 | ||
|
|
8e3da4b381 | ||
|
|
81538d4666 | ||
|
|
634b7cada1 | ||
|
|
bed2c78964 | ||
|
|
a75392c573 | ||
|
|
7b10665488 | ||
|
|
ddf995db1d | ||
|
|
8d9d55ce82 | ||
|
|
ccf473635e | ||
|
|
56c8b61e9e | ||
|
|
69234de51c | ||
|
|
893c06cc00 | ||
|
|
b2c07f6de6 | ||
|
|
229fbd4824 | ||
|
|
48c5a5e38a | ||
|
|
5b3f36936a | ||
|
|
b4c696c89b | ||
|
|
d53c133812 | ||
|
|
cbbfe1c611 | ||
|
|
437c7bb807 | ||
|
|
03faee8d3a | ||
|
|
e66a87e8df | ||
|
|
11f1daa08b | ||
|
|
784e64ece8 | ||
|
|
4da1333aa5 | ||
|
|
65413c7ab7 | ||
|
|
290e5329f8 | ||
|
|
ec060d1392 | ||
|
|
293501405f | ||
|
|
783b2d44ef | ||
|
|
29d38759eb | ||
|
|
97f30ad9ba | ||
|
|
c728d71868 | ||
|
|
27fc298b5e | ||
|
|
6eb8266d05 | ||
|
|
f22cac70e9 | ||
|
|
f1c94ea145 | ||
|
|
d587f3fd5c | ||
|
|
db874d3799 | ||
|
|
3f5b731703 | ||
|
|
258981b2e4 | ||
|
|
34b3545168 | ||
|
|
c37dafd228 | ||
|
|
dbe15bdc51 | ||
|
|
9eb4a3136a | ||
|
|
747596615e | ||
|
|
60221cf0e8 | ||
|
|
d9aa765284 | ||
|
|
b7a916e414 | ||
|
|
110c0d2628 | ||
|
|
ecfc6f948d | ||
|
|
990d94397b | ||
|
|
b861a30596 | ||
|
|
583534fae9 | ||
|
|
8136eb379d | ||
|
|
9f5c1b35c4 | ||
|
|
7bd51fa2fe | ||
|
|
4340ed48e6 | ||
|
|
2fabc8c4dc | ||
|
|
99884b9761 | ||
|
|
c80a9c1b32 | ||
|
|
3c993fe875 | ||
|
|
ca1f3c3f64 | ||
|
|
728b5c2a9c | ||
|
|
73600a49f8 | ||
|
|
8a2e806311 | ||
|
|
9c8462f9ce | ||
|
|
e2fc9878b0 | ||
|
|
f5f05703a0 | ||
|
|
b30f8853aa | ||
|
|
d85d62f3b4 | ||
|
|
8bd8d688ef | ||
|
|
c174a6bfb4 | ||
|
|
3125eb3751 | ||
|
|
1e5a84b392 | ||
|
|
180977b833 | ||
|
|
2d40e424e8 | ||
|
|
af0b5ff5f8 | ||
|
|
1b8e6cc6a1 | ||
|
|
eb04263751 | ||
|
|
daccab9bcc | ||
|
|
6577021bd7 | ||
|
|
de6ae7f7e1 | ||
|
|
a272aa11f2 | ||
|
|
6cc77adbab | ||
|
|
b6b476f9c8 | ||
|
|
86aef6961c | ||
|
|
542f99c484 | ||
|
|
6ce666a35d | ||
|
|
0ddd47b0e7 | ||
|
|
f55d7717f8 | ||
|
|
1eaacd1ed0 | ||
|
|
4b385e0ea2 | ||
|
|
ff90cc2937 | ||
|
|
8bb6ec2b7c | ||
|
|
7a4e55912c | ||
|
|
a1f97cd709 | ||
|
|
dbb2aa5610 | ||
|
|
3af46c80fa | ||
|
|
e10ef4bd75 | ||
|
|
54853c7a4d | ||
|
|
1dde9ab4b4 | ||
|
|
3585e20354 | ||
|
|
c926933804 | ||
|
|
5a43f7142c | ||
|
|
a15138afc8 | ||
|
|
bd62ecd8bd |
96
.env.example
96
.env.example
@@ -14,91 +14,63 @@
|
||||
# Docker containers to apply the changes.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ===========================================
|
||||
# NETWORK PORTS
|
||||
# ===========================================
|
||||
|
||||
# Configure the network ports used by AliasVault by the `reverse-proxy` and `smtp` containers.
|
||||
# You can change these if the defaults are in use on your system.
|
||||
# After making changes, re-run the install script to apply them.
|
||||
# You can change these if the defaults are already in use on your system.
|
||||
# Requires a restart before taking effect.
|
||||
HTTP_PORT=80
|
||||
HTTPS_PORT=443
|
||||
SMTP_PORT=25
|
||||
SMTP_TLS_PORT=587
|
||||
|
||||
# Set the hostname that your AliasVault will be accessible at.
|
||||
# E.g. `aliasvault.mydomain.com` or if you're running it on your local machine, choose `localhost`.
|
||||
HOSTNAME=
|
||||
# Whether to force redirect all HTTP traffic (80) to HTTPS (443). Defaults to true.
|
||||
FORCE_HTTPS_REDIRECT=true
|
||||
|
||||
# Set a random 32 character string for the JWT key.
|
||||
# This can be generated using the following command:
|
||||
# $ openssl rand -base64 32
|
||||
JWT_KEY=
|
||||
# ===========================================
|
||||
# EMAIL SERVER CONFIGURATION
|
||||
# ===========================================
|
||||
|
||||
# Set the password for the data protection certificate.
|
||||
# This can be generated using the following command:
|
||||
# $ openssl rand -base64 32
|
||||
DATA_PROTECTION_CERT_PASS=
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Database configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# These are the credentials that are used by the PostgreSQL container
|
||||
# on startup to create the database and user, and for the application to
|
||||
# connect to the database.
|
||||
POSTGRES_DB=aliasvault
|
||||
POSTGRES_USER=aliasvault
|
||||
|
||||
# Set the password for the database user.
|
||||
# This can be generated using the following command:
|
||||
# $ openssl rand -base64 32
|
||||
POSTGRES_PASSWORD=
|
||||
|
||||
# Note: in order to change the password for an existing installation
|
||||
# refer to https://docs.aliasvault.net/misc/dev/database-operations.html
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Admin user configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Set the password for the admin user. This is an encrypted hash that needs
|
||||
# to be generated using the `aliasvault-cli` tool. This allows you to login
|
||||
# to the admin panel at https://your-hostname/admin.
|
||||
#
|
||||
# For example:
|
||||
# docker run --rm ghcr.io/lanedirt/aliasvault-installcli:latest hash-password "my-password"
|
||||
#
|
||||
# Then copy the output and paste it into the ADMIN_PASSWORD_HASH variable below.
|
||||
# When changing the hash, update the ADMIN_PASSWORD_GENERATED variable to the current date and time
|
||||
# and then restart the AliasVault docker containers to apply the changes.
|
||||
ADMIN_PASSWORD_HASH=
|
||||
|
||||
# Set the date and time the admin password was last generated. When changing the
|
||||
# admin password hash manually, make sure to increase this value so the system
|
||||
# knows that the password has been changed and should be overwritten with the new hash.
|
||||
ADMIN_PASSWORD_GENERATED=2024-01-01T00:00:00Z
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Email server configuration for email aliases
|
||||
# ----------------------------------------------------------------------------
|
||||
# In order to use AliasVault's private email domains feature, you need to configure
|
||||
# your DNS. Please refer to the full documentation for more instructions on DNS:
|
||||
# https://docs.aliasvault.net/installation/install.html#3-email-server-setup
|
||||
#
|
||||
# Set the private email domains below that are allowed to be used (comma separated values).
|
||||
# Example: PRIVATE_EMAIL_DOMAINS=example.com,example2.org
|
||||
# To disable the private email domains feature, set this to "DISABLED.TLD"
|
||||
PRIVATE_EMAIL_DOMAINS=DISABLED.TLD
|
||||
# To disable the private email domains feature, keep this empty.
|
||||
PRIVATE_EMAIL_DOMAINS=
|
||||
|
||||
# Set whether TLS is enabled for SMTP.
|
||||
# Enable TLS for SMTP.
|
||||
# ⚠️ Requires valid TLS certificates on your mail server (not provided by the AliasVault installer).
|
||||
# If set to true without proper certificates, the SMTP service will fail to start.
|
||||
# For self-hosted setups, we recommend keeping this **false** unless you're sure how to configure it.
|
||||
# Note: Disabling TLS does **not** impact email deliverability.
|
||||
SMTP_TLS_ENABLED=false
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# ===========================================
|
||||
# Let's Encrypt configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# ===========================================
|
||||
# Set whether Let's Encrypt is enabled. This is only supported through
|
||||
# the install.sh script.
|
||||
# the install.sh script and should be set to false for manual installations.
|
||||
LETSENCRYPT_ENABLED=false
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Set the hostname that your AliasVault will be accessible at in order for LetsEncrypt
|
||||
# to do its validation. This value is only required when LETSENCRYPT_ENABLED
|
||||
# is set to true.
|
||||
# Example: `aliasvault.mydomain.net`.
|
||||
HOSTNAME=
|
||||
|
||||
# ===========================================
|
||||
# Optional configuration settings
|
||||
# ----------------------------------------------------------------------------
|
||||
# ===========================================
|
||||
# Enable or disable ability for new users to create an account via the web interface.
|
||||
# Note: make sure you have created your (own) accounts before setting this to false.
|
||||
PUBLIC_REGISTRATION_ENABLED=true
|
||||
|
||||
# Whether to enable IP logging for auth attempts. When set to true the last octet is
|
||||
# always still anonymized, e.g. "127.0.0.1" becomes "127.0.0.xxx".
|
||||
IP_LOGGING_ENABLED=true
|
||||
|
||||
# Set the support email address which is shown to users in the main web app.
|
||||
|
||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1,2 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
buy_me_a_coffee: lanedirt
|
||||
open_collective: aliasvault
|
||||
|
||||
239
.github/workflows/docker-build.yml
vendored
239
.github/workflows/docker-build.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Docker Pull and Build
|
||||
name: Docker Build Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,142 +11,153 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
docker-compose-pull:
|
||||
name: Docker Compose Pull Test
|
||||
docker-all-in-one-build:
|
||||
name: Docker All-in-One Build Test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
docker:
|
||||
image: docker:26.0.0
|
||||
options: --privileged
|
||||
|
||||
steps:
|
||||
- name: Get repository and branch information
|
||||
id: repo-info
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.pull_request.head.repo.fork }}" = "true" ]; then
|
||||
echo "REPO_FULL_NAME=lanedirt/AliasVault" >> $GITHUB_ENV
|
||||
echo "BRANCH_NAME=main" >> $GITHUB_ENV
|
||||
else
|
||||
echo "REPO_FULL_NAME=${GITHUB_REPOSITORY}" >> $GITHUB_ENV
|
||||
echo "BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Check local docker-compose.yml for :latest tags
|
||||
- name: Build all-in-one Docker image
|
||||
run: |
|
||||
# Check for explicit version tags instead of :latest
|
||||
if grep -E "ghcr\.io/lanedirt/aliasvault-[^:]+:[0-9]+\.[0-9]+\.[0-9]+" docker-compose.yml; then
|
||||
echo "❌ Error: docker-compose.yml contains explicit version tags instead of :latest"
|
||||
echo "Found the following explicit versions:"
|
||||
grep -E "ghcr\.io/lanedirt/aliasvault-[^:]+:[0-9]+\.[0-9]+\.[0-9]+" docker-compose.yml
|
||||
echo ""
|
||||
echo "All AliasVault images in docker-compose.yml must use ':latest' tags, not explicit versions."
|
||||
echo "Please update docker-compose.yml to use ':latest' for all AliasVault images."
|
||||
docker build -f dockerfiles/all-in-one/Dockerfile -t aliasvault-allinone:test .
|
||||
echo "✅ All-in-one Docker image built successfully"
|
||||
|
||||
- name: Run all-in-one container
|
||||
run: |
|
||||
docker run -d \
|
||||
--name aliasvault-test \
|
||||
-p 8080:80 \
|
||||
-p 8443:443 \
|
||||
-p 2525:25 \
|
||||
-p 2587:587 \
|
||||
-v "$(pwd)/database:/database" \
|
||||
-v "$(pwd)/certificates:/certificates" \
|
||||
-v "$(pwd)/logs:/logs" \
|
||||
-v "$(pwd)/secrets:/secrets" \
|
||||
aliasvault-allinone:test
|
||||
|
||||
- name: Wait for services to be ready
|
||||
run: |
|
||||
echo "Waiting for services to initialize..."
|
||||
for i in {1..60}; do
|
||||
if docker exec aliasvault-test curl -f http://localhost:3001/api 2>/dev/null; then
|
||||
echo "✅ API service is ready"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for services... ($i/60)"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
- name: Check container logs if needed
|
||||
if: failure()
|
||||
run: docker logs aliasvault-test
|
||||
|
||||
- name: Test root endpoint
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:8443/)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "❌ Root endpoint (/) failed with HTTP $http_code"
|
||||
docker logs aliasvault-test
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Root endpoint (/) returned HTTP 200"
|
||||
|
||||
- name: Test API endpoint
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:8443/api)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "❌ API endpoint (/api) failed with HTTP $http_code"
|
||||
docker logs aliasvault-test
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ API endpoint (/api) returned HTTP 200"
|
||||
|
||||
- name: Test Admin endpoint
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:8443/admin/user/login)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "❌ Admin endpoint (/admin) failed with HTTP $http_code"
|
||||
docker logs aliasvault-test
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Admin endpoint (/admin) returned HTTP 200"
|
||||
|
||||
- name: Verify admin password hash file does not exist initially
|
||||
run: |
|
||||
if [ -f "./secrets/admin_password_hash" ]; then
|
||||
echo "❌ Admin password hash file should not exist initially"
|
||||
cat ./secrets/admin_password_hash
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Admin password hash file correctly does not exist initially"
|
||||
|
||||
echo "✅ docker-compose.yml correctly uses :latest tags for all AliasVault images"
|
||||
|
||||
- name: Download install script from current branch
|
||||
- name: Test admin password reset flow
|
||||
run: |
|
||||
INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/$REPO_FULL_NAME/$BRANCH_NAME/install.sh"
|
||||
echo "Downloading install script from: $INSTALL_SCRIPT_URL"
|
||||
curl -f -o install.sh "$INSTALL_SCRIPT_URL"
|
||||
echo "🔧 Testing admin password reset flow..."
|
||||
|
||||
- name: Create .env file with custom SMTP port
|
||||
run: echo "SMTP_PORT=2525" > .env
|
||||
# Run the reset password script with auto-confirm
|
||||
echo "Running reset-admin-password command..."
|
||||
password_output=$(docker exec aliasvault-test aliasvault reset-admin-password -y 2>&1)
|
||||
echo "Script output:"
|
||||
echo "$password_output"
|
||||
|
||||
- name: Set permissions and run install.sh (install)
|
||||
id: install_script
|
||||
run: |
|
||||
chmod +x install.sh
|
||||
{
|
||||
./install.sh install --verbose
|
||||
exit_code=$?
|
||||
if [ $exit_code -eq 2 ]; then
|
||||
echo "skip_remaining=true" >> $GITHUB_OUTPUT
|
||||
true
|
||||
elif [ $exit_code -ne 0 ]; then
|
||||
false
|
||||
fi
|
||||
} || {
|
||||
if [ $exit_code -eq 2 ]; then
|
||||
echo "skip_remaining=true" >> $GITHUB_OUTPUT
|
||||
true
|
||||
else
|
||||
exit $exit_code
|
||||
fi
|
||||
}
|
||||
# Extract the generated password from the output
|
||||
generated_password=$(echo "$password_output" | grep -E "^Password: " | sed 's/Password: //')
|
||||
if [ -z "$generated_password" ]; then
|
||||
echo "❌ Failed to extract generated password from script output"
|
||||
echo "Full output was:"
|
||||
echo "$password_output"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Generated password extracted: $generated_password"
|
||||
|
||||
- name: Run docker compose up
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
run: docker compose -f docker-compose.yml up -d
|
||||
# Verify that the admin_password_hash file now exists in the container
|
||||
if ! docker exec aliasvault-test test -f /secrets/admin_password_hash; then
|
||||
echo "❌ Admin password hash file was not created in container"
|
||||
docker exec aliasvault-test ls -la /secrets/
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Admin password hash file created in container"
|
||||
|
||||
- name: Wait for services
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
run: sleep 10
|
||||
# Verify that the admin_password_hash file exists locally (mounted volume)
|
||||
if [ ! -f "./secrets/admin_password_hash" ]; then
|
||||
echo "❌ Admin password hash file not found in local secrets folder"
|
||||
ls -la ./secrets/
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Admin password hash file exists in local secrets folder"
|
||||
|
||||
- name: Test WASM App
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "WASM app failed with $http_code"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Test WebApi
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443/api)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "WebApi failed with $http_code"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Test Admin App
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443/admin/user/login)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "Admin app failed with $http_code"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Test SMTP
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
- name: Test SMTP port
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
if ! nc -zv localhost 2525 2>&1 | grep -q 'succeeded'; then
|
||||
echo "SMTP failed"
|
||||
echo "❌ SMTP port 2525 is not accessible"
|
||||
docker logs aliasvault-test
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ SMTP port 2525 is accessible"
|
||||
|
||||
- name: Test reset-admin-password output
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
output=$(./install.sh reset-admin-password | sed 's/\x1b\[[0-9;]*m//g')
|
||||
if ! echo "$output" | grep -Eq '^\s*Password: [A-Za-z0-9+/=]{8,}'; then
|
||||
echo "Invalid reset-admin-password output"
|
||||
exit 1
|
||||
fi
|
||||
docker stop aliasvault-test || true
|
||||
docker rm aliasvault-test || true
|
||||
|
||||
docker-compose-build:
|
||||
name: Docker Compose Build Test
|
||||
@@ -163,10 +174,10 @@ jobs:
|
||||
- name: Check local docker-compose.yml for :latest tags
|
||||
run: |
|
||||
# Check for explicit version tags instead of :latest
|
||||
if grep -E "ghcr\.io/lanedirt/aliasvault-[^:]+:[0-9]+\.[0-9]+\.[0-9]+" docker-compose.yml; then
|
||||
if grep -E "ghcr\.io/aliasvault/[^:]+:[0-9]+\.[0-9]+\.[0-9]+" docker-compose.yml; then
|
||||
echo "❌ Error: docker-compose.yml contains explicit version tags instead of :latest"
|
||||
echo "Found the following explicit versions:"
|
||||
grep -E "ghcr\.io/lanedirt/aliasvault-[^:]+:[0-9]+\.[0-9]+\.[0-9]+" docker-compose.yml
|
||||
grep -E "ghcr\.io/aliasvault/[^:]+:[0-9]+\.[0-9]+\.[0-9]+" docker-compose.yml
|
||||
echo ""
|
||||
echo "All AliasVault images in docker-compose.yml must use ':latest' tags, not explicit versions."
|
||||
echo "Please update docker-compose.yml to use ':latest' for all AliasVault images."
|
||||
|
||||
316
.github/workflows/release.yml
vendored
316
.github/workflows/release.yml
vendored
@@ -4,10 +4,27 @@ on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
inputs:
|
||||
build_browser_extensions:
|
||||
description: 'Build browser extensions'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
build_mobile_apps:
|
||||
description: 'Build mobile apps'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
build_multi_container:
|
||||
description: 'Build and push multi-container images'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
build_all_in_one:
|
||||
description: 'Build and push all-in-one image'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
upload-install-script:
|
||||
@@ -19,12 +36,14 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Upload install.sh to release
|
||||
if: github.event_name == 'release'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: install.sh
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build-chrome-extension:
|
||||
if: github.event_name == 'release' || inputs.build_browser_extensions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -34,11 +53,12 @@ jobs:
|
||||
uses: ./.github/actions/build-browser-extension
|
||||
with:
|
||||
browser: chrome
|
||||
upload_to_release: true
|
||||
upload_to_release: ${{ github.event_name == 'release' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build-firefox-extension:
|
||||
if: github.event_name == 'release' || inputs.build_browser_extensions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -48,11 +68,12 @@ jobs:
|
||||
uses: ./.github/actions/build-browser-extension
|
||||
with:
|
||||
browser: firefox
|
||||
upload_to_release: true
|
||||
upload_to_release: ${{ github.event_name == 'release' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build-edge-extension:
|
||||
if: github.event_name == 'release' || inputs.build_browser_extensions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -62,11 +83,12 @@ jobs:
|
||||
uses: ./.github/actions/build-browser-extension
|
||||
with:
|
||||
browser: edge
|
||||
upload_to_release: true
|
||||
upload_to_release: ${{ github.event_name == 'release' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build-android-release:
|
||||
if: github.event_name == 'release' || inputs.build_mobile_apps
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -76,7 +98,7 @@ jobs:
|
||||
uses: ./.github/actions/build-android-app
|
||||
with:
|
||||
signed: true
|
||||
upload_to_release: true
|
||||
upload_to_release: ${{ github.event_name == 'release' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
@@ -84,7 +106,8 @@ jobs:
|
||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
|
||||
build-and-push-docker:
|
||||
build-and-push-docker-multi-container:
|
||||
if: github.event_name == 'release' || inputs.build_multi_container
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -100,91 +123,316 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
run: |
|
||||
echo "REPO_LOWER=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
- name: Extract metadata for Postgres image
|
||||
id: postgres-meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}
|
||||
images: ghcr.io/aliasvault/postgres
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault PostgreSQL
|
||||
org.opencontainers.image.description=PostgreSQL database for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
annotations: |
|
||||
org.opencontainers.image.description=PostgreSQL database for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
|
||||
- name: Extract metadata for API image
|
||||
id: api-meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: ghcr.io/aliasvault/api
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault API
|
||||
org.opencontainers.image.description=REST API backend for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
annotations: |
|
||||
org.opencontainers.image.description=REST API backend for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
|
||||
- name: Extract metadata for Client image
|
||||
id: client-meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: ghcr.io/aliasvault/client
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault Client
|
||||
org.opencontainers.image.description=Blazor WebAssembly client UI for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
annotations: |
|
||||
org.opencontainers.image.description=Blazor WebAssembly client UI for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
|
||||
- name: Extract metadata for Admin image
|
||||
id: admin-meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: ghcr.io/aliasvault/admin
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault Admin
|
||||
org.opencontainers.image.description=Admin portal for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
annotations: |
|
||||
org.opencontainers.image.description=Admin portal for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
|
||||
- name: Extract metadata for Reverse Proxy image
|
||||
id: reverse-proxy-meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: ghcr.io/aliasvault/reverse-proxy
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault Reverse Proxy
|
||||
org.opencontainers.image.description=Nginx reverse proxy for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
annotations: |
|
||||
org.opencontainers.image.description=Nginx reverse proxy for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
|
||||
- name: Extract metadata for SMTP image
|
||||
id: smtp-meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: ghcr.io/aliasvault/smtp
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault SMTP Service
|
||||
org.opencontainers.image.description=SMTP service for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
annotations: |
|
||||
org.opencontainers.image.description=SMTP service for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
|
||||
- name: Extract metadata for TaskRunner image
|
||||
id: task-runner-meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: ghcr.io/aliasvault/task-runner
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault TaskRunner
|
||||
org.opencontainers.image.description=Background task runner for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
annotations: |
|
||||
org.opencontainers.image.description=Background task runner for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
|
||||
|
||||
- name: Extract metadata for InstallCLI image
|
||||
id: installcli-meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: ghcr.io/aliasvault/installcli
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault Install CLI
|
||||
org.opencontainers.image.description=Installation and configuration CLI for AliasVault. Used by install.sh for setup and configuration, not deployed as part of the application stack
|
||||
annotations: |
|
||||
org.opencontainers.image.description=Installation and configuration CLI for AliasVault. Used by install.sh for setup and configuration, not deployed as part of the application stack
|
||||
|
||||
|
||||
- name: Build and push Postgres image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: apps/server/Databases/AliasServerDb/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-postgres:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-postgres:${{ github.ref_name }}
|
||||
tags: ${{ steps.postgres-meta.outputs.tags }}
|
||||
labels: ${{ steps.postgres-meta.outputs.labels }}
|
||||
annotations: ${{ steps.postgres-meta.outputs.annotations }}
|
||||
|
||||
- name: Build and push API image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: apps/server/AliasVault.Api/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-api:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-api:${{ github.ref_name }}
|
||||
tags: ${{ steps.api-meta.outputs.tags }}
|
||||
labels: ${{ steps.api-meta.outputs.labels }}
|
||||
annotations: ${{ steps.api-meta.outputs.annotations }}
|
||||
|
||||
- name: Build and push Client image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: apps/server/AliasVault.Client/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-client:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-client:${{ github.ref_name }}
|
||||
tags: ${{ steps.client-meta.outputs.tags }}
|
||||
labels: ${{ steps.client-meta.outputs.labels }}
|
||||
annotations: ${{ steps.client-meta.outputs.annotations }}
|
||||
|
||||
- name: Build and push Admin image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: apps/server/AliasVault.Admin/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-admin:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-admin:${{ github.ref_name }}
|
||||
tags: ${{ steps.admin-meta.outputs.tags }}
|
||||
labels: ${{ steps.admin-meta.outputs.labels }}
|
||||
annotations: ${{ steps.admin-meta.outputs.annotations }}
|
||||
|
||||
- name: Build and push Reverse Proxy image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: apps/server/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-reverse-proxy:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-reverse-proxy:${{ github.ref_name }}
|
||||
tags: ${{ steps.reverse-proxy-meta.outputs.tags }}
|
||||
labels: ${{ steps.reverse-proxy-meta.outputs.labels }}
|
||||
annotations: ${{ steps.reverse-proxy-meta.outputs.annotations }}
|
||||
|
||||
- name: Build and push SMTP image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: apps/server/Services/AliasVault.SmtpService/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-smtp:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-smtp:${{ github.ref_name }}
|
||||
tags: ${{ steps.smtp-meta.outputs.tags }}
|
||||
labels: ${{ steps.smtp-meta.outputs.labels }}
|
||||
annotations: ${{ steps.smtp-meta.outputs.annotations }}
|
||||
|
||||
- name: Build and push TaskRunner image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: apps/server/Services/AliasVault.TaskRunner/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-task-runner:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-task-runner:${{ github.ref_name }}
|
||||
tags: ${{ steps.task-runner-meta.outputs.tags }}
|
||||
labels: ${{ steps.task-runner-meta.outputs.labels }}
|
||||
annotations: ${{ steps.task-runner-meta.outputs.annotations }}
|
||||
|
||||
- name: Build and push InstallCli image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: apps/server/Utilities/AliasVault.InstallCli/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:${{ github.ref_name }}
|
||||
tags: ${{ steps.installcli-meta.outputs.tags }}
|
||||
labels: ${{ steps.installcli-meta.outputs.labels }}
|
||||
annotations: ${{ steps.installcli-meta.outputs.annotations }}
|
||||
|
||||
build-and-push-docker-all-in-one:
|
||||
if: github.event_name == 'release' || inputs.build_all_in_one
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for all-in-one image
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/aliasvault/aliasvault
|
||||
aliasvault/aliasvault
|
||||
tags: |
|
||||
# For release events with latest tag (only for non-prerelease)
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
|
||||
# semver tags for releases (works for prerelease and normal release)
|
||||
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
|
||||
# For tags, use tag name
|
||||
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
|
||||
# For branches, use branch name and branch name + short SHA for uniqueness
|
||||
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
|
||||
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=AliasVault All-in-One
|
||||
org.opencontainers.image.description=Self-contained AliasVault server including web app, with all services bundled using s6-overlay. Single container solution for easy deployment (see docs.aliasvault.net).
|
||||
annotations: |
|
||||
org.opencontainers.image.description=Self-contained AliasVault server including web app, with all services bundled using s6-overlay. Single container solution for easy deployment (see docs.aliasvault.net).
|
||||
|
||||
- name: Build and push all-in-one image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: dockerfiles/all-in-one/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
|
||||
75
.github/workflows/sonarcloud-code-analysis.yml
vendored
75
.github/workflows/sonarcloud-code-analysis.yml
vendored
@@ -1,75 +0,0 @@
|
||||
# This workflow will perform a SonarCloud code analysis on every push to the main branch or
|
||||
# when a pull request is opened, synchronized, or reopened. The "pull_request_target" event is
|
||||
# used to ensure that the analysis is done on the source branch of the pull request which has
|
||||
# access to the SonarCloud token secret.
|
||||
name: SonarCloud code analysis
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and analyze
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Install WASM workload
|
||||
run: dotnet workload install wasm-tools
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'zulu'
|
||||
|
||||
- name: Checkout code of PR branch
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~\sonar\cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
restore-keys: ${{ runner.os }}-sonar
|
||||
|
||||
- name: Cache SonarCloud scanner
|
||||
id: cache-sonar-scanner
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .\.sonar\scanner
|
||||
key: ${{ runner.os }}-sonar-scanner
|
||||
restore-keys: ${{ runner.os }}-sonar-scanner
|
||||
|
||||
- name: Install SonarCloud scanner
|
||||
if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
|
||||
shell: powershell
|
||||
run: |
|
||||
New-Item -Path .\.sonar\scanner -ItemType Directory
|
||||
dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
|
||||
|
||||
- name: Build and analyze
|
||||
working-directory: apps/server
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
shell: powershell
|
||||
run: |
|
||||
$scanner = "${{ github.workspace }}\.sonar\scanner\dotnet-sonarscanner"
|
||||
if ('${{ github.event_name }}' -eq 'pull_request_target') {
|
||||
& $scanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.pullrequest.key=${{ github.event.pull_request.number }} /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs" /d:sonar.exclusions="**/__tests__/test-forms/*.html,**/dist/shared/**,**/*.sql"
|
||||
} else {
|
||||
& $scanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs" /d:sonar.exclusions="**/__tests__/test-forms/*.html,**/dist/shared/**,**/*.sql"
|
||||
}
|
||||
dotnet build
|
||||
dotnet test -c Release /p:CollectCoverage=true /p:CoverletOutput=coverage /p:CoverletOutputFormat=opencover --filter 'FullyQualifiedName!~AliasVault.E2ETests'
|
||||
& $scanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -404,8 +404,12 @@ certificates/**/*.crt
|
||||
certificates/**/*.key
|
||||
certificates/**/*.pfx
|
||||
certificates/**/*.pem
|
||||
certificates/**/.hostname_marker
|
||||
certificates/letsencrypt/**
|
||||
|
||||
# Secrets
|
||||
secrets/**
|
||||
|
||||
# Docs
|
||||
docs/_site
|
||||
docs/vendor
|
||||
|
||||
2
.vscode/tasks.json
vendored
2
.vscode/tasks.json
vendored
@@ -199,7 +199,7 @@
|
||||
{
|
||||
"label": "Build and watch Docs",
|
||||
"type": "shell",
|
||||
"command": "docker compose up",
|
||||
"command": "docker compose -f docker-compose.dev.yml build && docker compose -f docker-compose.dev.yml up",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
|
||||
41
README.md
41
README.md
@@ -1,15 +1,16 @@
|
||||
# <img src="https://github.com/user-attachments/assets/933c8b45-a190-4df6-913e-b7c64ad9938b" width="35" alt="AliasVault"> AliasVault
|
||||
AliasVault is a privacy-first password and email alias manager. Create unique identities, strong passwords, and random email aliases for every website you use. Fully end-to-end encrypted, with a built-in email server and zero third-party dependencies.
|
||||
|
||||
[<img src="https://img.shields.io/github/v/release/lanedirt/AliasVault?include_prereleases&logo=github&label=Release">](https://github.com/lanedirt/AliasVault/releases)
|
||||
[](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-e2e-tests.yml)
|
||||
[<img src="https://img.shields.io/sonar/quality_gate/lanedirt_AliasVault?server=https%3A%2F%2Fsonarcloud.io&label=Sonarcloud&logo=sonarcloud">](https://sonarcloud.io/summary/new_code?id=lanedirt_AliasVault)
|
||||
[<img src="https://img.shields.io/github/v/release/aliasvault/aliasvault?include_prereleases&logo=github&label=Release">](https://github.com/aliasvault/aliasvault/releases)
|
||||
[](https://github.com/aliasvault/aliasvault/actions/workflows/dotnet-e2e-tests.yml)
|
||||
[<img src="https://badges.crowdin.net/aliasvault/localized.svg">](https://crowdin.com/project/aliasvault)
|
||||
[<img alt="Discord" src="https://img.shields.io/discord/1309300619026235422?logo=discord&logoColor=%237289da&label=Discord&color=%237289da">](https://discord.gg/DsaXMTEtpF)
|
||||
|
||||
<a href="https://app.aliasvault.net">Try the cloud version 🔥</a> | <a href="https://aliasvault.net?utm_source=gh-readme">Website </a> | <a href="https://docs.aliasvault.net?utm_source=gh-readme">Documentation </a> | <a href="#self-hosting">Self-host instructions</a>
|
||||
|
||||
⭐ Star us on GitHub, it motivates us a lot!
|
||||
**⭐ Star us on GitHub, it motivates us a lot!**
|
||||
|
||||
If you enjoy using AliasVault, please also consider leaving a review on our apps or browser extensions, and share it with your friends or colleagues. Your support helps others discover a privacy-first alternative to traditional & closed-source password managers.
|
||||
|
||||
## About
|
||||
Built on 15 years of experience, AliasVault is independent, open-source, self-hostable and community-driven. It’s the response to a web that tracks everything: a way to take back control of your digital privacy and help you stay secure online.
|
||||
@@ -64,33 +65,31 @@ AliasVault is available on:
|
||||
[<img width="700" alt="Screenshot of AliasVault" src="docs/assets/img/screenshot.png">](https://app.aliasvault.net)
|
||||
|
||||
## Self-hosting
|
||||
For full control over your own data you can self-host and install AliasVault on your own servers.
|
||||
> [!NOTE]
|
||||
> **Requirements:** 1 vCPU, 1GB RAM, 16GB disk, Docker ≥ 20.10, 64-bit Linux
|
||||
|
||||
### Install using install script
|
||||
AliasVault can be self-hosted on your own servers using two different installation methods. Both use Docker, but they differ in how much is automated versus how much you manage yourself.
|
||||
|
||||
This method uses pre-built Docker images and works on minimal hardware specifications:
|
||||
- **Option 1: Install Script** - Managed solution with automatic SSL (recommended for VPS/cloud)
|
||||
|
||||
- 64-bit Linux VM (Ubuntu/AlmaLinux) or Raspberry Pi, with root access
|
||||
- Minimum: 1 vCPU, 1GB RAM, 16GB disk
|
||||
- Docker ≥ 20.10 and Docker Compose ≥ 2.0
|
||||
- **Option 2: Docker Compose** - Single container with manual setup for use with existing SSL infrastructure (NAS, homelab)
|
||||
|
||||
### Quick Start (Install Script)
|
||||
```bash
|
||||
# Download install script from latest stable release
|
||||
curl -L -o install.sh https://github.com/lanedirt/AliasVault/releases/latest/download/install.sh
|
||||
# Download and run install script
|
||||
curl -L -o install.sh https://github.com/aliasvault/aliasvault/releases/latest/download/install.sh
|
||||
|
||||
# Make install script executable and run it. This will create the .env file, pull the Docker images, and start the AliasVault containers.
|
||||
chmod +x install.sh
|
||||
|
||||
./install.sh install
|
||||
```
|
||||
|
||||
The install script will output the URL where the app is available. By default this is:
|
||||
- Client: https://localhost
|
||||
- Admin portal: https://localhost/admin
|
||||
For other installation methods and more detailed steps, please read the [full installation guide](https://docs.aliasvault.net/installation) in the official docs.
|
||||
|
||||
> Note: If you want to change the default AliasVault ports you can do so in the `.env` file.
|
||||
|
||||
## Technical documentation
|
||||
## Documentation
|
||||
For more information about the installation process, manual setup instructions and other topics, please see the official documentation website:
|
||||
|
||||
- [Documentation website (docs.aliasvault.net) 📚](https://docs.aliasvault.net)
|
||||
|
||||
## Security Architecture
|
||||
@@ -131,12 +130,12 @@ Core features that are being worked on:
|
||||
- [ ] Support for FIDO2/WebAuthn hardware keys and passkeys
|
||||
- [ ] Adding support for family/team sharing (organization features)
|
||||
|
||||
👉 [View the full AliasVault roadmap here](https://github.com/lanedirt/AliasVault/issues/731)
|
||||
👉 [View the full AliasVault roadmap here](https://github.com/aliasvault/aliasvault/issues/731)
|
||||
|
||||
### Got feedback or ideas?
|
||||
Feel free to open an issue or join our [Discord](https://discord.gg/DsaXMTEtpF)! Contributions are warmly welcomed—whether in feature development, testing, or spreading the word. Get in touch on Discord if you're interested in contributing.
|
||||
Feel free to open an issue or discussion on GitHub. We warmly welcome all contributions: whether it’s translating, testing, helping to build features, sharing feedback - or helping spread the word about AliasVault. Every bit of support helps the project grow, so don’t hesitate to jump in and [say hi to us on Discord](https://discord.gg/DsaXMTEtpF)!
|
||||
|
||||
### Support the mission
|
||||
Your donation helps me dedicate more time and resources to improving AliasVault, making the internet safer for everyone!
|
||||
AliasVault is open-source and community-driven. If you like what we’re building, consider supporting us through [Open Collective](https://opencollective.com/aliasvault) or through:
|
||||
|
||||
<a href="https://www.buymeacoffee.com/lanedirt" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 50px !important;" ></a>
|
||||
|
||||
17
SECURITY.txt
Normal file
17
SECURITY.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
Contact: mailto:security@support.aliasvault.net
|
||||
Expires: 2026-09-16T12:00:00.000Z
|
||||
Preferred-Languages: en
|
||||
Canonical: https://raw.githubusercontent.com/aliasvault/aliasvault/main/SECURITY.txt
|
||||
|
||||
# Security Policy for AliasVault
|
||||
#
|
||||
# We take security seriously and appreciate responsible disclosure of vulnerabilities.
|
||||
# Please report security issues to the email above rather than opening public issues.
|
||||
#
|
||||
# Include the following information in your report:
|
||||
# - Description of the vulnerability
|
||||
# - Steps to reproduce
|
||||
# - Potential impact
|
||||
# - Suggested remediation (if any)
|
||||
#
|
||||
# We will acknowledge receipt within 48 hours and provide updates as we investigate.
|
||||
@@ -1,13 +0,0 @@
|
||||
<SonarLint>
|
||||
<Rules>
|
||||
<Rule>
|
||||
<Key>S1135</Key>
|
||||
<Parameters>
|
||||
<Parameter>
|
||||
<Name>sonarlint.rule.enabled</Name>
|
||||
<Value>false</Value>
|
||||
</Parameter>
|
||||
</Parameters>
|
||||
</Rule>
|
||||
</Rules>
|
||||
</SonarLint>
|
||||
18
apps/browser-extension/package-lock.json
generated
18
apps/browser-extension/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "aliasvault-browser-extension",
|
||||
"version": "0.20.2",
|
||||
"version": "0.22.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aliasvault-browser-extension",
|
||||
"version": "0.20.2",
|
||||
"version": "0.22.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
@@ -49,7 +49,7 @@
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.6.3",
|
||||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"vite-plugin-static-copy": "^2.3.2",
|
||||
"wxt": "^0.20.6"
|
||||
}
|
||||
},
|
||||
@@ -13064,9 +13064,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"version": "6.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
@@ -13160,9 +13160,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-static-copy": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.1.tgz",
|
||||
"integrity": "sha512-EfsPcBm3ewg3UMG8RJaC0ADq6/qnUZnokXx4By4+2cAcipjT9i0Y0owIJGqmZI7d6nxk4qB1q5aXOwNuSyPdyA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.2.tgz",
|
||||
"integrity": "sha512-iwrrf+JupY4b9stBttRWzGHzZbeMjAHBhkrn67MNACXJVjEMRpCI10Q3AkxdBkl45IHaTfw/CNVevzQhP7yTwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "aliasvault-browser-extension",
|
||||
"description": "AliasVault Browser Extension",
|
||||
"private": true,
|
||||
"version": "0.21.2",
|
||||
"version": "0.23.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:chrome": "wxt -b chrome",
|
||||
@@ -66,7 +66,7 @@
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.6.3",
|
||||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"vite-plugin-static-copy": "^2.3.2",
|
||||
"wxt": "^0.20.6"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 174 KiB |
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg enable-background="new 0 0 500 500" version="1.1" viewBox="0 0 500 500" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m459.87 294.95c0.016205 5.4005 0.03241 10.801-0.35022 16.873-1.111 6.3392-1.1941 12.173-2.6351 17.649-10.922 41.508-36.731 69.481-77.351 83.408-7.2157 2.4739-14.972 3.3702-22.479 4.995-23.629 0.042205-47.257 0.11453-70.886 0.12027-46.762 0.011322-93.523-0.01416-140.95-0.43411-8.59-2.0024-16.766-2.8352-24.398-5.3326-21.595-7.0666-39.523-19.656-53.708-37.552-10.227-12.903-17.579-27.17-21.28-43.221-1.475-6.3967-2.4711-12.904-3.6852-19.361-0.051849-5.747-0.1037-11.494 0.26915-17.886 4.159-42.973 27.68-71.638 63.562-92.153 0-0.70761-0.001961-1.6988 3.12e-4 -2.69 0.022484-9.8293-1.3071-19.894 0.35664-29.438 3.2391-18.579 11.08-35.272 23.763-49.773 12.098-13.832 26.457-23.989 43.609-30.029 7.813-2.7512 16.14-4.0417 24.234-5.9948 7.392-0.025734 14.784-0.05146 22.835 0.32253 4.1959 0.95392 7.7946 1.2538 11.258 2.1053 17.16 4.2192 32.287 12.176 45.469 24.104 2.2558 2.0411 4.372 6.6241 9.621 3.868 16.839-8.8419 34.718-11.597 53.603-8.594 16.791 2.6699 31.602 9.4308 44.236 20.636 11.531 10.227 19.84 22.841 25.393 37.236 6.3436 16.445 10.389 33.163 6.0798 49.389 7.9587 8.9321 15.807 16.704 22.421 25.414 9.162 12.065 15.33 25.746 18.144 40.776 0.97046 5.1848 1.9111 10.375 2.8654 15.563m-71.597 71.012c5.5615-5.2284 12.002-9.7986 16.508-15.817 10.474-13.992 14.333-29.916 11.288-47.446-2.2496-12.95-8.1973-24.076-17.243-33.063-12.746-12.663-28.865-18.614-46.786-18.569-69.912 0.17712-139.82 0.56831-209.74 0.96176-15.922 0.089599-29.168 7.4209-39.685 18.296-14.45 14.944-20.408 33.343-16.655 54.368 2.2763 12.754 8.2167 23.748 17.158 32.66 13.299 13.255 30.097 18.653 48.728 18.651 59.321-0.005188 118.64 0.042358 177.96-0.046601 9.5912-0.014374 19.181-0.86588 28.773-0.88855 10.649-0.025146 19.978-3.825 29.687-9.1074z" fill="#EEC170"/>
|
||||
<path d="m162.77 293c15.654 4.3883 20.627 22.967 10.304 34.98-5.3104 6.1795-14.817 8.3208-24.278 5.0472-7.0723-2.4471-12.332-10.362-12.876-17.933-1.0451-14.542 11.089-23.176 21.705-23.046 1.5794 0.019287 3.1517 0.61566 5.1461 0.95184z" fill="#EEC170"/>
|
||||
<path d="m227.18 293.64c7.8499 2.3973 11.938 8.2143 13.524 15.077 1.8591 8.0439-0.44817 15.706-7.1588 21.121-6.7633 5.4572-14.417 6.8794-22.578 3.1483-8.2972-3.7933-12.836-10.849-12.736-19.438 0.1687-14.497 14.13-25.368 28.948-19.908z" fill="#EEC170"/>
|
||||
<path d="m261.57 319.07c-2.495-14.418 4.6853-22.603 14.596-26.108 9.8945-3.4995 23.181 3.4303 26.267 13.779 4.6504 15.591-7.1651 29.064-21.665 28.161-8.5254-0.53088-17.202-6.5094-19.198-15.831z" fill="#EEC170"/>
|
||||
<path d="m336.91 333.41c-9.0175-4.2491-15.337-14.349-13.829-21.682 3.0825-14.989 13.341-20.304 23.018-19.585 10.653 0.79141 17.93 7.407 19.765 17.547 1.9588 10.824-4.1171 19.939-13.494 23.703-5.272 2.1162-10.091 1.5086-15.46 0.017883z" fill="#EEC170"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
12
apps/browser-extension/public/offscreen.html
Normal file
12
apps/browser-extension/public/offscreen.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>AliasVault Offscreen Document</title>
|
||||
<!-- The offscreen.html is used for clipboard clearing. It is a hidden document that runs in a hidden context with access to clipboard operations. -->
|
||||
</head>
|
||||
<body>
|
||||
<textarea id="text"></textarea>
|
||||
<script src="offscreen.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
37
apps/browser-extension/public/offscreen.js
Normal file
37
apps/browser-extension/public/offscreen.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Offscreen document for clipboard operations.
|
||||
* This document runs in a hidden context with access to clipboard operations.
|
||||
*/
|
||||
|
||||
// Listen for messages from the service worker
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'CLEAR_CLIPBOARD') {
|
||||
clearClipboard()
|
||||
.then(() => {
|
||||
sendResponse({ success: true, message: 'Clipboard cleared successfully' });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[OFFSCREEN] Failed to clear clipboard:', error);
|
||||
sendResponse({ success: false, message: error.message });
|
||||
});
|
||||
// Return true to indicate we'll send response asynchronously
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
const textEl = document.querySelector('#text');
|
||||
|
||||
/**
|
||||
* Clear the clipboard by writing a space using execCommand.
|
||||
*/
|
||||
async function clearClipboard() {
|
||||
try {
|
||||
// Use execCommand to clear clipboard
|
||||
textEl.value = '\n';
|
||||
textEl.select();
|
||||
document.execCommand('copy');
|
||||
} catch (error) {
|
||||
console.error('[OFFSCREEN] Error clearing clipboard:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -447,7 +447,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -460,7 +460,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.21.2;
|
||||
MARKETING_VERSION = 0.23.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -479,7 +479,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -492,7 +492,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.21.2;
|
||||
MARKETING_VERSION = 0.23.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -515,7 +515,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -530,7 +530,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.21.2;
|
||||
MARKETING_VERSION = 0.23.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -554,7 +554,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
CURRENT_PROJECT_VERSION = 230200;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -569,7 +569,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.21.2;
|
||||
MARKETING_VERSION = 0.23.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { onMessage, sendMessage } from "webext-bridge/background";
|
||||
|
||||
import { handleResetAutoLockTimer, handlePopupHeartbeat, handleSetAutoLockTimeout } from '@/entrypoints/background/AutolockTimeoutHandler';
|
||||
import { handleClipboardCopied, handleCancelClipboardClear, handleGetClipboardClearTimeout, handleSetClipboardClearTimeout, handleGetClipboardCountdownState } from '@/entrypoints/background/ClipboardClearHandler';
|
||||
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
|
||||
import { handleOpenPopup, handlePopupWithCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler';
|
||||
import { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentitySettings, handleGetDerivedKey, handleGetPasswordSettings, handleGetPersistedFormValues, handleGetVault, handlePersistFormValues, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler';
|
||||
import { handleOpenPopup, handlePopupWithCredential, handleOpenPopupCreateCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler';
|
||||
import { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentitySettings, handleGetEncryptionKey, handleGetEncryptionKeyDerivationParams, handleGetPasswordSettings, handleGetPersistedFormValues, handleGetVault, handlePersistFormValues, handleStoreEncryptionKey, handleStoreEncryptionKeyDerivationParams, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler';
|
||||
|
||||
import { GLOBAL_CONTEXT_MENU_ENABLED_KEY } from '@/utils/Constants';
|
||||
import type { EncryptionKeyDerivationParams } from '@/utils/dist/shared/models/metadata';
|
||||
|
||||
import { defineBackground, storage, browser } from '#imports';
|
||||
|
||||
@@ -15,25 +18,50 @@ export default defineBackground({
|
||||
async main() {
|
||||
// Listen for messages using webext-bridge
|
||||
onMessage('CHECK_AUTH_STATUS', () => handleCheckAuthStatus());
|
||||
onMessage('STORE_VAULT', ({ data }) => handleStoreVault(data));
|
||||
onMessage('UPLOAD_VAULT', ({ data }) => handleUploadVault(data));
|
||||
onMessage('SYNC_VAULT', () => handleSyncVault());
|
||||
|
||||
onMessage('GET_ENCRYPTION_KEY', () => handleGetEncryptionKey());
|
||||
onMessage('GET_ENCRYPTION_KEY_DERIVATION_PARAMS', () => handleGetEncryptionKeyDerivationParams());
|
||||
onMessage('GET_VAULT', () => handleGetVault());
|
||||
onMessage('CLEAR_VAULT', () => handleClearVault());
|
||||
onMessage('GET_CREDENTIALS', () => handleGetCredentials());
|
||||
onMessage('CREATE_IDENTITY', ({ data }) => handleCreateIdentity(data));
|
||||
|
||||
onMessage('GET_DEFAULT_EMAIL_DOMAIN', () => handleGetDefaultEmailDomain());
|
||||
onMessage('GET_DEFAULT_IDENTITY_SETTINGS', () => handleGetDefaultIdentitySettings());
|
||||
onMessage('GET_PASSWORD_SETTINGS', () => handleGetPasswordSettings());
|
||||
onMessage('GET_DERIVED_KEY', () => handleGetDerivedKey());
|
||||
|
||||
onMessage('STORE_VAULT', ({ data }) => handleStoreVault(data));
|
||||
onMessage('STORE_ENCRYPTION_KEY', ({ data }) => handleStoreEncryptionKey(data as string));
|
||||
onMessage('STORE_ENCRYPTION_KEY_DERIVATION_PARAMS', ({ data }) => handleStoreEncryptionKeyDerivationParams(data as EncryptionKeyDerivationParams));
|
||||
|
||||
onMessage('CREATE_IDENTITY', ({ data }) => handleCreateIdentity(data));
|
||||
onMessage('UPLOAD_VAULT', ({ data }) => handleUploadVault(data));
|
||||
onMessage('SYNC_VAULT', () => handleSyncVault());
|
||||
|
||||
onMessage('CLEAR_VAULT', () => handleClearVault());
|
||||
|
||||
onMessage('OPEN_POPUP', () => handleOpenPopup());
|
||||
onMessage('OPEN_POPUP_WITH_CREDENTIAL', ({ data }) => handlePopupWithCredential(data));
|
||||
onMessage('OPEN_POPUP_CREATE_CREDENTIAL', ({ data }) => handleOpenPopupCreateCredential(data));
|
||||
onMessage('TOGGLE_CONTEXT_MENU', ({ data }) => handleToggleContextMenu(data));
|
||||
|
||||
onMessage('PERSIST_FORM_VALUES', ({ data }) => handlePersistFormValues(data));
|
||||
onMessage('GET_PERSISTED_FORM_VALUES', () => handleGetPersistedFormValues());
|
||||
onMessage('CLEAR_PERSISTED_FORM_VALUES', () => handleClearPersistedFormValues());
|
||||
|
||||
// Clipboard management messages
|
||||
onMessage('CLIPBOARD_COPIED', () => handleClipboardCopied());
|
||||
onMessage('CANCEL_CLIPBOARD_CLEAR', () => handleCancelClipboardClear());
|
||||
onMessage('GET_CLIPBOARD_CLEAR_TIMEOUT', () => handleGetClipboardClearTimeout());
|
||||
onMessage('SET_CLIPBOARD_CLEAR_TIMEOUT', ({ data }) => handleSetClipboardClearTimeout(data as number));
|
||||
onMessage('GET_CLIPBOARD_COUNTDOWN_STATE', () => handleGetClipboardCountdownState());
|
||||
|
||||
// Auto-lock management messages
|
||||
onMessage('RESET_AUTO_LOCK_TIMER', () => handleResetAutoLockTimer());
|
||||
onMessage('SET_AUTO_LOCK_TIMEOUT', ({ data }) => handleSetAutoLockTimeout(data as number));
|
||||
onMessage('POPUP_HEARTBEAT', () => handlePopupHeartbeat());
|
||||
|
||||
// Handle clipboard copied from context menu
|
||||
onMessage('CLIPBOARD_COPIED_FROM_CONTEXT', () => handleClipboardCopied());
|
||||
|
||||
// Setup context menus
|
||||
const isContextMenuEnabled = await storage.getItem(GLOBAL_CONTEXT_MENU_ENABLED_KEY) ?? true;
|
||||
if (isContextMenuEnabled) {
|
||||
@@ -77,4 +105,4 @@ function getActiveElementIdentifier() : string {
|
||||
return target.id || target.name || '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { storage } from 'wxt/utils/storage';
|
||||
|
||||
import { handleClearVault } from '@/entrypoints/background/VaultMessageHandler';
|
||||
|
||||
import { AUTO_LOCK_TIMEOUT_KEY } from '@/utils/Constants';
|
||||
|
||||
let autoLockTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
/**
|
||||
* Reset the auto-lock timer.
|
||||
*/
|
||||
export function handleResetAutoLockTimer(): void {
|
||||
resetAutoLockTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle popup heartbeat - extend auto-lock timer.
|
||||
*/
|
||||
export function handlePopupHeartbeat(): void {
|
||||
extendAutoLockTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the auto-lock timeout setting.
|
||||
*/
|
||||
export async function handleSetAutoLockTimeout(timeout: number): Promise<boolean> {
|
||||
await storage.setItem(AUTO_LOCK_TIMEOUT_KEY, timeout);
|
||||
resetAutoLockTimer();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the auto-lock timer based on current settings.
|
||||
*/
|
||||
async function resetAutoLockTimer(): Promise<void> {
|
||||
// Clear existing timer
|
||||
if (autoLockTimer) {
|
||||
clearTimeout(autoLockTimer);
|
||||
autoLockTimer = null;
|
||||
}
|
||||
|
||||
// Get timeout setting
|
||||
const timeout = await storage.getItem(AUTO_LOCK_TIMEOUT_KEY) as number ?? 0;
|
||||
|
||||
// Don't set timer if timeout is 0 (disabled) or if vault is already locked
|
||||
if (timeout === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if vault is unlocked before setting timer
|
||||
const encryptionKey = await storage.getItem('session:encryptionKey') as string | null;
|
||||
|
||||
if (!encryptionKey) {
|
||||
// Vault is already locked, don't start timer
|
||||
return;
|
||||
}
|
||||
|
||||
// Set new timer
|
||||
autoLockTimer = setTimeout(async () => {
|
||||
try {
|
||||
// Lock the vault using the existing handler
|
||||
handleClearVault();
|
||||
|
||||
console.info('[AUTO_LOCK] Vault locked due to inactivity');
|
||||
autoLockTimer = null;
|
||||
} catch (error) {
|
||||
console.error('[AUTO_LOCK] Error locking vault:', error);
|
||||
}
|
||||
}, timeout * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the auto-lock timer by the full timeout period.
|
||||
* This is called by popup heartbeats to prevent locking while popup is active.
|
||||
*/
|
||||
async function extendAutoLockTimer(): Promise<void> {
|
||||
// Get timeout setting
|
||||
const timeout = await storage.getItem(AUTO_LOCK_TIMEOUT_KEY) as number ?? 0;
|
||||
|
||||
// Don't extend timer if timeout is 0 (disabled)
|
||||
if (timeout === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if vault is unlocked
|
||||
const encryptionKey = await storage.getItem('session:encryptionKey') as string | null;
|
||||
|
||||
if (!encryptionKey) {
|
||||
// Vault is already locked, don't extend timer
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing timer and start a new one
|
||||
if (autoLockTimer) {
|
||||
clearTimeout(autoLockTimer);
|
||||
autoLockTimer = null;
|
||||
}
|
||||
|
||||
// Set new timer
|
||||
autoLockTimer = setTimeout(async () => {
|
||||
try {
|
||||
// Lock the vault using the existing handler
|
||||
handleClearVault();
|
||||
|
||||
console.info('[AUTO_LOCK] Vault locked due to inactivity');
|
||||
autoLockTimer = null;
|
||||
} catch (error) {
|
||||
console.error('[AUTO_LOCK] Error locking vault:', error);
|
||||
}
|
||||
}, timeout * 1000);
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import { sendMessage } from 'webext-bridge/background';
|
||||
import { storage } from 'wxt/utils/storage';
|
||||
|
||||
import { CLIPBOARD_CLEAR_TIMEOUT_KEY } from '@/utils/Constants';
|
||||
|
||||
let clipboardClearTimer: NodeJS.Timeout | null = null;
|
||||
let countdownInterval: NodeJS.Timeout | null = null;
|
||||
let remainingTime = 0;
|
||||
let currentCountdownId = 0;
|
||||
let totalCountdownTime = 0;
|
||||
let countdownStartTime = 0;
|
||||
let offscreenDocumentCreated = false;
|
||||
|
||||
/**
|
||||
* Create offscreen document if it doesn't exist.
|
||||
*/
|
||||
async function createOffscreenDocument(): Promise<void> {
|
||||
if (offscreenDocumentCreated) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if chrome.offscreen API is available (Chrome 109+)
|
||||
if (!chrome.offscreen) {
|
||||
console.warn('[CLIPBOARD] Offscreen API not available, falling back to direct clipboard access');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if offscreen document already exists
|
||||
if (chrome.runtime.getContexts) {
|
||||
const existingContexts = await chrome.runtime.getContexts({
|
||||
contextTypes: ['OFFSCREEN_DOCUMENT'],
|
||||
documentUrls: [chrome.runtime.getURL('offscreen.html')]
|
||||
});
|
||||
|
||||
if (existingContexts.length > 0) {
|
||||
offscreenDocumentCreated = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create offscreen document
|
||||
await chrome.offscreen.createDocument({
|
||||
url: 'offscreen.html',
|
||||
reasons: [chrome.offscreen.Reason.CLIPBOARD],
|
||||
justification: 'Clear clipboard after timeout for security'
|
||||
});
|
||||
|
||||
offscreenDocumentCreated = true;
|
||||
} catch (error) {
|
||||
console.error('[CLIPBOARD] Failed to create offscreen document:', error);
|
||||
offscreenDocumentCreated = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear clipboard using offscreen document or fallback method.
|
||||
*/
|
||||
async function clearClipboardContent(): Promise<void> {
|
||||
if (import.meta.env.CHROME || import.meta.env.EDGE) {
|
||||
/*
|
||||
* Chrome and Edge use mv3 and do not have direct access to clipboard
|
||||
* so we use an offscreen document to clear the clipboard.
|
||||
*/
|
||||
await createOffscreenDocument();
|
||||
|
||||
// Send message to offscreen document to clear clipboard
|
||||
const response = await chrome.runtime.sendMessage({ type: 'CLEAR_CLIPBOARD' });
|
||||
|
||||
if (response?.success) {
|
||||
console.info('[CLIPBOARD] Clipboard cleared via offscreen document');
|
||||
} else {
|
||||
throw new Error(response?.message || 'Failed to clear clipboard via offscreen');
|
||||
}
|
||||
} else {
|
||||
// Firefox and Safari use mv2 and can use direct clipboard access.
|
||||
await navigator.clipboard.writeText('');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle clipboard copied event - starts countdown and timer to clear clipboard.
|
||||
*/
|
||||
export async function handleClipboardCopied() : Promise<void> {
|
||||
const timeout = await storage.getItem(CLIPBOARD_CLEAR_TIMEOUT_KEY) as number ?? 10;
|
||||
|
||||
// Clear any existing timer
|
||||
if (clipboardClearTimer) {
|
||||
clearTimeout(clipboardClearTimer);
|
||||
clipboardClearTimer = null;
|
||||
}
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval);
|
||||
countdownInterval = null;
|
||||
}
|
||||
|
||||
// Don't set timer if timeout is 0 (disabled)
|
||||
if (timeout === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate new countdown ID
|
||||
currentCountdownId = Date.now();
|
||||
const thisCountdownId = currentCountdownId;
|
||||
countdownStartTime = Date.now();
|
||||
totalCountdownTime = timeout;
|
||||
|
||||
remainingTime = timeout;
|
||||
|
||||
// Send initial countdown immediately with ID
|
||||
sendMessage('CLIPBOARD_COUNTDOWN', { remaining: remainingTime, total: timeout, id: thisCountdownId }, 'popup').catch(() => {});
|
||||
|
||||
// Send countdown updates to popup every 100ms for smooth animation
|
||||
let elapsed = 0;
|
||||
countdownInterval = setInterval(() => {
|
||||
// Check if this countdown is still active
|
||||
if (thisCountdownId !== currentCountdownId) {
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
elapsed += 0.1;
|
||||
remainingTime = Math.max(0, timeout - elapsed);
|
||||
sendMessage('CLIPBOARD_COUNTDOWN', { remaining: remainingTime, total: timeout, id: thisCountdownId }, 'popup').catch(() => {});
|
||||
|
||||
if (elapsed >= timeout && countdownInterval) {
|
||||
clearInterval(countdownInterval);
|
||||
countdownInterval = null;
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Set timer to clear clipboard
|
||||
clipboardClearTimer = setTimeout(async () => {
|
||||
try {
|
||||
// Clear clipboard using offscreen document or fallback
|
||||
await clearClipboardContent();
|
||||
|
||||
// Clean up regardless of success/failure
|
||||
clipboardClearTimer = null;
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval);
|
||||
countdownInterval = null;
|
||||
}
|
||||
|
||||
// Reset countdown tracking
|
||||
currentCountdownId = 0;
|
||||
countdownStartTime = 0;
|
||||
totalCountdownTime = 0;
|
||||
|
||||
sendMessage('CLIPBOARD_CLEARED', {}, 'popup').catch(() => {});
|
||||
} catch (error) {
|
||||
console.error('[CLIPBOARD] Error during clipboard clear:', error);
|
||||
|
||||
// Clean up even on error
|
||||
clipboardClearTimer = null;
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval);
|
||||
countdownInterval = null;
|
||||
}
|
||||
currentCountdownId = 0;
|
||||
countdownStartTime = 0;
|
||||
totalCountdownTime = 0;
|
||||
sendMessage('CLIPBOARD_CLEARED', {}, 'popup').catch(() => {});
|
||||
}
|
||||
}, timeout * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel clipboard clear countdown and timer.
|
||||
*/
|
||||
export function handleCancelClipboardClear(): void {
|
||||
if (clipboardClearTimer) {
|
||||
clearTimeout(clipboardClearTimer);
|
||||
clipboardClearTimer = null;
|
||||
}
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval);
|
||||
countdownInterval = null;
|
||||
}
|
||||
sendMessage('CLIPBOARD_COUNTDOWN_CANCELLED', {}, 'popup').catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the clipboard clear timeout setting.
|
||||
*/
|
||||
export async function handleGetClipboardClearTimeout(): Promise<number> {
|
||||
const timeout = await storage.getItem(CLIPBOARD_CLEAR_TIMEOUT_KEY) as number ?? 10;
|
||||
return timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the clipboard clear timeout setting.
|
||||
*/
|
||||
export async function handleSetClipboardClearTimeout(data: number): Promise<boolean> {
|
||||
await storage.setItem(CLIPBOARD_CLEAR_TIMEOUT_KEY, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current clipboard countdown state.
|
||||
*/
|
||||
export function handleGetClipboardCountdownState(): { remaining: number; total: number; id: number } | null {
|
||||
// Calculate actual remaining time based on elapsed time
|
||||
if (currentCountdownId && countdownStartTime && totalCountdownTime) {
|
||||
const elapsed = (Date.now() - countdownStartTime) / 1000;
|
||||
const actualRemaining = Math.max(0, totalCountdownTime - elapsed);
|
||||
|
||||
if (actualRemaining > 0) {
|
||||
return {
|
||||
remaining: actualRemaining,
|
||||
total: totalCountdownTime,
|
||||
id: currentCountdownId
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
|
||||
|
||||
import { SKIP_FORM_RESTORE_KEY } from '@/utils/Constants';
|
||||
import { BoolResponse } from '@/utils/types/messaging/BoolResponse';
|
||||
|
||||
import { browser } from '#imports';
|
||||
@@ -37,6 +38,53 @@ export function handlePopupWithCredential(message: any) : Promise<BoolResponse>
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle opening the popup on create credential page with prefilled service name.
|
||||
*/
|
||||
export function handleOpenPopupCreateCredential(message: any) : Promise<BoolResponse> {
|
||||
return (async () : Promise<BoolResponse> => {
|
||||
const serviceName = encodeURIComponent(message.serviceName || '');
|
||||
|
||||
// Use the URL passed from the content script (current page URL)
|
||||
let serviceUrl = '';
|
||||
if (message.currentUrl) {
|
||||
try {
|
||||
const url = new URL(message.currentUrl);
|
||||
// Only include http/https URLs
|
||||
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
||||
serviceUrl = encodeURIComponent(url.origin + url.pathname);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing current URL:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Set a localStorage flag to skip restoring previously persisted form values as we want to start fresh with this explicit create credential request.
|
||||
await browser.storage.local.set({ [SKIP_FORM_RESTORE_KEY]: true });
|
||||
|
||||
const urlParams = new URLSearchParams();
|
||||
urlParams.set('expanded', 'true');
|
||||
if (serviceName) {
|
||||
urlParams.set('serviceName', serviceName);
|
||||
}
|
||||
if (serviceUrl) {
|
||||
urlParams.set('serviceUrl', serviceUrl);
|
||||
}
|
||||
if (message.currentUrl) {
|
||||
urlParams.set('currentUrl', message.currentUrl);
|
||||
}
|
||||
|
||||
browser.windows.create({
|
||||
url: browser.runtime.getURL(`/popup.html?${urlParams.toString()}#/credentials/add`),
|
||||
type: 'popup',
|
||||
width: 400,
|
||||
height: 600,
|
||||
focused: true
|
||||
});
|
||||
return { success: true };
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle toggling the context menu.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { storage } from 'wxt/utils/storage';
|
||||
|
||||
import type { EncryptionKeyDerivationParams } from '@/utils/dist/shared/models/metadata';
|
||||
import type { Vault, VaultResponse, VaultPostResponse } from '@/utils/dist/shared/models/webapi';
|
||||
import { EncryptionUtility } from '@/utils/EncryptionUtility';
|
||||
import { SqliteClient } from '@/utils/SqliteClient';
|
||||
@@ -82,11 +83,6 @@ export async function handleStoreVault(
|
||||
* Some updates, e.g. when mutating local database, these values will not be set.
|
||||
*/
|
||||
|
||||
// Store derived key in session storage (if it has a value)
|
||||
if (vaultRequest.derivedKey) {
|
||||
await storage.setItem('session:derivedKey', vaultRequest.derivedKey);
|
||||
}
|
||||
|
||||
if (vaultRequest.publicEmailDomainList) {
|
||||
await storage.setItem('session:publicEmailDomains', vaultRequest.publicEmailDomainList);
|
||||
}
|
||||
@@ -106,6 +102,36 @@ export async function handleStoreVault(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the encryption key (derived key) in browser storage.
|
||||
*/
|
||||
export async function handleStoreEncryptionKey(
|
||||
encryptionKey: string,
|
||||
) : Promise<messageBoolResponse> {
|
||||
try {
|
||||
await storage.setItem('session:encryptionKey', encryptionKey);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to store encryption key:', error);
|
||||
return { success: false, error: await t('common.errors.failedToStoreEncryptionKey') };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the encryption key derivation parameters in browser storage.
|
||||
*/
|
||||
export async function handleStoreEncryptionKeyDerivationParams(
|
||||
params: EncryptionKeyDerivationParams,
|
||||
) : Promise<messageBoolResponse> {
|
||||
try {
|
||||
await storage.setItem('session:encryptionKeyDerivationParams', params);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to store encryption key derivation params:', error);
|
||||
return { success: false, error: await t('common.errors.failedToStoreEncryptionParams') };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the vault with the server to check if a newer vault is available. If so, the vault will be updated.
|
||||
*/
|
||||
@@ -141,8 +167,9 @@ export async function handleSyncVault(
|
||||
export async function handleGetVault(
|
||||
) : Promise<messageVaultResponse> {
|
||||
try {
|
||||
const encryptionKey = await handleGetEncryptionKey();
|
||||
|
||||
const encryptedVault = await storage.getItem('session:encryptedVault') as string;
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[];
|
||||
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[];
|
||||
const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number;
|
||||
@@ -152,9 +179,14 @@ export async function handleGetVault(
|
||||
return { success: false, error: await t('common.errors.vaultNotAvailable') };
|
||||
}
|
||||
|
||||
if (!encryptionKey) {
|
||||
console.error('Encryption key not available');
|
||||
return { success: false, error: await t('common.errors.vaultIsLocked') };
|
||||
}
|
||||
|
||||
const decryptedVault = await EncryptionUtility.symmetricDecrypt(
|
||||
encryptedVault,
|
||||
derivedKey
|
||||
encryptionKey
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -166,7 +198,7 @@ export async function handleGetVault(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to get vault:', error);
|
||||
return { success: false, error: await t('common.errors.failedToGetVault') };
|
||||
return { success: false, error: await t('common.errors.failedToRetrieveData') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +209,10 @@ export function handleClearVault(
|
||||
) : messageBoolResponse {
|
||||
storage.removeItems([
|
||||
'session:encryptedVault',
|
||||
'session:encryptionKey',
|
||||
// TODO: the derivedKey clear can be removed some period of time after 0.22.0 is released.
|
||||
'session:derivedKey',
|
||||
'session:encryptionKeyDerivationParams',
|
||||
'session:publicEmailDomains',
|
||||
'session:privateEmailDomains',
|
||||
'session:vaultRevisionNumber'
|
||||
@@ -191,9 +226,9 @@ export function handleClearVault(
|
||||
*/
|
||||
export async function handleGetCredentials(
|
||||
) : Promise<messageCredentialsResponse> {
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
const encryptionKey = await handleGetEncryptionKey();
|
||||
|
||||
if (!derivedKey) {
|
||||
if (!encryptionKey) {
|
||||
return { success: false, error: await t('common.errors.vaultIsLocked') };
|
||||
}
|
||||
|
||||
@@ -203,7 +238,7 @@ export async function handleGetCredentials(
|
||||
return { success: true, credentials: credentials };
|
||||
} catch (error) {
|
||||
console.error('Error getting credentials:', error);
|
||||
return { success: false, error: await t('common.errors.failedToGetCredentials') };
|
||||
return { success: false, error: await t('common.errors.failedToRetrieveData') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,9 +248,9 @@ export async function handleGetCredentials(
|
||||
export async function handleCreateIdentity(
|
||||
message: any,
|
||||
) : Promise<messageBoolResponse> {
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
const encryptionKey = await handleGetEncryptionKey();
|
||||
|
||||
if (!derivedKey) {
|
||||
if (!encryptionKey) {
|
||||
return { success: false, error: await t('common.errors.vaultIsLocked') };
|
||||
}
|
||||
|
||||
@@ -223,7 +258,7 @@ export async function handleCreateIdentity(
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
|
||||
// Add the new credential to the vault/database.
|
||||
sqliteClient.createCredential(message.credential);
|
||||
await sqliteClient.createCredential(message.credential, message.attachments || []);
|
||||
|
||||
// Upload the new vault to the server.
|
||||
await uploadNewVaultToServer(sqliteClient);
|
||||
@@ -231,7 +266,7 @@ export async function handleCreateIdentity(
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to create identity:', error);
|
||||
return { success: false, error: await t('common.errors.failedToCreateIdentity') };
|
||||
return { success: false, error: await t('common.errors.unknownError') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +308,7 @@ export function handleGetDefaultEmailDomain(): Promise<stringResponse> {
|
||||
return { success: true, value: defaultEmailDomain ?? undefined };
|
||||
} catch (error) {
|
||||
console.error('Error getting default email domain:', error);
|
||||
return { success: false, error: await t('common.errors.failedToGetDefaultEmailDomain') };
|
||||
return { success: false, error: await t('common.errors.failedToRetrieveData') };
|
||||
}
|
||||
})();
|
||||
}
|
||||
@@ -297,7 +332,7 @@ export async function handleGetDefaultIdentitySettings(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting default identity settings:', error);
|
||||
return { success: false, error: await t('common.errors.failedToGetDefaultIdentitySettings') };
|
||||
return { success: false, error: await t('common.errors.failedToRetrieveData') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,17 +348,34 @@ export async function handleGetPasswordSettings(
|
||||
return { success: true, settings: passwordSettings };
|
||||
} catch (error) {
|
||||
console.error('Error getting password settings:', error);
|
||||
return { success: false, error: await t('common.errors.failedToGetPasswordSettings') };
|
||||
return { success: false, error: await t('common.errors.failedToRetrieveData') };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the derived key for the encrypted vault.
|
||||
* Get the encryption key for the encrypted vault.
|
||||
*/
|
||||
export async function handleGetDerivedKey(
|
||||
) : Promise<string> {
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
return derivedKey;
|
||||
export async function handleGetEncryptionKey(
|
||||
) : Promise<string | null> {
|
||||
// Try the current key name first (since 0.22.0)
|
||||
let encryptionKey = await storage.getItem('session:encryptionKey') as string | null;
|
||||
|
||||
// Fall back to the legacy key name if not found
|
||||
if (!encryptionKey) {
|
||||
// TODO: this check can be removed some period of time after 0.22.0 is released.
|
||||
encryptionKey = await storage.getItem('session:derivedKey') as string | null;
|
||||
}
|
||||
|
||||
return encryptionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the encryption key derivation parameters for password change detection and offline mode.
|
||||
*/
|
||||
export async function handleGetEncryptionKeyDerivationParams(
|
||||
) : Promise<EncryptionKeyDerivationParams | null> {
|
||||
const params = await storage.getItem('session:encryptionKeyDerivationParams') as EncryptionKeyDerivationParams | null;
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -353,16 +405,16 @@ export async function handleUploadVault(
|
||||
* Data is encrypted using the derived key for additional security.
|
||||
*/
|
||||
export async function handlePersistFormValues(data: any): Promise<void> {
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
if (!derivedKey) {
|
||||
throw new Error(await t('common.errors.noDerivedKeyAvailable'));
|
||||
const encryptionKey = await handleGetEncryptionKey();
|
||||
if (!encryptionKey) {
|
||||
throw new Error(await t('common.errors.unknownError'));
|
||||
}
|
||||
|
||||
// Always stringify the data properly
|
||||
const serializedData = JSON.stringify(data);
|
||||
const encryptedData = await EncryptionUtility.symmetricEncrypt(
|
||||
serializedData,
|
||||
derivedKey
|
||||
encryptionKey
|
||||
);
|
||||
await storage.setItem('session:persistedFormValues', encryptedData);
|
||||
}
|
||||
@@ -372,17 +424,17 @@ export async function handlePersistFormValues(data: any): Promise<void> {
|
||||
* Data is decrypted using the derived key.
|
||||
*/
|
||||
export async function handleGetPersistedFormValues(): Promise<any | null> {
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
const encryptionKey = await handleGetEncryptionKey();
|
||||
const encryptedData = await storage.getItem('session:persistedFormValues') as string | null;
|
||||
|
||||
if (!encryptedData || !derivedKey) {
|
||||
if (!encryptedData || !encryptionKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedData = await EncryptionUtility.symmetricDecrypt(
|
||||
encryptedData,
|
||||
derivedKey
|
||||
encryptionKey
|
||||
);
|
||||
return JSON.parse(decryptedData);
|
||||
} catch (error) {
|
||||
@@ -403,11 +455,15 @@ export async function handleClearPersistedFormValues(): Promise<void> {
|
||||
*/
|
||||
async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<VaultPostResponse> {
|
||||
const updatedVaultData = sqliteClient.exportToBase64();
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
const encryptionKey = await handleGetEncryptionKey();
|
||||
|
||||
if (!encryptionKey) {
|
||||
throw new Error(await t('common.errors.vaultIsLocked'));
|
||||
}
|
||||
|
||||
const encryptedVault = await EncryptionUtility.symmetricEncrypt(
|
||||
updatedVaultData,
|
||||
derivedKey
|
||||
encryptionKey
|
||||
);
|
||||
|
||||
await storage.setItems([
|
||||
@@ -443,7 +499,7 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<Vaul
|
||||
if (response.status === 0) {
|
||||
await storage.setItem('session:vaultRevisionNumber', response.newRevisionNumber);
|
||||
} else {
|
||||
throw new Error(await t('common.errors.failedToUploadVaultToServer'));
|
||||
throw new Error(await t('common.errors.failedToUploadVault'));
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -454,15 +510,15 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<Vaul
|
||||
*/
|
||||
async function createVaultSqliteClient() : Promise<SqliteClient> {
|
||||
const encryptedVault = await storage.getItem('session:encryptedVault') as string;
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
if (!encryptedVault || !derivedKey) {
|
||||
throw new Error(await t('common.errors.noVaultOrDerivedKeyFound'));
|
||||
const encryptionKey = await handleGetEncryptionKey();
|
||||
if (!encryptedVault || !encryptionKey) {
|
||||
throw new Error(await t('common.errors.unknownError'));
|
||||
}
|
||||
|
||||
// Decrypt the vault.
|
||||
const decryptedVault = await EncryptionUtility.symmetricDecrypt(
|
||||
encryptedVault,
|
||||
derivedKey
|
||||
encryptionKey
|
||||
);
|
||||
|
||||
// Initialize the SQLite client with the decrypted vault.
|
||||
|
||||
@@ -9,8 +9,7 @@ import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/Boo
|
||||
|
||||
import { t } from '@/i18n/StandaloneI18n';
|
||||
|
||||
import { defineContentScript } from '#imports';
|
||||
import { createShadowRootUi } from '#imports';
|
||||
import { defineContentScript, createShadowRootUi } from '#imports';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
@@ -35,6 +34,7 @@ export default defineContentScript({
|
||||
name: 'aliasvault-ui',
|
||||
position: 'inline',
|
||||
anchor: 'body',
|
||||
mode: 'closed',
|
||||
/**
|
||||
* Handle mount.
|
||||
*/
|
||||
|
||||
@@ -1,108 +1,212 @@
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import { CombinedStopWords } from '@/utils/formDetector/FieldPatterns';
|
||||
|
||||
export enum AutofillMatchingMode {
|
||||
DEFAULT = 'default',
|
||||
URL_EXACT = 'url_exact',
|
||||
URL_SUBDOMAIN = 'url_subdomain'
|
||||
}
|
||||
|
||||
type CredentialWithPriority = Credential & {
|
||||
priority: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter credentials based on current URL and page context to determine which credentials to show
|
||||
* in the autofill popup. Credentials are sorted by priority:
|
||||
* 1. Exact URL match (highest priority)
|
||||
* 2. Base URL match AND page title word match
|
||||
* 3. Base URL match only
|
||||
* 4. Page title word match only (lowest priority)
|
||||
* Extract domain from URL, handling both full URLs and partial domains
|
||||
* @param url - URL or domain string
|
||||
* @returns Normalized domain without protocol or www
|
||||
*/
|
||||
export function filterCredentials(credentials: Credential[], currentUrl: string, pageTitle: string): Credential[] {
|
||||
const urlObject = new URL(currentUrl);
|
||||
const baseUrl = `${urlObject.protocol}//${urlObject.hostname}`;
|
||||
const filtered: CredentialWithPriority[] = [];
|
||||
|
||||
const sanitizedCurrentUrl = currentUrl.toLowerCase().replace('www.', '');
|
||||
|
||||
// 1. Exact URL match (priority 1)
|
||||
credentials.forEach(cred => {
|
||||
if (!cred.ServiceUrl || cred.ServiceUrl.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedCredUrl = cred.ServiceUrl.toLowerCase().replace('www.', '');
|
||||
|
||||
if (sanitizedCurrentUrl.startsWith(sanitizedCredUrl)) {
|
||||
filtered.push({ ...cred, priority: 1 });
|
||||
}
|
||||
});
|
||||
|
||||
// If we have one or more exact matches, do not continue to other matches
|
||||
if (filtered.length > 0) {
|
||||
return filtered;
|
||||
function extractDomain(url: string): string {
|
||||
if (!url) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Prepare page title words for matching
|
||||
const titleWords = pageTitle.length > 0
|
||||
? pageTitle.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(word =>
|
||||
word.length > 3 &&
|
||||
!CombinedStopWords.has(word.toLowerCase())
|
||||
)
|
||||
: [];
|
||||
// Remove protocol if present
|
||||
let domain = url.toLowerCase().trim();
|
||||
domain = domain.replace(/^https?:\/\//, '');
|
||||
|
||||
// Check for base URL matches and page title matches
|
||||
// Remove www. prefix
|
||||
domain = domain.replace(/^www\./, '');
|
||||
|
||||
// Remove path, query, and fragment
|
||||
domain = domain.split('/')[0];
|
||||
domain = domain.split('?')[0];
|
||||
domain = domain.split('#')[0];
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two domains match, supporting partial matches
|
||||
* @param domain1 - First domain
|
||||
* @param domain2 - Second domain
|
||||
* @returns True if domains match (including partial matches)
|
||||
*/
|
||||
function domainsMatch(domain1: string, domain2: string): boolean {
|
||||
if (!domain1 || !domain2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const d1 = extractDomain(domain1);
|
||||
const d2 = extractDomain(domain2);
|
||||
|
||||
// Exact match
|
||||
if (d1 === d2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if one domain contains the other (for subdomain matching)
|
||||
if (d1.includes(d2) || d2.includes(d1)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Extract root domains for comparison
|
||||
const d1Parts = d1.split('.');
|
||||
const d2Parts = d2.split('.');
|
||||
|
||||
// Get the last 2 parts (domain.tld) for comparison
|
||||
const d1Root = d1Parts.slice(-2).join('.');
|
||||
const d2Root = d2Parts.slice(-2).join('.');
|
||||
|
||||
return d1Root === d2Root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract meaningful words from text, removing punctuation and filtering stop words
|
||||
* @param text - Text to extract words from
|
||||
* @returns Array of filtered words
|
||||
*/
|
||||
function extractWords(text: string): string[] {
|
||||
if (!text || text.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return text.toLowerCase()
|
||||
// Replace common separators and punctuation with spaces
|
||||
.replace(/[|,;:\-–—/\\()[\]{}'"`~!@#$%^&*+=<>?]/g, ' ')
|
||||
// Split on whitespace and filter
|
||||
.split(/\s+/)
|
||||
.filter(word =>
|
||||
word.length > 3 &&
|
||||
!CombinedStopWords.has(word)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter credentials based on current URL and page context with anti-phishing protection.
|
||||
*
|
||||
* **Security Note**: When searching with a URL, text search fallback only applies to
|
||||
* credentials with no service URL defined. This prevents phishing attacks where a
|
||||
* malicious site might match credentials intended for the legitimate site.
|
||||
*
|
||||
* Credentials are sorted by priority:
|
||||
* 1. Exact domain match (priority 1 - highest)
|
||||
* 2. Partial/subdomain match (priority 2)
|
||||
* 3. Service name fallback match (priority 5 - lowest, only for credentials without URLs)
|
||||
*/
|
||||
export function filterCredentials(credentials: Credential[], currentUrl: string, pageTitle: string, matchingMode: AutofillMatchingMode = AutofillMatchingMode.DEFAULT): Credential[] {
|
||||
const filtered: CredentialWithPriority[] = [];
|
||||
const currentDomain = extractDomain(currentUrl);
|
||||
|
||||
// Determine feature flags based on matching mode
|
||||
let enableExactMatch = false;
|
||||
let enableSubdomainMatch = false;
|
||||
let enableServiceNameFallback = false;
|
||||
|
||||
switch (matchingMode) {
|
||||
case AutofillMatchingMode.URL_EXACT:
|
||||
enableExactMatch = true;
|
||||
enableSubdomainMatch = false;
|
||||
enableServiceNameFallback = false;
|
||||
break;
|
||||
|
||||
case AutofillMatchingMode.URL_SUBDOMAIN:
|
||||
enableExactMatch = true;
|
||||
enableSubdomainMatch = true;
|
||||
enableServiceNameFallback = false;
|
||||
break;
|
||||
|
||||
case AutofillMatchingMode.DEFAULT:
|
||||
enableExactMatch = true;
|
||||
enableSubdomainMatch = true;
|
||||
enableServiceNameFallback = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Process credentials with service URLs
|
||||
credentials.forEach(cred => {
|
||||
if (!cred.ServiceUrl || filtered.some(f => f.Id === cred.Id)) {
|
||||
if (!cred.ServiceUrl || cred.ServiceUrl.length === 0) {
|
||||
return; // Handle these in service name fallback
|
||||
}
|
||||
|
||||
const credDomain = extractDomain(cred.ServiceUrl);
|
||||
|
||||
// Check for exact match (priority 1)
|
||||
if (enableExactMatch && currentDomain === credDomain) {
|
||||
filtered.push({ ...cred, priority: 1 });
|
||||
return;
|
||||
}
|
||||
|
||||
let hasBaseUrlMatch = false;
|
||||
let hasTitleMatch = false;
|
||||
|
||||
// Check base URL match
|
||||
try {
|
||||
const credUrlObject = new URL(cred.ServiceUrl);
|
||||
const currentUrlObject = new URL(baseUrl);
|
||||
|
||||
const credDomainParts = credUrlObject.hostname.toLowerCase().split('.');
|
||||
const currentDomainParts = currentUrlObject.hostname.toLowerCase().split('.');
|
||||
|
||||
const credRootDomain = credDomainParts.slice(-2).join('.');
|
||||
const currentRootDomain = currentDomainParts.slice(-2).join('.');
|
||||
|
||||
if (credUrlObject.protocol === currentUrlObject.protocol &&
|
||||
credRootDomain === currentRootDomain) {
|
||||
hasBaseUrlMatch = true;
|
||||
}
|
||||
} catch {
|
||||
// Invalid URL, skip
|
||||
}
|
||||
|
||||
// Check page title match
|
||||
if (titleWords.length > 0) {
|
||||
const credNameWords = cred.ServiceName.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(word => word.length > 3 && !CombinedStopWords.has(word));
|
||||
hasTitleMatch = titleWords.some(word =>
|
||||
credNameWords.some(credWord => credWord.includes(word))
|
||||
);
|
||||
}
|
||||
|
||||
// Assign priority based on matches
|
||||
if (hasBaseUrlMatch && hasTitleMatch) {
|
||||
// Check for subdomain/partial match (priority 2)
|
||||
if (enableSubdomainMatch && domainsMatch(currentDomain, credDomain)) {
|
||||
filtered.push({ ...cred, priority: 2 });
|
||||
} else if (hasBaseUrlMatch) {
|
||||
filtered.push({ ...cred, priority: 3 });
|
||||
} else if (hasTitleMatch) {
|
||||
filtered.push({ ...cred, priority: 4 });
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by priority and then take unique credentials
|
||||
// Service name fallback for credentials without URLs (priority 5)
|
||||
if (enableServiceNameFallback) {
|
||||
/*
|
||||
* SECURITY: Service name matching only applies to credentials with no service URL.
|
||||
* This prevents phishing attacks where a malicious site might match credentials
|
||||
* intended for a legitimate site.
|
||||
*/
|
||||
|
||||
// Extract words from page title
|
||||
const titleWords = extractWords(pageTitle);
|
||||
|
||||
if (titleWords.length > 0) {
|
||||
credentials.forEach(cred => {
|
||||
// CRITICAL: Only check credentials that have NO service URL defined
|
||||
if (cred.ServiceUrl && cred.ServiceUrl.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if already in filtered list
|
||||
if (filtered.some(f => f.Id === cred.Id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check page title match with service name
|
||||
if (cred.ServiceName) {
|
||||
const credNameWords = extractWords(cred.ServiceName);
|
||||
|
||||
/*
|
||||
* Match only complete words, not substrings
|
||||
* For example: "Express" should match "My Express Account" but not "AliExpress"
|
||||
*/
|
||||
const hasTitleMatch = titleWords.some(titleWord =>
|
||||
credNameWords.some(credWord =>
|
||||
titleWord === credWord // Exact word match only
|
||||
)
|
||||
);
|
||||
|
||||
if (hasTitleMatch) {
|
||||
filtered.push({ ...cred, priority: 5 });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by priority and return unique credentials (max 3)
|
||||
const uniqueCredentials = Array.from(
|
||||
new Map(filtered
|
||||
.sort((a, b) => a.priority - b.priority)
|
||||
.map(cred => [cred.Id, cred]))
|
||||
.values()
|
||||
new Map(
|
||||
filtered
|
||||
.sort((a, b) => a.priority - b.priority)
|
||||
.map(cred => [cred.Id, cred])
|
||||
).values()
|
||||
);
|
||||
// Show max 3 results
|
||||
|
||||
return uniqueCredentials.slice(0, 3);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { sendMessage } from 'webext-bridge/content-script';
|
||||
|
||||
import { openAutofillPopup } from '@/entrypoints/contentScript/Popup';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { FormFiller } from '@/utils/formDetector/FormFiller';
|
||||
import { ClickValidator } from '@/utils/security/ClickValidator';
|
||||
|
||||
/**
|
||||
* Global timestamp to track popup debounce time.
|
||||
@@ -12,6 +15,11 @@ import { FormFiller } from '@/utils/formDetector/FormFiller';
|
||||
*/
|
||||
let popupDebounceTime = 0;
|
||||
|
||||
/**
|
||||
* ClickValidator instance for form security validation
|
||||
*/
|
||||
const clickValidator = ClickValidator.getInstance();
|
||||
|
||||
/**
|
||||
* Check if popup can be shown based on debounce time.
|
||||
*/
|
||||
@@ -32,6 +40,8 @@ export function hidePopupFor(ms: number) : void {
|
||||
|
||||
/**
|
||||
* Validates if an element is a supported input field that can be processed for autofill.
|
||||
* This function supports regular input elements, custom elements with type attributes,
|
||||
* and custom web components that may contain shadow DOM.
|
||||
* @param element The element to validate
|
||||
* @returns An object containing validation result and the element cast as HTMLInputElement if valid
|
||||
*/
|
||||
@@ -42,14 +52,30 @@ export function validateInputField(element: Element | null): { isValid: boolean;
|
||||
|
||||
const textInputTypes = ['text', 'email', 'tel', 'password', 'search', 'url', 'number'];
|
||||
const elementType = element.getAttribute('type');
|
||||
const isInputElement = element.tagName.toLowerCase() === 'input';
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
const isInputElement = tagName === 'input';
|
||||
|
||||
// Check if element has shadow DOM with input elements
|
||||
const elementWithShadow = element as HTMLElement & { shadowRoot?: ShadowRoot };
|
||||
const hasShadowDOMInput = elementWithShadow.shadowRoot &&
|
||||
elementWithShadow.shadowRoot.querySelector('input, textarea');
|
||||
|
||||
// Check if it's a custom element that might be an input
|
||||
const isLikelyCustomInputElement = tagName.includes('-') && (
|
||||
tagName.includes('input') ||
|
||||
tagName.includes('field') ||
|
||||
tagName.includes('text') ||
|
||||
hasShadowDOMInput
|
||||
);
|
||||
|
||||
// Check if it's a valid input field we should process
|
||||
const isValid = (
|
||||
// Case 1: It's an input element (with either explicit type or defaulting to "text")
|
||||
(isInputElement && (!elementType || textInputTypes.includes(elementType?.toLowerCase() ?? ''))) ||
|
||||
// Case 2: Non-input element but has valid type attribute
|
||||
(!isInputElement && elementType && textInputTypes.includes(elementType.toLowerCase()))
|
||||
(!isInputElement && elementType && textInputTypes.includes(elementType.toLowerCase())) ||
|
||||
// Case 3: It's a custom element that likely contains an input
|
||||
(isLikelyCustomInputElement)
|
||||
) as boolean;
|
||||
|
||||
return {
|
||||
@@ -64,10 +90,15 @@ export function validateInputField(element: Element | null): { isValid: boolean;
|
||||
* @param credential - The credential to fill.
|
||||
* @param input - The input element that triggered the popup. Required when filling credentials to know which form to fill.
|
||||
*/
|
||||
export function fillCredential(credential: Credential, input: HTMLInputElement) : void {
|
||||
export async function fillCredential(credential: Credential, input: HTMLInputElement): Promise<void> {
|
||||
// Set debounce time to 300ms to prevent the popup from being shown again within 300ms because of autofill events.
|
||||
hidePopupFor(300);
|
||||
|
||||
// Reset auto-lock timer when autofilling
|
||||
sendMessage('RESET_AUTO_LOCK_TIMER', {}, 'background').catch(() => {
|
||||
// Ignore errors as background script might not be ready
|
||||
});
|
||||
|
||||
const formDetector = new FormDetector(document, input);
|
||||
const form = formDetector.getForm();
|
||||
|
||||
@@ -77,7 +108,7 @@ export function fillCredential(credential: Credential, input: HTMLInputElement)
|
||||
}
|
||||
|
||||
const formFiller = new FormFiller(form, triggerInputEvents);
|
||||
formFiller.fillFields(credential);
|
||||
await formFiller.fillFields(credential);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,7 +129,7 @@ function findActualInput(element: HTMLElement): HTMLInputElement {
|
||||
return element as HTMLInputElement;
|
||||
}
|
||||
|
||||
// Try to find a visible child input
|
||||
// Try to find a visible child input in regular DOM
|
||||
const childInput = element.querySelector('input');
|
||||
if (childInput) {
|
||||
const style = window.getComputedStyle(childInput);
|
||||
@@ -107,6 +138,17 @@ function findActualInput(element: HTMLElement): HTMLInputElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find input in shadow DOM if element has shadowRoot
|
||||
if (element.shadowRoot) {
|
||||
const shadowInput = element.shadowRoot.querySelector('input');
|
||||
if (shadowInput) {
|
||||
const style = window.getComputedStyle(shadowInput);
|
||||
if (style.display !== 'none' && style.visibility !== 'hidden') {
|
||||
return shadowInput as HTMLInputElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to the provided element if no child input found
|
||||
return element as HTMLInputElement;
|
||||
}
|
||||
@@ -179,9 +221,16 @@ export function injectIcon(input: HTMLInputElement, container: HTMLElement): voi
|
||||
window.addEventListener('resize', updateIconPosition);
|
||||
|
||||
// Add click event to trigger the autofill popup and refocus the input
|
||||
icon.addEventListener('click', (e: MouseEvent) => {
|
||||
icon.addEventListener('click', async (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Validate the click for security
|
||||
if (!await clickValidator.validateClick(e)) {
|
||||
console.warn('[AliasVault Security] Blocked autofill popup opening due to security validation failure');
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => actualInput.focus(), 0);
|
||||
openAutofillPopup(actualInput, container);
|
||||
});
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { sendMessage } from 'webext-bridge/content-script';
|
||||
|
||||
import { filterCredentials } from '@/entrypoints/contentScript/Filter';
|
||||
import { filterCredentials, AutofillMatchingMode } from '@/entrypoints/contentScript/Filter';
|
||||
import { fillCredential } from '@/entrypoints/contentScript/Form';
|
||||
|
||||
import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY, LAST_CUSTOM_EMAIL_KEY, LAST_CUSTOM_USERNAME_KEY } from '@/utils/Constants';
|
||||
import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY, AUTOFILL_MATCHING_MODE_KEY, CUSTOM_EMAIL_HISTORY_KEY, CUSTOM_USERNAME_HISTORY_KEY } from '@/utils/Constants';
|
||||
import { CreateIdentityGenerator } from '@/utils/dist/shared/identity-generator';
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import { CreatePasswordGenerator, PasswordGenerator, PasswordSettings } from '@/utils/dist/shared/password-generator';
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { ClickValidator } from '@/utils/security/ClickValidator';
|
||||
import { ServiceDetectionUtility } from '@/utils/serviceDetection/ServiceDetectionUtility';
|
||||
import { SqliteClient } from '@/utils/SqliteClient';
|
||||
import { CredentialsResponse } from '@/utils/types/messaging/CredentialsResponse';
|
||||
import { IdentitySettingsResponse } from '@/utils/types/messaging/IdentitySettingsResponse';
|
||||
@@ -23,6 +24,11 @@ import { storage } from '#imports';
|
||||
*/
|
||||
let popupListeners = new WeakMap<HTMLElement, EventListener>();
|
||||
|
||||
/**
|
||||
* Global ClickValidator instance for content script security
|
||||
*/
|
||||
const clickValidator = ClickValidator.getInstance();
|
||||
|
||||
/**
|
||||
* Open (or refresh) the autofill popup including check if vault is locked.
|
||||
*/
|
||||
@@ -181,10 +187,14 @@ export async function createAutofillPopup(input: HTMLInputElement, credentials:
|
||||
credentials = [];
|
||||
}
|
||||
|
||||
// Load autofill matching mode setting
|
||||
const matchingMode = await storage.getItem(AUTOFILL_MATCHING_MODE_KEY) as AutofillMatchingMode ?? AutofillMatchingMode.DEFAULT;
|
||||
|
||||
const filteredCredentials = filterCredentials(
|
||||
credentials,
|
||||
window.location.href,
|
||||
document.title
|
||||
document.title,
|
||||
matchingMode
|
||||
);
|
||||
|
||||
updatePopupContent(filteredCredentials, credentialList, input, rootContainer, noMatchesText);
|
||||
@@ -217,8 +227,8 @@ export async function createAutofillPopup(input: HTMLInputElement, credentials:
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
const suggestedNames = FormDetector.getSuggestedServiceName(document, window.location);
|
||||
const result = await createAliasCreationPopup(suggestedNames, rootContainer);
|
||||
const serviceInfo = ServiceDetectionUtility.getServiceInfo(document, window.location);
|
||||
const result = await createAliasCreationPopup(serviceInfo.suggestedNames, rootContainer);
|
||||
|
||||
if (!result) {
|
||||
// User cancelled
|
||||
@@ -318,20 +328,21 @@ export async function createAutofillPopup(input: HTMLInputElement, credentials:
|
||||
}
|
||||
};
|
||||
|
||||
// Add click listener with capture and prevent removal.
|
||||
// Add click listener with capture and prevent removal and security validation.
|
||||
addReliableClickHandler(createButton, handleCreateClick);
|
||||
|
||||
// Create search input.
|
||||
// Create search input with native placeholder.
|
||||
const searchInput = document.createElement('input');
|
||||
searchInput.type = 'text';
|
||||
searchInput.dataset.avDisable = 'true';
|
||||
searchInput.placeholder = searchPlaceholder;
|
||||
searchInput.dataset.avDisable = 'true';
|
||||
searchInput.id = 'aliasvault-search-input';
|
||||
searchInput.className = 'av-search-input';
|
||||
|
||||
// Handle search input.
|
||||
let searchTimeout: NodeJS.Timeout | null = null;
|
||||
searchInput.addEventListener('input', () => {
|
||||
handleSearchInput(searchInput, credentials, rootContainer, searchTimeout, credentialList, input, noMatchesText);
|
||||
searchInput.addEventListener('input', async () => {
|
||||
await handleSearchInput(searchInput, credentials, rootContainer, searchTimeout, credentialList, input, noMatchesText);
|
||||
});
|
||||
|
||||
// Close button
|
||||
@@ -412,7 +423,7 @@ export async function createAutofillPopup(input: HTMLInputElement, credentials:
|
||||
addReliableClickHandler(contextMenu, handleContextMenuClick);
|
||||
};
|
||||
|
||||
// Add click handlers
|
||||
// Add click handlers with security validation
|
||||
addReliableClickHandler(closeButton, (e: Event) => {
|
||||
handleCloseClick(e);
|
||||
});
|
||||
@@ -465,7 +476,7 @@ export async function createVaultLockedPopup(input: HTMLInputElement, rootContai
|
||||
const container = document.createElement('div');
|
||||
container.className = 'av-vault-locked-container';
|
||||
|
||||
// Make the entire container clickable
|
||||
// Make the entire container clickable with security validation
|
||||
addReliableClickHandler(container, handleUnlockClick);
|
||||
container.style.cursor = 'pointer';
|
||||
|
||||
@@ -507,7 +518,7 @@ export async function createVaultLockedPopup(input: HTMLInputElement, rootContai
|
||||
closeButton.style.top = '50%';
|
||||
closeButton.style.transform = 'translateY(-50%)';
|
||||
|
||||
// Handle close button click
|
||||
// Handle close button click with security validation
|
||||
addReliableClickHandler(closeButton, async (e) => {
|
||||
e.stopPropagation(); // Prevent opening the unlock popup
|
||||
await dismissVaultLockedPopup();
|
||||
@@ -540,7 +551,7 @@ export async function createVaultLockedPopup(input: HTMLInputElement, rootContai
|
||||
/**
|
||||
* Handle popup search input by filtering credentials based on the search term.
|
||||
*/
|
||||
function handleSearchInput(searchInput: HTMLInputElement, credentials: Credential[], rootContainer: HTMLElement, searchTimeout: NodeJS.Timeout | null, credentialList: HTMLElement | null, input: HTMLInputElement, noMatchesText?: string) : void {
|
||||
async function handleSearchInput(searchInput: HTMLInputElement, credentials: Credential[], rootContainer: HTMLElement, searchTimeout: NodeJS.Timeout | null, credentialList: HTMLElement | null, input: HTMLInputElement, noMatchesText?: string) : Promise<void> {
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout);
|
||||
}
|
||||
@@ -551,11 +562,15 @@ function handleSearchInput(searchInput: HTMLInputElement, credentials: Credentia
|
||||
let filteredCredentials;
|
||||
|
||||
if (searchTerm === '') {
|
||||
// Load autofill matching mode setting
|
||||
const matchingMode = await storage.getItem(AUTOFILL_MATCHING_MODE_KEY) as AutofillMatchingMode ?? AutofillMatchingMode.DEFAULT;
|
||||
|
||||
// If search is empty, use original URL-based filtering
|
||||
filteredCredentials = filterCredentials(
|
||||
uniqueCredentials,
|
||||
window.location.href,
|
||||
document.title
|
||||
document.title,
|
||||
matchingMode
|
||||
).sort((a, b) => {
|
||||
// First compare by service name
|
||||
const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? '');
|
||||
@@ -654,7 +669,7 @@ function createCredentialList(credentials: Credential[], input: HTMLInputElement
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Handle popout click
|
||||
// Handle popout click with security validation
|
||||
addReliableClickHandler(popoutIcon, (e) => {
|
||||
e.stopPropagation(); // Prevent credential fill
|
||||
sendMessage('OPEN_POPUP_WITH_CREDENTIAL', { credentialId: cred.Id }, 'background');
|
||||
@@ -664,7 +679,7 @@ function createCredentialList(credentials: Credential[], input: HTMLInputElement
|
||||
item.appendChild(credentialInfo);
|
||||
item.appendChild(popoutIcon);
|
||||
|
||||
// Update click handler to only trigger on credentialInfo
|
||||
// Update click handler to only trigger on credentialInfo with security validation
|
||||
addReliableClickHandler(credentialInfo, () => {
|
||||
fillCredential(cred, input);
|
||||
removeExistingPopup(rootContainer);
|
||||
@@ -747,9 +762,9 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
// Close existing popup
|
||||
removeExistingPopup(rootContainer);
|
||||
|
||||
// Load last used values
|
||||
const lastEmail = await storage.getItem(LAST_CUSTOM_EMAIL_KEY) as string ?? '';
|
||||
const lastUsername = await storage.getItem(LAST_CUSTOM_USERNAME_KEY) as string ?? '';
|
||||
// Load history
|
||||
const emailHistory = await storage.getItem(CUSTOM_EMAIL_HISTORY_KEY) as string[] ?? [];
|
||||
const usernameHistory = await storage.getItem(CUSTOM_USERNAME_HISTORY_KEY) as string[] ?? [];
|
||||
|
||||
return new Promise((resolve) => {
|
||||
(async (): Promise<void> => {
|
||||
@@ -814,11 +829,20 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
${randomIdentityIcon}
|
||||
<h3 class="av-create-popup-title">${randomIdentityTitle}</h3>
|
||||
</div>
|
||||
<button class="av-create-popup-mode-dropdown">
|
||||
<svg class="av-icon" viewBox="0 0 24 24">
|
||||
<path d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="av-create-popup-header-buttons">
|
||||
<button class="av-create-popup-mode-dropdown">
|
||||
<svg class="av-icon" viewBox="0 0 24 24">
|
||||
<path d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="av-create-popup-popout" title="Open in main popup">
|
||||
<svg class="av-icon" viewBox="0 0 24 24">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
||||
<polyline points="15 3 21 3 21 9"></polyline>
|
||||
<line x1="10" y1="14" x2="21" y2="3"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -873,8 +897,8 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
id="custom-email"
|
||||
class="av-create-popup-input"
|
||||
placeholder="${enterEmailAddressText}"
|
||||
data-default-value="${lastEmail}"
|
||||
>
|
||||
<div class="av-field-suggestions" id="email-suggestions"></div>
|
||||
</div>
|
||||
<div class="av-create-popup-field-group">
|
||||
<label for="custom-username">${usernameText}</label>
|
||||
@@ -883,8 +907,8 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
id="custom-username"
|
||||
class="av-create-popup-input"
|
||||
placeholder="${enterUsernameText}"
|
||||
data-default-value="${lastUsername}"
|
||||
>
|
||||
<div class="av-field-suggestions" id="username-suggestions"></div>
|
||||
</div>
|
||||
<div class="av-create-popup-field-group">
|
||||
<label>${passwordText}</label>
|
||||
@@ -945,6 +969,7 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
const customMode = popup.querySelector('.av-create-popup-custom-mode') as HTMLElement;
|
||||
const dropdownMenu = popup.querySelector('.av-create-popup-mode-dropdown-menu') as HTMLElement;
|
||||
const titleContainer = popup.querySelector('.av-create-popup-title-container') as HTMLElement;
|
||||
const popoutBtn = popup.querySelector('.av-create-popup-popout') as HTMLButtonElement;
|
||||
const cancelBtn = popup.querySelector('#cancel-btn') as HTMLButtonElement;
|
||||
const customCancelBtn = popup.querySelector('#custom-cancel-btn') as HTMLButtonElement;
|
||||
const saveBtn = popup.querySelector('#save-btn') as HTMLButtonElement;
|
||||
@@ -955,41 +980,154 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
const passwordPreview = popup.querySelector('#password-preview') as HTMLInputElement;
|
||||
const regenerateBtn = popup.querySelector('#regenerate-password') as HTMLButtonElement;
|
||||
const toggleVisibilityBtn = popup.querySelector('#toggle-password-visibility') as HTMLButtonElement;
|
||||
const emailSuggestions = popup.querySelector('#email-suggestions') as HTMLElement;
|
||||
const usernameSuggestions = popup.querySelector('#username-suggestions') as HTMLElement;
|
||||
|
||||
/**
|
||||
* Setup default value for input with placeholder styling.
|
||||
* Update history with new value (max 2 unique entries)
|
||||
*/
|
||||
const setupDefaultValue = (input: HTMLInputElement) : void => {
|
||||
const defaultValue = input.dataset.defaultValue;
|
||||
if (defaultValue) {
|
||||
input.value = defaultValue;
|
||||
input.classList.add('av-create-popup-input-default');
|
||||
const updateHistory = async (value: string, historyKey: typeof CUSTOM_EMAIL_HISTORY_KEY | typeof CUSTOM_USERNAME_HISTORY_KEY, maxItems: number = 2): Promise<string[]> => {
|
||||
const history = await storage.getItem(historyKey) as string[] ?? [];
|
||||
|
||||
// Remove the value if it already exists
|
||||
const filteredHistory = history.filter((item: string) => item !== value);
|
||||
|
||||
// Add the new value at the beginning
|
||||
if (value.trim()) {
|
||||
filteredHistory.unshift(value);
|
||||
}
|
||||
|
||||
// Keep only the first maxItems
|
||||
const updatedHistory = filteredHistory.slice(0, maxItems);
|
||||
|
||||
// Save the updated history
|
||||
await storage.setItem(historyKey, updatedHistory);
|
||||
|
||||
return updatedHistory;
|
||||
};
|
||||
|
||||
setupDefaultValue(customEmail);
|
||||
setupDefaultValue(customUsername);
|
||||
/**
|
||||
* Remove item from history
|
||||
*/
|
||||
const removeFromHistory = async (value: string, historyKey: typeof CUSTOM_EMAIL_HISTORY_KEY | typeof CUSTOM_USERNAME_HISTORY_KEY): Promise<string[]> => {
|
||||
const history = await storage.getItem(historyKey) as string[] ?? [];
|
||||
const updatedHistory = history.filter((item: string) => item !== value);
|
||||
await storage.setItem(historyKey, updatedHistory);
|
||||
return updatedHistory;
|
||||
};
|
||||
|
||||
// Handle input changes
|
||||
customEmail.addEventListener('input', () => {
|
||||
const value = customEmail.value.trim();
|
||||
if (value || value === '') {
|
||||
customEmail.classList.remove('av-create-popup-input-default');
|
||||
storage.setItem(LAST_CUSTOM_EMAIL_KEY, value);
|
||||
/**
|
||||
* Format suggestions HTML as pill-style buttons
|
||||
*/
|
||||
const formatSuggestionsHtml = async (history: string[], currentValue: string): Promise<string> => {
|
||||
// Filter out the current value from history and limit to 2 items
|
||||
const filteredHistory = history
|
||||
.filter(item => item.toLowerCase() !== currentValue.toLowerCase())
|
||||
.slice(0, 2);
|
||||
|
||||
if (filteredHistory.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Build HTML with pill-style buttons
|
||||
return filteredHistory.map(item =>
|
||||
`<span class="av-suggestion-pill">
|
||||
<span class="av-suggestion-pill-text" data-value="${item}">${item}</span>
|
||||
<span class="av-suggestion-pill-delete" data-value="${item}" title="Remove">×</span>
|
||||
</span>`
|
||||
).join(' ');
|
||||
};
|
||||
|
||||
/**
|
||||
* Update suggestions display
|
||||
*/
|
||||
const updateSuggestions = async (input: HTMLInputElement, suggestionsContainer: HTMLElement, history: string[]): Promise<void> => {
|
||||
const currentValue = input.value.trim();
|
||||
const html = await formatSuggestionsHtml(history, currentValue);
|
||||
suggestionsContainer.innerHTML = html;
|
||||
suggestionsContainer.style.display = html ? 'flex' : 'none';
|
||||
};
|
||||
|
||||
// Initial display of suggestions
|
||||
await updateSuggestions(customEmail, emailSuggestions, emailHistory);
|
||||
await updateSuggestions(customUsername, usernameSuggestions, usernameHistory);
|
||||
|
||||
// Handle popout button click
|
||||
popoutBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const serviceName = inputServiceName.value.trim();
|
||||
const encodedServiceInfo = ServiceDetectionUtility.getEncodedServiceInfo(document, window.location);
|
||||
sendMessage('OPEN_POPUP_CREATE_CREDENTIAL', {
|
||||
serviceName: serviceName || encodedServiceInfo.serviceName,
|
||||
currentUrl: encodedServiceInfo.currentUrl
|
||||
}, 'background');
|
||||
closePopup(null);
|
||||
});
|
||||
|
||||
// Handle email input
|
||||
customEmail.addEventListener('input', async () => {
|
||||
await updateSuggestions(customEmail, emailSuggestions, emailHistory);
|
||||
});
|
||||
|
||||
// Handle username input
|
||||
customUsername.addEventListener('input', async () => {
|
||||
await updateSuggestions(customUsername, usernameSuggestions, usernameHistory);
|
||||
});
|
||||
|
||||
// Handle suggestion clicks for email
|
||||
emailSuggestions.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Check if delete button was clicked
|
||||
if (target.classList.contains('av-suggestion-pill-delete')) {
|
||||
const value = target.dataset.value;
|
||||
if (value) {
|
||||
const updatedHistory = await removeFromHistory(value, CUSTOM_EMAIL_HISTORY_KEY);
|
||||
emailHistory.splice(0, emailHistory.length, ...updatedHistory);
|
||||
await updateSuggestions(customEmail, emailSuggestions, emailHistory);
|
||||
}
|
||||
} else {
|
||||
customEmail.classList.add('av-create-popup-input-default');
|
||||
storage.setItem(LAST_CUSTOM_EMAIL_KEY, '');
|
||||
// Check if pill or pill text was clicked
|
||||
let pillElement = target.closest('.av-suggestion-pill') as HTMLElement;
|
||||
if (pillElement) {
|
||||
const textElement = pillElement.querySelector('.av-suggestion-pill-text') as HTMLElement;
|
||||
const value = textElement?.dataset.value;
|
||||
if (value) {
|
||||
customEmail.value = value;
|
||||
await updateSuggestions(customEmail, emailSuggestions, emailHistory);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
customUsername.addEventListener('input', () => {
|
||||
const value = customUsername.value.trim();
|
||||
if (value || value === '') {
|
||||
customUsername.classList.remove('av-create-popup-input-default');
|
||||
storage.setItem(LAST_CUSTOM_USERNAME_KEY, value);
|
||||
// Handle suggestion clicks for username
|
||||
usernameSuggestions.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Check if delete button was clicked
|
||||
if (target.classList.contains('av-suggestion-pill-delete')) {
|
||||
const value = target.dataset.value;
|
||||
if (value) {
|
||||
const updatedHistory = await removeFromHistory(value, CUSTOM_USERNAME_HISTORY_KEY);
|
||||
usernameHistory.splice(0, usernameHistory.length, ...updatedHistory);
|
||||
await updateSuggestions(customUsername, usernameSuggestions, usernameHistory);
|
||||
}
|
||||
} else {
|
||||
customUsername.classList.add('av-create-popup-input-default');
|
||||
storage.setItem(LAST_CUSTOM_USERNAME_KEY, '');
|
||||
// Check if pill or pill text was clicked
|
||||
let pillElement = target.closest('.av-suggestion-pill') as HTMLElement;
|
||||
if (pillElement) {
|
||||
const textElement = pillElement.querySelector('.av-suggestion-pill-text') as HTMLElement;
|
||||
const value = textElement?.dataset.value;
|
||||
if (value) {
|
||||
customUsername.value = value;
|
||||
await updateSuggestions(customUsername, usernameSuggestions, usernameHistory);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1357,12 +1495,8 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
if (serviceName) {
|
||||
const email = customEmail.value.trim();
|
||||
const username = customUsername.value.trim();
|
||||
const hasDefaultEmail = customEmail.classList.contains('av-create-popup-input-default');
|
||||
const hasDefaultUsername = customUsername.classList.contains('av-create-popup-input-default');
|
||||
|
||||
// If using default values, use the dataset values
|
||||
const finalEmail = hasDefaultEmail ? customEmail.dataset.defaultValue : email;
|
||||
const finalUsername = hasDefaultUsername ? customUsername.dataset.defaultValue : username;
|
||||
const finalEmail = email;
|
||||
const finalUsername = username;
|
||||
|
||||
if (!finalEmail && !finalUsername) {
|
||||
// Add error styling to fields
|
||||
@@ -1409,6 +1543,14 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
return;
|
||||
}
|
||||
|
||||
// Update history when saving
|
||||
if (finalEmail) {
|
||||
await updateHistory(finalEmail, CUSTOM_EMAIL_HISTORY_KEY);
|
||||
}
|
||||
if (finalUsername) {
|
||||
await updateHistory(finalUsername, CUSTOM_USERNAME_HISTORY_KEY);
|
||||
}
|
||||
|
||||
closePopup({
|
||||
serviceName,
|
||||
isCustomCredential: true,
|
||||
@@ -1679,14 +1821,32 @@ function getValidServiceUrl(): string {
|
||||
|
||||
/**
|
||||
* Add click handler with mousedown/mouseup backup for better click reliability in shadow DOM.
|
||||
* Now includes optional security validation.
|
||||
*
|
||||
* Some websites due to their design cause the AliasVault autofill to re-trigger when clicking
|
||||
* outside of the input field, which causes the AliasVault popup to close before the click event
|
||||
* is registered. This is a workaround to ensure the click event is always registered.
|
||||
*/
|
||||
function addReliableClickHandler(element: HTMLElement, handler: (e: Event) => void): void {
|
||||
function addReliableClickHandler(
|
||||
element: HTMLElement,
|
||||
handler: (e: Event) => void
|
||||
): void {
|
||||
/**
|
||||
* Secure wrapper that validates clicks before executing handler
|
||||
*/
|
||||
const secureHandler = async (e: Event): Promise<void> => {
|
||||
const mouseEvent = e as MouseEvent;
|
||||
|
||||
if (!await clickValidator.validateClick(mouseEvent)) {
|
||||
console.warn(`[AliasVault Security] Blocked click action due to security validation failure`);
|
||||
return;
|
||||
}
|
||||
|
||||
handler(e);
|
||||
};
|
||||
|
||||
// Add primary click listener with capture and prevent removal
|
||||
element.addEventListener('click', handler, {
|
||||
element.addEventListener('click', secureHandler, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
@@ -1699,11 +1859,11 @@ function addReliableClickHandler(element: HTMLElement, handler: (e: Event) => vo
|
||||
isMouseDown = true;
|
||||
}, { capture: true });
|
||||
|
||||
element.addEventListener('mouseup', (e) => {
|
||||
element.addEventListener('mouseup', async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isMouseDown) {
|
||||
handler(e);
|
||||
await secureHandler(e);
|
||||
}
|
||||
isMouseDown = false;
|
||||
}, { capture: true });
|
||||
@@ -1728,7 +1888,6 @@ export async function createUpgradeRequiredPopup(input: HTMLInputElement, rootCo
|
||||
const container = document.createElement('div');
|
||||
container.className = 'av-upgrade-required-container';
|
||||
|
||||
// Make the entire container clickable
|
||||
addReliableClickHandler(container, handleUpgradeClick);
|
||||
container.style.cursor = 'pointer';
|
||||
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
import { filterCredentials } from '../Filter';
|
||||
|
||||
describe('Filter - Credential URL Matching', () => {
|
||||
let testCredentials: Credential[];
|
||||
|
||||
beforeEach(() => {
|
||||
// Create test credentials using shared test data structure
|
||||
testCredentials = createSharedTestCredentials();
|
||||
});
|
||||
|
||||
// [#1] - Exact URL match
|
||||
it('should match exact URL', () => {
|
||||
const matches = filterCredentials(
|
||||
testCredentials,
|
||||
'www.coolblue.nl',
|
||||
''
|
||||
);
|
||||
|
||||
expect(matches).toHaveLength(1);
|
||||
expect(matches[0].ServiceName).toBe('Coolblue');
|
||||
});
|
||||
|
||||
// [#2] - Base URL with path match
|
||||
it('should match base URL with path', () => {
|
||||
const matches = filterCredentials(
|
||||
testCredentials,
|
||||
'https://gmail.com/signin',
|
||||
''
|
||||
);
|
||||
|
||||
expect(matches).toHaveLength(1);
|
||||
expect(matches[0].ServiceName).toBe('Gmail');
|
||||
});
|
||||
|
||||
// [#3] - Root domain with subdomain match
|
||||
it('should match root domain with subdomain', () => {
|
||||
const matches = filterCredentials(
|
||||
testCredentials,
|
||||
'https://mail.google.com',
|
||||
''
|
||||
);
|
||||
|
||||
expect(matches).toHaveLength(1);
|
||||
expect(matches[0].ServiceName).toBe('Google');
|
||||
});
|
||||
|
||||
// [#4] - No matches for non-existent domain
|
||||
it('should return empty array for no matches', () => {
|
||||
const matches = filterCredentials(
|
||||
testCredentials,
|
||||
'https://nonexistent.com',
|
||||
''
|
||||
);
|
||||
|
||||
expect(matches).toHaveLength(0);
|
||||
});
|
||||
|
||||
// [#5] - Partial URL stored matches full URL search
|
||||
it('should match partial URL with full URL - dumpert.nl case', () => {
|
||||
// Test case: stored URL is "dumpert.nl", search with full URL
|
||||
const matches = filterCredentials(
|
||||
testCredentials,
|
||||
'https://www.dumpert.nl',
|
||||
''
|
||||
);
|
||||
|
||||
expect(matches).toHaveLength(1);
|
||||
expect(matches[0].ServiceName).toBe('Dumpert');
|
||||
});
|
||||
|
||||
// [#6] - Full URL stored matches partial URL search
|
||||
it('should match full URL with partial URL', () => {
|
||||
const matches = filterCredentials(
|
||||
testCredentials,
|
||||
'coolblue.nl',
|
||||
''
|
||||
);
|
||||
|
||||
expect(matches).toHaveLength(1);
|
||||
expect(matches[0].ServiceName).toBe('Coolblue');
|
||||
});
|
||||
|
||||
// [#7] - Protocol variations (http/https/none) match
|
||||
it('should handle protocol variations correctly', () => {
|
||||
// Test that http and https variations match
|
||||
const httpsMatches = filterCredentials(
|
||||
testCredentials,
|
||||
'https://github.com',
|
||||
''
|
||||
);
|
||||
const httpMatches = filterCredentials(
|
||||
testCredentials,
|
||||
'http://github.com',
|
||||
''
|
||||
);
|
||||
const noProtocolMatches = filterCredentials(
|
||||
testCredentials,
|
||||
'https://github.com', // Converting no-protocol to https for test
|
||||
''
|
||||
);
|
||||
|
||||
expect(httpsMatches).toHaveLength(1);
|
||||
expect(httpMatches).toHaveLength(1);
|
||||
expect(noProtocolMatches).toHaveLength(1);
|
||||
expect(httpsMatches[0].ServiceName).toBe('GitHub');
|
||||
expect(httpMatches[0].ServiceName).toBe('GitHub');
|
||||
expect(noProtocolMatches[0].ServiceName).toBe('GitHub');
|
||||
});
|
||||
|
||||
// [#8] - WWW prefix variations match
|
||||
it('should handle www variations correctly', () => {
|
||||
// Test that www variations match
|
||||
const withWww = filterCredentials(
|
||||
testCredentials,
|
||||
'https://www.dumpert.nl',
|
||||
''
|
||||
);
|
||||
const withoutWww = filterCredentials(
|
||||
testCredentials,
|
||||
'https://dumpert.nl',
|
||||
''
|
||||
);
|
||||
|
||||
expect(withWww).toHaveLength(1);
|
||||
expect(withoutWww).toHaveLength(1);
|
||||
expect(withWww[0].ServiceName).toBe('Dumpert');
|
||||
expect(withoutWww[0].ServiceName).toBe('Dumpert');
|
||||
});
|
||||
|
||||
// [#9] - Subdomain matching
|
||||
it('should handle subdomain matching', () => {
|
||||
// Test subdomain matching
|
||||
const appSubdomain = filterCredentials(
|
||||
testCredentials,
|
||||
'https://app.example.com',
|
||||
''
|
||||
);
|
||||
const wwwSubdomain = filterCredentials(
|
||||
testCredentials,
|
||||
'https://www.example.com',
|
||||
''
|
||||
);
|
||||
const noSubdomain = filterCredentials(
|
||||
testCredentials,
|
||||
'https://example.com',
|
||||
''
|
||||
);
|
||||
|
||||
expect(appSubdomain).toHaveLength(1);
|
||||
expect(appSubdomain[0].ServiceName).toBe('Subdomain Example');
|
||||
expect(wwwSubdomain).toHaveLength(1);
|
||||
expect(wwwSubdomain[0].ServiceName).toBe('Subdomain Example');
|
||||
expect(noSubdomain).toHaveLength(1);
|
||||
expect(noSubdomain[0].ServiceName).toBe('Subdomain Example');
|
||||
});
|
||||
|
||||
// [#10] - Paths and query strings ignored
|
||||
it('should ignore paths and query strings', () => {
|
||||
// Test that paths and query strings are ignored
|
||||
const withPath = filterCredentials(
|
||||
testCredentials,
|
||||
'https://github.com/user/repo',
|
||||
''
|
||||
);
|
||||
const withQuery = filterCredentials(
|
||||
testCredentials,
|
||||
'https://stackoverflow.com/questions?tab=newest',
|
||||
''
|
||||
);
|
||||
const withFragment = filterCredentials(
|
||||
testCredentials,
|
||||
'https://gmail.com#inbox',
|
||||
''
|
||||
);
|
||||
|
||||
expect(withPath).toHaveLength(1);
|
||||
expect(withPath[0].ServiceName).toBe('GitHub');
|
||||
expect(withQuery).toHaveLength(1);
|
||||
expect(withQuery[0].ServiceName).toBe('Stack Overflow');
|
||||
expect(withFragment).toHaveLength(1);
|
||||
expect(withFragment[0].ServiceName).toBe('Gmail');
|
||||
});
|
||||
|
||||
// [#11] - Complex URL variations
|
||||
it('should handle complex URL variations', () => {
|
||||
// Test complex URL matching scenario
|
||||
const complexUrl = filterCredentials(
|
||||
testCredentials,
|
||||
'https://www.coolblue.nl/product/12345?ref=google',
|
||||
''
|
||||
);
|
||||
|
||||
expect(complexUrl).toHaveLength(1);
|
||||
expect(complexUrl[0].ServiceName).toBe('Coolblue');
|
||||
});
|
||||
|
||||
// [#12] - Priority ordering
|
||||
it('should handle priority ordering', () => {
|
||||
const matches = filterCredentials(
|
||||
testCredentials,
|
||||
'coolblue.nl',
|
||||
''
|
||||
);
|
||||
|
||||
expect(matches).toHaveLength(1);
|
||||
expect(matches[0].ServiceName).toBe('Coolblue');
|
||||
});
|
||||
|
||||
// [#13] - Title-only matching
|
||||
it('should match title only', () => {
|
||||
const matches = filterCredentials(
|
||||
testCredentials,
|
||||
'https://nomatch.com',
|
||||
'newyorktimes'
|
||||
);
|
||||
|
||||
expect(matches).toHaveLength(1);
|
||||
expect(matches[0].ServiceName).toBe('Title Only newyorktimes');
|
||||
});
|
||||
|
||||
/* [#14] - Domain name part matching */
|
||||
it('should handle domain name part matching', () => {
|
||||
const matches = filterCredentials(
|
||||
testCredentials,
|
||||
'https://coolblue.be',
|
||||
''
|
||||
);
|
||||
|
||||
expect(matches).toHaveLength(0);
|
||||
});
|
||||
|
||||
// [#15] - Package name matching
|
||||
it('should handle package name matching', () => {
|
||||
const matches = filterCredentials(
|
||||
testCredentials,
|
||||
'com.coolblue.app',
|
||||
''
|
||||
);
|
||||
|
||||
expect(matches).toHaveLength(1);
|
||||
expect(matches[0].ServiceName).toBe('Coolblue App');
|
||||
});
|
||||
|
||||
// [#16] - Invalid URL handling
|
||||
it('should handle invalid URL', () => {
|
||||
const matches = filterCredentials(
|
||||
testCredentials,
|
||||
'not a url',
|
||||
''
|
||||
);
|
||||
|
||||
expect(matches).toHaveLength(0);
|
||||
});
|
||||
|
||||
// [#17] - Anti-phishing protection
|
||||
it('should handle anti-phishing protection', () => {
|
||||
const matches = filterCredentials(
|
||||
testCredentials,
|
||||
'https://secure-bankk.com',
|
||||
''
|
||||
);
|
||||
|
||||
expect(matches).toHaveLength(0);
|
||||
});
|
||||
|
||||
// [#18] - Ensure only full words are matched
|
||||
it('should not match on string part of word', () => {
|
||||
const matches = filterCredentials(
|
||||
testCredentials,
|
||||
'',
|
||||
'Express Yourself App | Description'
|
||||
);
|
||||
|
||||
// The string above should not match "AliExpress" service name
|
||||
expect(matches).toHaveLength(0);
|
||||
});
|
||||
|
||||
// [#19] - Ensure separators and punctuation are stripped for matching
|
||||
it('should match service names when separated by commas and other punctuation', () => {
|
||||
const matches = filterCredentials(
|
||||
testCredentials,
|
||||
'https://nomatch.com',
|
||||
'Reddit, social media platform'
|
||||
);
|
||||
|
||||
// Should match "Reddit" even though it's followed by a comma and description
|
||||
expect(matches).toHaveLength(1);
|
||||
expect(matches[0].ServiceName).toBe('Reddit');
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates the shared test credential dataset used across all platforms.
|
||||
* Note: when making changes to this list, make sure to update the corresponding list for iOS and Android tests as well.
|
||||
*/
|
||||
function createSharedTestCredentials(): Credential[] {
|
||||
return [
|
||||
createTestCredential('Gmail', 'https://gmail.com', 'user@gmail.com'),
|
||||
createTestCredential('Google', 'https://google.com', 'user@google.com'),
|
||||
createTestCredential('Coolblue', 'https://www.coolblue.nl', 'user@coolblue.nl'),
|
||||
createTestCredential('Amazon', 'https://amazon.com', 'user@amazon.com'),
|
||||
createTestCredential('Coolblue App', 'com.coolblue.app', 'user@coolblue.nl'),
|
||||
createTestCredential('Dumpert', 'dumpert.nl', 'user@dumpert.nl'),
|
||||
createTestCredential('GitHub', 'github.com', 'user@github.com'),
|
||||
createTestCredential('Stack Overflow', 'https://stackoverflow.com', 'user@stackoverflow.com'),
|
||||
createTestCredential('Subdomain Example', 'https://app.example.com', 'user@example.com'),
|
||||
createTestCredential('Title Only newyorktimes', '', ''),
|
||||
createTestCredential('Bank Account', 'https://secure-bank.com', 'user@bank.com'),
|
||||
createTestCredential('AliExpress', 'https://aliexpress.com', 'user@aliexpress.com'),
|
||||
createTestCredential('Reddit', '', 'user@reddit.com'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create test credentials with standardized structure.
|
||||
* @param serviceName - The name of the service
|
||||
* @param serviceUrl - The URL of the service
|
||||
* @param username - The username for the service
|
||||
* @returns A test credential matching the platform's Credential type
|
||||
*/
|
||||
function createTestCredential(
|
||||
serviceName: string,
|
||||
serviceUrl: string,
|
||||
username: string
|
||||
): Credential {
|
||||
return {
|
||||
Id: Math.random().toString(),
|
||||
ServiceName: serviceName,
|
||||
ServiceUrl: serviceUrl,
|
||||
Username: username,
|
||||
Password: 'password123',
|
||||
Notes: '',
|
||||
Logo: new Uint8Array(),
|
||||
Alias: {
|
||||
FirstName: '',
|
||||
LastName: '',
|
||||
NickName: '',
|
||||
BirthDate: '',
|
||||
Gender: undefined,
|
||||
Email: username
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -222,7 +222,7 @@ body {
|
||||
|
||||
/* Search Input */
|
||||
.av-search-input {
|
||||
flex: 2;
|
||||
flex: 1;
|
||||
border-radius: 4px;
|
||||
background: #374151;
|
||||
color: #e5e7eb;
|
||||
@@ -231,12 +231,13 @@ body {
|
||||
border: 1px solid #4b5563;
|
||||
outline: none;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
min-width: 0px;
|
||||
padding: 8px 12px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.av-search-input::placeholder {
|
||||
color: #bdbebe;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.av-search-input:focus {
|
||||
@@ -538,6 +539,62 @@ body {
|
||||
box-shadow: 0 0 0 1px #ef4444 !important;
|
||||
}
|
||||
|
||||
/* Field Suggestions - Pill Style */
|
||||
.av-field-suggestions {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.av-suggestion-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: #4b5563;
|
||||
border: 1px solid #6b7280;
|
||||
border-radius: 16px;
|
||||
padding: 4px 8px 4px 12px;
|
||||
font-size: 13px;
|
||||
color: #e5e7eb;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.av-suggestion-pill:hover {
|
||||
background: #6b7280;
|
||||
border-color: #9ca3af;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.av-suggestion-pill-text {
|
||||
display: inline-block;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.av-suggestion-pill-delete {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 6px;
|
||||
padding: 0 2px;
|
||||
color: #9ca3af;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
border-left: 1px solid #6b7280;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.av-suggestion-pill-delete:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.av-create-popup-error-text {
|
||||
color: #ef4444;
|
||||
font-size: 0.875rem;
|
||||
@@ -727,28 +784,41 @@ body {
|
||||
.av-create-popup-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.av-create-popup-title-wrapper {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: #d68338;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.av-create-popup-header-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
|
||||
.av-create-popup-title-wrapper .av-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke-width: 1.5;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.av-create-popup-title-wrapper .av-create-popup-title {
|
||||
@@ -756,6 +826,7 @@ body {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #f8f9fa;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.av-create-popup-title-container:hover {
|
||||
@@ -784,6 +855,34 @@ body {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.av-create-popup-popout {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
color: #9ca3af;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.av-create-popup-popout:hover {
|
||||
background-color: #4b5563;
|
||||
color: #d68338;
|
||||
}
|
||||
|
||||
.av-create-popup-popout .av-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-dropdown-menu {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { ClipboardCountdownBar } from '@/entrypoints/popup/components/ClipboardCountdownBar';
|
||||
import BottomNav from '@/entrypoints/popup/components/Layout/BottomNav';
|
||||
import Header from '@/entrypoints/popup/components/Layout/Header';
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
@@ -9,20 +11,25 @@ import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { NavigationProvider } from '@/entrypoints/popup/context/NavigationContext';
|
||||
import AuthSettings from '@/entrypoints/popup/pages/AuthSettings';
|
||||
import CredentialAddEdit from '@/entrypoints/popup/pages/CredentialAddEdit';
|
||||
import CredentialDetails from '@/entrypoints/popup/pages/CredentialDetails';
|
||||
import CredentialsList from '@/entrypoints/popup/pages/CredentialsList';
|
||||
import EmailDetails from '@/entrypoints/popup/pages/EmailDetails';
|
||||
import EmailsList from '@/entrypoints/popup/pages/EmailsList';
|
||||
import AuthSettings from '@/entrypoints/popup/pages/auth/AuthSettings';
|
||||
import Login from '@/entrypoints/popup/pages/auth/Login';
|
||||
import Logout from '@/entrypoints/popup/pages/auth/Logout';
|
||||
import Unlock from '@/entrypoints/popup/pages/auth/Unlock';
|
||||
import UnlockSuccess from '@/entrypoints/popup/pages/auth/UnlockSuccess';
|
||||
import Upgrade from '@/entrypoints/popup/pages/auth/Upgrade';
|
||||
import CredentialAddEdit from '@/entrypoints/popup/pages/credentials/CredentialAddEdit';
|
||||
import CredentialDetails from '@/entrypoints/popup/pages/credentials/CredentialDetails';
|
||||
import CredentialsList from '@/entrypoints/popup/pages/credentials/CredentialsList';
|
||||
import EmailDetails from '@/entrypoints/popup/pages/emails/EmailDetails';
|
||||
import EmailsList from '@/entrypoints/popup/pages/emails/EmailsList';
|
||||
import Index from '@/entrypoints/popup/pages/Index';
|
||||
import Login from '@/entrypoints/popup/pages/Login';
|
||||
import Logout from '@/entrypoints/popup/pages/Logout';
|
||||
import Reinitialize from '@/entrypoints/popup/pages/Reinitialize';
|
||||
import Settings from '@/entrypoints/popup/pages/Settings';
|
||||
import Unlock from '@/entrypoints/popup/pages/Unlock';
|
||||
import UnlockSuccess from '@/entrypoints/popup/pages/UnlockSuccess';
|
||||
import Upgrade from '@/entrypoints/popup/pages/Upgrade';
|
||||
import AutofillSettings from '@/entrypoints/popup/pages/settings/AutofillSettings';
|
||||
import AutoLockSettings from '@/entrypoints/popup/pages/settings/AutoLockSettings';
|
||||
import ClipboardSettings from '@/entrypoints/popup/pages/settings/ClipboardSettings';
|
||||
import ContextMenuSettings from '@/entrypoints/popup/pages/settings/ContextMenuSettings';
|
||||
import LanguageSettings from '@/entrypoints/popup/pages/settings/LanguageSettings';
|
||||
import Settings from '@/entrypoints/popup/pages/settings/Settings';
|
||||
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
|
||||
@@ -65,6 +72,11 @@ const App: React.FC = () => {
|
||||
{ path: '/emails', element: <EmailsList />, showBackButton: false },
|
||||
{ path: '/emails/:id', element: <EmailDetails />, showBackButton: true, title: t('emails.title') },
|
||||
{ path: '/settings', element: <Settings />, showBackButton: false },
|
||||
{ path: '/settings/autofill', element: <AutofillSettings />, showBackButton: true, title: t('settings.autofillSettings') },
|
||||
{ path: '/settings/context-menu', element: <ContextMenuSettings />, showBackButton: true, title: t('settings.contextMenuSettings') },
|
||||
{ path: '/settings/clipboard', element: <ClipboardSettings />, showBackButton: true, title: t('settings.clipboardSettings') },
|
||||
{ path: '/settings/language', element: <LanguageSettings />, showBackButton: true, title: t('settings.language') },
|
||||
{ path: '/settings/auto-lock', element: <AutoLockSettings />, showBackButton: true, title: t('settings.autoLockTimeout') },
|
||||
{ path: '/logout', element: <Logout />, showBackButton: false },
|
||||
], [t]);
|
||||
|
||||
@@ -74,6 +86,29 @@ const App: React.FC = () => {
|
||||
}
|
||||
}, [isInitialLoading, setIsLoading]);
|
||||
|
||||
/**
|
||||
* Send heartbeat to background every 5 seconds while popup is open.
|
||||
* This extends the auto-lock timer to prevent vault locking while popup is active.
|
||||
*/
|
||||
useEffect(() => {
|
||||
// Send initial heartbeat
|
||||
sendMessage('POPUP_HEARTBEAT', {}, 'background').catch(() => {
|
||||
// Ignore errors as background script might not be ready
|
||||
});
|
||||
|
||||
// Set up heartbeat interval
|
||||
const heartbeatInterval = setInterval(() => {
|
||||
sendMessage('POPUP_HEARTBEAT', {}, 'background').catch(() => {
|
||||
// Ignore errors as background script might not be ready
|
||||
});
|
||||
}, 5000); // Send heartbeat every 5 seconds
|
||||
|
||||
// Cleanup: clear interval when popup closes
|
||||
return () : void => {
|
||||
clearInterval(heartbeatInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Print global message if it exists.
|
||||
*/
|
||||
@@ -95,6 +130,7 @@ const App: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ClipboardCountdownBar />
|
||||
<Header
|
||||
routes={routes}
|
||||
rightButtons={headerButtons}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { onMessage, sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
/**
|
||||
* Clipboard countdown bar component.
|
||||
*/
|
||||
export const ClipboardCountdownBar: React.FC = () => {
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||
const animationRef = useRef<HTMLDivElement>(null);
|
||||
const currentCountdownIdRef = useRef<number>(0);
|
||||
|
||||
/**
|
||||
* Starts the countdown animation.
|
||||
*/
|
||||
const startAnimation = (remaining: number, total: number) : void => {
|
||||
// Use a small delay to ensure the component is fully rendered
|
||||
setTimeout(() => {
|
||||
if (animationRef.current) {
|
||||
// Calculate the starting percentage based on remaining time
|
||||
const percentage = (remaining / total) * 100;
|
||||
|
||||
// Reset any existing animation
|
||||
animationRef.current.style.transition = 'none';
|
||||
animationRef.current.style.width = `${percentage}%`;
|
||||
|
||||
// Force browser to flush styles
|
||||
void animationRef.current.offsetHeight;
|
||||
|
||||
// Start animation from current position to 0
|
||||
requestAnimationFrame(() => {
|
||||
if (animationRef.current) {
|
||||
animationRef.current.style.transition = `width ${remaining}s linear`;
|
||||
animationRef.current.style.width = '0%';
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Request current countdown state on mount
|
||||
sendMessage('GET_CLIPBOARD_COUNTDOWN_STATE', {}, 'background').then((state) => {
|
||||
const countdownState = state as { remaining: number; total: number; id: number } | null;
|
||||
if (countdownState && countdownState.remaining > 0) {
|
||||
currentCountdownIdRef.current = countdownState.id;
|
||||
setIsVisible(true);
|
||||
startAnimation(countdownState.remaining, countdownState.total);
|
||||
}
|
||||
}).catch(() => {
|
||||
// No active countdown
|
||||
});
|
||||
// Listen for countdown updates from background script
|
||||
const unsubscribe = onMessage('CLIPBOARD_COUNTDOWN', ({ data }) => {
|
||||
const { remaining, total, id } = data as { remaining: number; total: number; id: number };
|
||||
setIsVisible(remaining > 0);
|
||||
|
||||
// Check if this is a new countdown (different ID)
|
||||
const isNewCountdown = id !== currentCountdownIdRef.current;
|
||||
|
||||
// Start animation when new countdown begins
|
||||
if (isNewCountdown && remaining > 0) {
|
||||
currentCountdownIdRef.current = id;
|
||||
startAnimation(remaining, total);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for clipboard cleared message
|
||||
const unsubscribeClear = onMessage('CLIPBOARD_CLEARED', () => {
|
||||
setIsVisible(false);
|
||||
currentCountdownIdRef.current = 0;
|
||||
if (animationRef.current) {
|
||||
animationRef.current.style.transition = 'none';
|
||||
animationRef.current.style.width = '0%';
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for countdown cancelled message
|
||||
const unsubscribeCancel = onMessage('CLIPBOARD_COUNTDOWN_CANCELLED', () => {
|
||||
setIsVisible(false);
|
||||
currentCountdownIdRef.current = 0;
|
||||
if (animationRef.current) {
|
||||
animationRef.current.style.transition = 'none';
|
||||
animationRef.current.style.width = '0%';
|
||||
}
|
||||
});
|
||||
|
||||
return () : void => {
|
||||
// Clean up listeners
|
||||
unsubscribe();
|
||||
unsubscribeClear();
|
||||
unsubscribeCancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 left-0 right-0 z-50 h-1 bg-gray-200 dark:bg-gray-700">
|
||||
<div
|
||||
ref={animationRef}
|
||||
className="h-full bg-orange-500"
|
||||
style={{ width: '100%', transition: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -21,14 +21,18 @@ const HeaderBlock: React.FC<HeaderBlockProps> = ({ credential }) => (
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-gray-900 dark:text-white">{credential.ServiceName}</h1>
|
||||
{credential.ServiceUrl && (
|
||||
<a
|
||||
href={credential.ServiceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 break-all"
|
||||
>
|
||||
{credential.ServiceUrl}
|
||||
</a>
|
||||
/^https?:\/\//i.test(credential.ServiceUrl) ? (
|
||||
<a
|
||||
href={credential.ServiceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 break-all"
|
||||
>
|
||||
{credential.ServiceUrl}
|
||||
</a>
|
||||
) : (
|
||||
<span className="break-all">{credential.ServiceUrl}</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as OTPAuth from 'otpauth';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
@@ -68,6 +69,9 @@ const TotpBlock: React.FC<TotpBlockProps> = ({ credentialId }) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopiedId(id);
|
||||
|
||||
// Notify background script that clipboard was copied
|
||||
await sendMessage('CLIPBOARD_COPIED', { value: code }, 'background');
|
||||
|
||||
// Reset copied state after 2 seconds
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
type EmailDomainFieldProps = {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
error?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
// Hardcoded public email domains (same as in AliasVault.Client)
|
||||
const PUBLIC_EMAIL_DOMAINS = [
|
||||
'spamok.com',
|
||||
'solarflarecorp.com',
|
||||
'spamok.nl',
|
||||
'3060.nl',
|
||||
'landmail.nl',
|
||||
'asdasd.nl',
|
||||
'spamok.de',
|
||||
'spamok.com.ua',
|
||||
'spamok.es',
|
||||
'spamok.fr',
|
||||
];
|
||||
|
||||
/**
|
||||
* Email domain field component with domain chooser functionality.
|
||||
* Allows users to select from private/public domains or enter custom email addresses.
|
||||
*/
|
||||
const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
error,
|
||||
required = false
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dbContext = useDb();
|
||||
const [isCustomDomain, setIsCustomDomain] = useState(false);
|
||||
const [localPart, setLocalPart] = useState('');
|
||||
const [selectedDomain, setSelectedDomain] = useState('');
|
||||
const [isPopupVisible, setIsPopupVisible] = useState(false);
|
||||
const [privateEmailDomains, setPrivateEmailDomains] = useState<string[]>([]);
|
||||
const popupRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Get private email domains from vault metadata
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Load private email domains from vault metadata.
|
||||
*/
|
||||
const loadDomains = async (): Promise<void> => {
|
||||
const metadata = await dbContext.getVaultMetadata();
|
||||
if (metadata?.privateEmailDomains) {
|
||||
setPrivateEmailDomains(metadata.privateEmailDomains);
|
||||
}
|
||||
};
|
||||
loadDomains();
|
||||
}, [dbContext]);
|
||||
|
||||
// Check if private domains are available and valid
|
||||
const showPrivateDomains = useMemo(() => {
|
||||
return privateEmailDomains.length > 0 &&
|
||||
!(privateEmailDomains.length === 1 && (privateEmailDomains[0] === 'DISABLED.TLD' || privateEmailDomains[0] === ''));
|
||||
}, [privateEmailDomains]);
|
||||
|
||||
// Initialize state from value prop
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
// Set default domain
|
||||
if (showPrivateDomains && privateEmailDomains[0]) {
|
||||
setSelectedDomain(privateEmailDomains[0]);
|
||||
} else if (PUBLIC_EMAIL_DOMAINS[0]) {
|
||||
setSelectedDomain(PUBLIC_EMAIL_DOMAINS[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.includes('@')) {
|
||||
const [local, domain] = value.split('@');
|
||||
setLocalPart(local);
|
||||
setSelectedDomain(domain);
|
||||
|
||||
// Check if it's a custom domain
|
||||
const isKnownDomain = PUBLIC_EMAIL_DOMAINS.includes(domain) ||
|
||||
privateEmailDomains.includes(domain);
|
||||
setIsCustomDomain(!isKnownDomain);
|
||||
} else {
|
||||
setLocalPart(value);
|
||||
// Don't reset isCustomDomain here - preserve the current mode
|
||||
|
||||
// Set default domain if not already set
|
||||
if (!selectedDomain && !value.includes('@')) {
|
||||
if (showPrivateDomains && privateEmailDomains[0]) {
|
||||
setSelectedDomain(privateEmailDomains[0]);
|
||||
} else if (PUBLIC_EMAIL_DOMAINS[0]) {
|
||||
setSelectedDomain(PUBLIC_EMAIL_DOMAINS[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [value, privateEmailDomains, showPrivateDomains, selectedDomain]);
|
||||
|
||||
// Handle local part changes
|
||||
const handleLocalPartChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newLocalPart = e.target.value;
|
||||
|
||||
// If in custom domain mode, always pass through the full value
|
||||
if (isCustomDomain) {
|
||||
onChange(newLocalPart);
|
||||
// Stay in custom domain mode - don't auto-switch back
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if new value contains '@' symbol, if so, switch to custom domain mode
|
||||
if (newLocalPart.includes('@')) {
|
||||
setIsCustomDomain(true);
|
||||
onChange(newLocalPart);
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalPart(newLocalPart);
|
||||
// If the local part is empty, treat the whole field as empty
|
||||
if (!newLocalPart || newLocalPart.trim() === '') {
|
||||
onChange('');
|
||||
} else if (selectedDomain) {
|
||||
onChange(`${newLocalPart}@${selectedDomain}`);
|
||||
}
|
||||
}, [isCustomDomain, selectedDomain, onChange]);
|
||||
|
||||
// Select a domain from the popup
|
||||
const selectDomain = useCallback((domain: string) => {
|
||||
setSelectedDomain(domain);
|
||||
const cleanLocalPart = localPart.includes('@') ? localPart.split('@')[0] : localPart;
|
||||
// If the local part is empty, treat the whole field as empty
|
||||
if (!cleanLocalPart || cleanLocalPart.trim() === '') {
|
||||
onChange('');
|
||||
} else {
|
||||
onChange(`${cleanLocalPart}@${domain}`);
|
||||
}
|
||||
setIsCustomDomain(false);
|
||||
setIsPopupVisible(false);
|
||||
}, [localPart, onChange]);
|
||||
|
||||
// Toggle between custom domain and domain chooser
|
||||
const toggleCustomDomain = useCallback(() => {
|
||||
const newIsCustom = !isCustomDomain;
|
||||
setIsCustomDomain(newIsCustom);
|
||||
|
||||
if (newIsCustom) {
|
||||
/*
|
||||
* Switching to custom domain mode
|
||||
* If we have a domain-based value, extract just the local part
|
||||
*/
|
||||
if (value && value.includes('@')) {
|
||||
const [local] = value.split('@');
|
||||
onChange(local);
|
||||
setLocalPart(local);
|
||||
}
|
||||
} else {
|
||||
// Switching to domain chooser mode
|
||||
const defaultDomain = showPrivateDomains && privateEmailDomains[0]
|
||||
? privateEmailDomains[0]
|
||||
: PUBLIC_EMAIL_DOMAINS[0];
|
||||
setSelectedDomain(defaultDomain);
|
||||
|
||||
// Only add domain if we have a local part
|
||||
if (localPart && localPart.trim()) {
|
||||
onChange(`${localPart}@${defaultDomain}`);
|
||||
} else if (value && !value.includes('@')) {
|
||||
// If we have a value without @, add the domain
|
||||
onChange(`${value}@${defaultDomain}`);
|
||||
}
|
||||
}
|
||||
}, [isCustomDomain, value, localPart, showPrivateDomains, privateEmailDomains, onChange]);
|
||||
|
||||
// Handle clicks outside the popup
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Handle clicks outside the popup to close it.
|
||||
*/
|
||||
const handleClickOutside = (event: MouseEvent): void => {
|
||||
if (popupRef.current && !popupRef.current.contains(event.target as Node)) {
|
||||
setIsPopupVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isPopupVisible) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return (): void => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}
|
||||
}, [isPopupVisible]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label htmlFor={id} className="block font-medium text-gray-700 dark:text-gray-300">
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
|
||||
<div className="relative w-full">
|
||||
<div className="flex w-full">
|
||||
<input
|
||||
type="text"
|
||||
id={id}
|
||||
className={`flex-1 min-w-0 px-3 py-2 border text-sm ${
|
||||
error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
|
||||
} ${
|
||||
!isCustomDomain ? 'rounded-l-md' : 'rounded-md'
|
||||
} focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white`}
|
||||
value={isCustomDomain ? value : localPart}
|
||||
onChange={handleLocalPartChange}
|
||||
placeholder={isCustomDomain ? t('credentials.enterFullEmail') : t('credentials.enterEmailPrefix')}
|
||||
/>
|
||||
|
||||
{!isCustomDomain && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPopupVisible(!isPopupVisible)}
|
||||
className="inline-flex items-center px-2 py-2 border border-l-0 border-gray-300 dark:border-gray-600 rounded-r-md bg-gray-50 dark:bg-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-500 cursor-pointer text-sm truncate max-w-[120px]"
|
||||
>
|
||||
<span className="text-gray-500 dark:text-gray-400">@</span>
|
||||
<span className="truncate ml-0.5">{selectedDomain}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Domain selection popup */}
|
||||
{isPopupVisible && !isCustomDomain && (
|
||||
<div
|
||||
ref={popupRef}
|
||||
className="absolute z-50 mt-2 w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 max-h-96 overflow-y-auto"
|
||||
>
|
||||
<div className="p-4">
|
||||
{showPrivateDomains && (
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
{t('credentials.privateEmailTitle')} <span className="text-gray-500 dark:text-gray-400">({t('credentials.privateEmailAliasVaultServer')})</span>
|
||||
</h4>
|
||||
<p className="text-gray-500 dark:text-gray-400 mb-3">
|
||||
{t('credentials.privateEmailDescription')}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{privateEmailDomains.map((domain) => (
|
||||
<button
|
||||
key={domain}
|
||||
type="button"
|
||||
onClick={() => selectDomain(domain)}
|
||||
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
|
||||
selectedDomain === domain
|
||||
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
{domain}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={showPrivateDomains ? 'border-t border-gray-200 dark:border-gray-600 pt-4' : ''}>
|
||||
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
{t('credentials.publicEmailTitle')}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
{t('credentials.publicEmailDescription')}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{PUBLIC_EMAIL_DOMAINS.map((domain) => (
|
||||
<button
|
||||
key={domain}
|
||||
type="button"
|
||||
onClick={() => selectDomain(domain)}
|
||||
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
|
||||
selectedDomain === domain
|
||||
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
{domain}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Toggle custom domain button */}
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleCustomDomain}
|
||||
className="text-sm text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300"
|
||||
>
|
||||
{isCustomDomain
|
||||
? t('credentials.useDomainChooser')
|
||||
: t('credentials.enterCustomDomain')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<p className="text-sm text-red-500 mt-1">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailDomainField;
|
||||
@@ -213,7 +213,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
}
|
||||
if (emails.length === 0) {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4 text-sm">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">{t('common.recentEmails')}</h2>
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
|
||||
@@ -109,7 +109,7 @@ export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(({
|
||||
}
|
||||
};
|
||||
|
||||
const inputClasses = `mt-1 block w-full rounded-md ${
|
||||
const inputClasses = `mt-1 block text-sm w-full rounded-md ${
|
||||
error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
|
||||
} text-gray-900 sm:text-sm rounded-lg shadow-sm border focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400 py-2 px-3`;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { ClipboardCopyService } from '@/entrypoints/popup/utils/ClipboardCopyService';
|
||||
|
||||
@@ -82,6 +83,9 @@ export const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> =
|
||||
await navigator.clipboard.writeText(value);
|
||||
clipboardService.setCopied(id);
|
||||
|
||||
// Notify background script that clipboard was copied
|
||||
await sendMessage('CLIPBOARD_COPIED', { value }, 'background');
|
||||
|
||||
// Reset copied state after 2 seconds
|
||||
setTimeout(() => {
|
||||
if (clipboardService.getCopiedId() === id) {
|
||||
@@ -107,7 +111,7 @@ export const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> =
|
||||
onClick={copyToClipboard}
|
||||
className={`w-full px-3 py-2.5 bg-white border ${
|
||||
copied ? 'border-green-500 border-2' : 'border-gray-300'
|
||||
} text-gray-900 sm:text-sm rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400`}
|
||||
} text-gray-900 text-sm rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400`}
|
||||
/>
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
||||
{copied ? (
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type HelpModalProps = {
|
||||
titleKey: string;
|
||||
contentKey: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable help modal component with a question mark icon button.
|
||||
* Shows a modal popup with help information when clicked.
|
||||
*/
|
||||
const HelpModal: React.FC<HelpModalProps> = ({ titleKey, contentKey, className = '' }) => {
|
||||
const { t } = useTranslation();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setShowModal(true)}
|
||||
className={`${className}`}
|
||||
type="button"
|
||||
aria-label="Help"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 cursor-help"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-sm w-full">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{t(titleKey)}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
aria-label={t('common.close')}
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{t(contentKey)}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="mt-4 w-full px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-md transition-colors"
|
||||
>
|
||||
{t('common.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpModal;
|
||||
@@ -9,7 +9,9 @@ export enum HeaderIconType {
|
||||
EXTERNAL_LINK = 'external_link',
|
||||
SAVE = 'save',
|
||||
PLUS = 'plus',
|
||||
TAB = 'tab'
|
||||
TAB = 'tab',
|
||||
EYE = 'eye',
|
||||
EYE_OFF = 'eye_off'
|
||||
}
|
||||
|
||||
type HeaderIconProps = {
|
||||
@@ -131,19 +133,7 @@ export const HeaderIcon: React.FC<HeaderIconProps> = ({ type, className = 'w-5 h
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M17 3H7a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V7l-4-4z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 3v5h10"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 12a2 2 0 100 4 2 2 0 000-4z"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
@@ -179,6 +169,44 @@ export const HeaderIcon: React.FC<HeaderIconProps> = ({ type, className = 'w-5 h
|
||||
d="M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
[HeaderIconType.EYE]: (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
[HeaderIconType.EYE_OFF]: (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ const BottomNav: React.FC = () => {
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
<span className="text-xs mt-1">{t('menu.credentials')}</span>
|
||||
<span className="text-sm mt-1">{t('menu.credentials')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTabChange('emails')}
|
||||
@@ -73,7 +73,7 @@ const BottomNav: React.FC = () => {
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="text-xs mt-1">{t('menu.emails')}</span>
|
||||
<span className="text-sm mt-1">{t('menu.emails')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTabChange('settings')}
|
||||
@@ -85,7 +85,7 @@ const BottomNav: React.FC = () => {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span className="text-xs mt-1">{t('menu.settings')}</span>
|
||||
<span className="text-sm mt-1">{t('menu.settings')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
import Logo from '@/entrypoints/popup/components/Logo';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
|
||||
/**
|
||||
@@ -87,11 +88,15 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onClick={() => logoClick()}
|
||||
className="flex items-center hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<img src="/assets/images/logo.svg" alt="AliasVault" className="h-8 w-8 mr-2" />
|
||||
<h1 className="text-gray-900 dark:text-white text-xl font-bold">{t('common.appName')}</h1>
|
||||
<Logo
|
||||
width={125}
|
||||
height={40}
|
||||
showText={true}
|
||||
className="text-gray-900 dark:text-white"
|
||||
/>
|
||||
{/* Hide beta badge on Safari as it's not allowed to show non-production badges */}
|
||||
{!import.meta.env.SAFARI && (
|
||||
<span className="text-primary-500 text-[10px] ml-1 font-normal">BETA</span>
|
||||
<span className="text-primary-500 text-[10px] font-normal">BETA</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ const LoginServerInfo: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 mb-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
({t('auth.connectingTo')}{' '}
|
||||
<button
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
|
||||
type LogoProps = {
|
||||
className?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
showText?: boolean;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logo component.
|
||||
*/
|
||||
const Logo: React.FC<LogoProps> = ({
|
||||
className = '',
|
||||
width = 200,
|
||||
height = 50,
|
||||
showText = true,
|
||||
color = 'currentColor'
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlSpace="preserve"
|
||||
version="1.1"
|
||||
viewBox="0 0 2000 500"
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
>
|
||||
{/* Logo mark */}
|
||||
<path
|
||||
d="m459.87 294.95c0.016205 5.4005 0.03241 10.801-0.35022 16.873-1.111 6.3392-1.1941 12.173-2.6351 17.649-10.922 41.508-36.731 69.481-77.351 83.408-7.2157 2.4739-14.972 3.3702-22.479 4.995-23.629 0.042205-47.257 0.11453-70.886 0.12027-46.762 0.011322-93.523-0.01416-140.95-0.43411-8.59-2.0024-16.766-2.8352-24.398-5.3326-21.595-7.0666-39.523-19.656-53.708-37.552-10.227-12.903-17.579-27.17-21.28-43.221-1.475-6.3967-2.4711-12.904-3.6852-19.361-0.051849-5.747-0.1037-11.494 0.26915-17.886 4.159-42.973 27.68-71.638 63.562-92.153 0-0.70761-0.001961-1.6988 3.12e-4 -2.69 0.022484-9.8293-1.3071-19.894 0.35664-29.438 3.2391-18.579 11.08-35.272 23.763-49.773 12.098-13.832 26.457-23.989 43.609-30.029 7.813-2.7512 16.14-4.0417 24.234-5.9948 7.392-0.025734 14.784-0.05146 22.835 0.32253 4.1959 0.95392 7.7946 1.2538 11.258 2.1053 17.16 4.2192 32.287 12.176 45.469 24.104 2.2558 2.0411 4.372 6.6241 9.621 3.868 16.839-8.8419 34.718-11.597 53.603-8.594 16.791 2.6699 31.602 9.4308 44.236 20.636 11.531 10.227 19.84 22.841 25.393 37.236 6.3436 16.445 10.389 33.163 6.0798 49.389 7.9587 8.9321 15.807 16.704 22.421 25.414 9.162 12.065 15.33 25.746 18.144 40.776 0.97046 5.1848 1.9111 10.375 2.8654 15.563m-71.597 71.012c5.5615-5.2284 12.002-9.7986 16.508-15.817 10.474-13.992 14.333-29.916 11.288-47.446-2.2496-12.95-8.1973-24.076-17.243-33.063-12.746-12.663-28.865-18.614-46.786-18.569-69.912 0.17712-139.82 0.56831-209.74 0.96176-15.922 0.089599-29.168 7.4209-39.685 18.296-14.45 14.944-20.408 33.343-16.655 54.368 2.2763 12.754 8.2167 23.748 17.158 32.66 13.299 13.255 30.097 18.653 48.728 18.651 59.321-0.005188 118.64 0.042358 177.96-0.046601 9.5912-0.014374 19.181-0.86588 28.773-0.88855 10.649-0.025146 19.978-3.825 29.687-9.1074z"
|
||||
fill="#EEC170"
|
||||
/>
|
||||
<path
|
||||
d="m162.77 293c15.654 4.3883 20.627 22.967 10.304 34.98-5.3104 6.1795-14.817 8.3208-24.278 5.0472-7.0723-2.4471-12.332-10.362-12.876-17.933-1.0451-14.542 11.089-23.176 21.705-23.046 1.5794 0.019287 3.1517 0.61566 5.1461 0.95184z"
|
||||
fill="#EEC170"
|
||||
/>
|
||||
<path
|
||||
d="m227.18 293.64c7.8499 2.3973 11.938 8.2143 13.524 15.077 1.8591 8.0439-0.44817 15.706-7.1588 21.121-6.7633 5.4572-14.417 6.8794-22.578 3.1483-8.2972-3.7933-12.836-10.849-12.736-19.438 0.1687-14.497 14.13-25.368 28.948-19.908z"
|
||||
fill="#EEC170"
|
||||
/>
|
||||
<path
|
||||
d="m261.57 319.07c-2.495-14.418 4.6853-22.603 14.596-26.108 9.8945-3.4995 23.181 3.4303 26.267 13.779 4.6504 15.591-7.1651 29.064-21.665 28.161-8.5254-0.53088-17.202-6.5094-19.198-15.831z"
|
||||
fill="#EEC170"
|
||||
/>
|
||||
<path
|
||||
d="m336.91 333.41c-9.0175-4.2491-15.337-14.349-13.829-21.682 3.0825-14.989 13.341-20.304 23.018-19.585 10.653 0.79141 17.93 7.407 19.765 17.547 1.9588 10.824-4.1171 19.939-13.494 23.703-5.272 2.1162-10.091 1.5086-15.46 0.017883z"
|
||||
fill="#EEC170"
|
||||
/>
|
||||
|
||||
{/* Wordmark - only show if showText is true */}
|
||||
{showText && (
|
||||
<text
|
||||
x="550"
|
||||
y="355"
|
||||
fontFamily="Arial, Helvetica, sans-serif"
|
||||
fontWeight="700"
|
||||
fontSize="290"
|
||||
letterSpacing="-7"
|
||||
fill={color}
|
||||
>
|
||||
AliasVault
|
||||
</text>
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logo;
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
import type { PasswordSettings } from '@/utils/dist/shared/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
|
||||
@@ -15,7 +17,6 @@ interface IPasswordFieldProps {
|
||||
error?: string;
|
||||
showPassword?: boolean;
|
||||
onShowPasswordChange?: (show: boolean) => void;
|
||||
initialSettings: PasswordSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,13 +30,14 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
|
||||
placeholder,
|
||||
error,
|
||||
showPassword: controlledShowPassword,
|
||||
onShowPasswordChange,
|
||||
initialSettings
|
||||
onShowPasswordChange
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dbContext = useDb();
|
||||
const [internalShowPassword, setInternalShowPassword] = useState(false);
|
||||
const [showConfigDialog, setShowConfigDialog] = useState(false);
|
||||
const [currentSettings, setCurrentSettings] = useState<PasswordSettings>(initialSettings);
|
||||
const [currentSettings, setCurrentSettings] = useState<PasswordSettings | null>(null);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
// Use controlled or uncontrolled showPassword state
|
||||
const showPassword = controlledShowPassword !== undefined ? controlledShowPassword : internalShowPassword;
|
||||
@@ -51,11 +53,24 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
|
||||
}
|
||||
}, [controlledShowPassword, onShowPasswordChange]);
|
||||
|
||||
// Initialize settings only once when component mounts
|
||||
// Load password settings from database
|
||||
useEffect(() => {
|
||||
setCurrentSettings({ ...initialSettings });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); // Only run on mount to avoid resetting user changes
|
||||
/**
|
||||
* Load password settings from the database.
|
||||
*/
|
||||
const loadSettings = async (): Promise<void> => {
|
||||
try {
|
||||
if (dbContext.sqliteClient) {
|
||||
const settings = dbContext.sqliteClient.getPasswordSettings();
|
||||
setCurrentSettings(settings);
|
||||
setIsLoaded(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading password settings:', error);
|
||||
}
|
||||
};
|
||||
void loadSettings();
|
||||
}, [dbContext.sqliteClient]);
|
||||
|
||||
const generatePassword = useCallback((settings: PasswordSettings) => {
|
||||
try {
|
||||
@@ -69,6 +84,9 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
|
||||
}, [onChange, setShowPassword]);
|
||||
|
||||
const handleLengthChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!currentSettings) {
|
||||
return;
|
||||
}
|
||||
const length = parseInt(e.target.value, 10);
|
||||
const newSettings = { ...currentSettings, Length: length };
|
||||
setCurrentSettings(newSettings);
|
||||
@@ -78,6 +96,9 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
|
||||
}, [currentSettings, generatePassword]);
|
||||
|
||||
const handleRegeneratePassword = useCallback(() => {
|
||||
if (!currentSettings) {
|
||||
return;
|
||||
}
|
||||
generatePassword(currentSettings);
|
||||
}, [generatePassword, currentSettings]);
|
||||
|
||||
@@ -98,6 +119,18 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
|
||||
setShowConfigDialog(true);
|
||||
}, []);
|
||||
|
||||
// Don't render until settings are loaded
|
||||
if (!currentSettings || !isLoaded) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label htmlFor={id} className="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
{label}
|
||||
</label>
|
||||
<div className="animate-pulse bg-gray-200 dark:bg-gray-700 h-10 rounded-lg"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Label */}
|
||||
@@ -114,7 +147,7 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="outline-0 shadow-sm border border-gray-300 bg-gray-50 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
className="outline-0 text-sm shadow-sm border border-gray-300 bg-gray-50 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
|
||||
@@ -49,7 +49,7 @@ const UsernameField: React.FC<IUsernameFieldProps> = ({
|
||||
value={value}
|
||||
onChange={handleInputChange}
|
||||
placeholder={placeholder}
|
||||
className="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
className="outline-0 text-sm shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import type { VaultMetadata } from '@/utils/dist/shared/models/metadata';
|
||||
import type { EncryptionKeyDerivationParams, VaultMetadata } from '@/utils/dist/shared/models/metadata';
|
||||
import type { VaultResponse } from '@/utils/dist/shared/models/webapi';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import SqliteClient from '@/utils/SqliteClient';
|
||||
@@ -13,6 +13,8 @@ type DbContextType = {
|
||||
dbInitialized: boolean;
|
||||
dbAvailable: boolean;
|
||||
initializeDatabase: (vaultResponse: VaultResponse, derivedKey: string) => Promise<SqliteClient>;
|
||||
storeEncryptionKey: (derivedKey: string) => Promise<void>;
|
||||
storeEncryptionKeyDerivationParams: (params: EncryptionKeyDerivationParams) => Promise<void>;
|
||||
clearDatabase: () => void;
|
||||
getVaultMetadata: () => Promise<VaultMetadata | null>;
|
||||
setCurrentVaultRevisionNumber: (revisionNumber: number) => Promise<void>;
|
||||
@@ -70,7 +72,6 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
*/
|
||||
const request: StoreVaultRequest = {
|
||||
vaultBlob: vaultResponse.vault.blob,
|
||||
derivedKey: derivedKey,
|
||||
publicEmailDomainList: vaultResponse.vault.publicEmailDomainList,
|
||||
privateEmailDomainList: vaultResponse.vault.privateEmailDomainList,
|
||||
vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber,
|
||||
@@ -145,6 +146,20 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
}
|
||||
}, [dbInitialized, checkStoredVault]);
|
||||
|
||||
/**
|
||||
* Store encryption key in background worker.
|
||||
*/
|
||||
const storeEncryptionKey = useCallback(async (encryptionKey: string) : Promise<void> => {
|
||||
await sendMessage('STORE_ENCRYPTION_KEY', encryptionKey, 'background');
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Store encryption key derivation params in background worker.
|
||||
*/
|
||||
const storeEncryptionKeyDerivationParams = useCallback(async (params: EncryptionKeyDerivationParams) : Promise<void> => {
|
||||
await sendMessage('STORE_ENCRYPTION_KEY_DERIVATION_PARAMS', params, 'background');
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Clear database and remove from background worker, called when logging out.
|
||||
*/
|
||||
@@ -160,11 +175,13 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
dbInitialized,
|
||||
dbAvailable,
|
||||
initializeDatabase,
|
||||
storeEncryptionKey,
|
||||
storeEncryptionKeyDerivationParams,
|
||||
clearDatabase,
|
||||
getVaultMetadata,
|
||||
setCurrentVaultRevisionNumber,
|
||||
hasPendingMigrations,
|
||||
}), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, clearDatabase, getVaultMetadata, setCurrentVaultRevisionNumber, hasPendingMigrations]);
|
||||
}), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, storeEncryptionKey, storeEncryptionKeyDerivationParams, clearDatabase, getVaultMetadata, setCurrentVaultRevisionNumber, hasPendingMigrations]);
|
||||
|
||||
return (
|
||||
<DbContext.Provider value={contextValue}>
|
||||
|
||||
@@ -51,9 +51,15 @@ export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
||||
const segments = location.pathname.split('/').filter(Boolean);
|
||||
const historyEntries: NavigationHistoryEntry[] = [];
|
||||
|
||||
// Build history entries for each segment
|
||||
let currentPath = '';
|
||||
for (const segment of segments) {
|
||||
currentPath += '/' + segment;
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
currentPath += '/' + segments[i];
|
||||
|
||||
/*
|
||||
* For settings subpages, include both /settings and the subpage
|
||||
* For email details, include both /emails and the specific email
|
||||
*/
|
||||
historyEntries.push({
|
||||
pathname: currentPath,
|
||||
search: location.search,
|
||||
|
||||
@@ -47,13 +47,13 @@ export function useVaultMutate() : {
|
||||
// Upload the updated vault to the server.
|
||||
const base64Vault = dbContext.sqliteClient!.exportToBase64();
|
||||
|
||||
// Get derived key from background worker
|
||||
const derivedKey = await sendMessage('GET_DERIVED_KEY', {}, 'background') as string;
|
||||
// Get encryption key from background worker
|
||||
const encryptionKey = await sendMessage('GET_ENCRYPTION_KEY', {}, 'background') as string;
|
||||
|
||||
// Encrypt the vault.
|
||||
const encryptedVaultBlob = await EncryptionUtility.symmetricEncrypt(
|
||||
base64Vault,
|
||||
derivedKey
|
||||
encryptionKey
|
||||
);
|
||||
|
||||
const request: UploadVaultRequest = {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
|
||||
import type { EncryptionKeyDerivationParams } from '@/utils/dist/shared/models/metadata';
|
||||
import type { VaultResponse } from '@/utils/dist/shared/models/webapi';
|
||||
|
||||
/**
|
||||
@@ -81,6 +82,19 @@ export const useVaultSync = () : {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the SRP salt has changed compared to locally stored encryption key derivation params
|
||||
const storedEncryptionParams = await sendMessage('GET_ENCRYPTION_KEY_DERIVATION_PARAMS', {}, 'background') as EncryptionKeyDerivationParams | null;
|
||||
if (storedEncryptionParams && statusResponse.srpSalt && statusResponse.srpSalt !== storedEncryptionParams.salt) {
|
||||
/**
|
||||
* Server SRP salt has changed compared to locally stored value, which means the user has changed
|
||||
* their password since the last time they logged in. This means that the local encryption key is no
|
||||
* longer valid and the user needs to re-authenticate. We trigger a logout but do not revoke tokens
|
||||
* as these were already revoked by the server upon password change.
|
||||
*/
|
||||
await webApi.logout(t('common.errors.passwordChanged'));
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* If we get here, it means we have a valid connection to the server.
|
||||
* TODO: browser extension does not support offline mode yet.
|
||||
@@ -114,9 +128,9 @@ export const useVaultSync = () : {
|
||||
}
|
||||
|
||||
try {
|
||||
// Get derived key from background worker
|
||||
const passwordHashBase64 = await sendMessage('GET_DERIVED_KEY', {}, 'background') as string;
|
||||
const sqliteClient = await dbContext.initializeDatabase(vaultResponseJson as VaultResponse, passwordHashBase64);
|
||||
// Get encryption key from background worker
|
||||
const encryptionKey = await sendMessage('GET_ENCRYPTION_KEY', {}, 'background') as string;
|
||||
const sqliteClient = await dbContext.initializeDatabase(vaultResponseJson as VaultResponse, encryptionKey);
|
||||
|
||||
// Check if the current vault version is known and up to date, if not known trigger an exception, if not up to date redirect to the upgrade page.
|
||||
if (await sqliteClient.hasPendingMigrations()) {
|
||||
|
||||
@@ -51,21 +51,16 @@ const Reinitialize: React.FC = () => {
|
||||
if (lastPage && lastVisitTime) {
|
||||
const timeSinceLastVisit = Date.now() - lastVisitTime;
|
||||
if (timeSinceLastVisit <= PAGE_MEMORY_DURATION) {
|
||||
// Restore the navigation history
|
||||
if (savedHistory?.length) {
|
||||
// First navigate to credentials page as the base
|
||||
navigate('/credentials', { replace: true });
|
||||
|
||||
// Then restore the history stack
|
||||
for (const entry of savedHistory) {
|
||||
navigate(entry.pathname + entry.search + entry.hash);
|
||||
}
|
||||
return;
|
||||
// For nested routes, build up the navigation history properly
|
||||
if (savedHistory?.length > 1) {
|
||||
// Navigate to the base route first
|
||||
navigate(savedHistory[0].pathname, { replace: true });
|
||||
// Then navigate to the final destination
|
||||
navigate(lastPage, { replace: false });
|
||||
} else {
|
||||
// Simple navigation for non-nested routes
|
||||
navigate(lastPage, { replace: true });
|
||||
}
|
||||
|
||||
// Fallback to simple navigation if no history
|
||||
navigate('/credentials', { replace: true });
|
||||
navigate(lastPage, { replace: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,470 +0,0 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import LanguageSwitcher from '@/entrypoints/popup/components/LanguageSwitcher';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useTheme } from '@/entrypoints/popup/context/ThemeContext';
|
||||
import { useApiUrl } from '@/entrypoints/popup/utils/ApiUrlUtility';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
import { DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, GLOBAL_CONTEXT_MENU_ENABLED_KEY, TEMPORARY_DISABLED_SITES_KEY } from '@/utils/Constants';
|
||||
|
||||
import { storage, browser } from "#imports";
|
||||
|
||||
/**
|
||||
* Popup settings type.
|
||||
*/
|
||||
type PopupSettings = {
|
||||
disabledUrls: string[];
|
||||
temporaryDisabledUrls: Record<string, number>;
|
||||
currentUrl: string;
|
||||
isEnabled: boolean;
|
||||
isGloballyEnabled: boolean;
|
||||
isContextMenuEnabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings page component.
|
||||
*/
|
||||
const Settings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const authContext = useAuth();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const { loadApiUrl, getDisplayUrl } = useApiUrl();
|
||||
const navigate = useNavigate();
|
||||
const [settings, setSettings] = useState<PopupSettings>({
|
||||
disabledUrls: [],
|
||||
temporaryDisabledUrls: {},
|
||||
currentUrl: '',
|
||||
isEnabled: true,
|
||||
isGloballyEnabled: true,
|
||||
isContextMenuEnabled: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Get current tab in browser.
|
||||
*/
|
||||
const getCurrentTab = async (): Promise<browser.Tabs.Tab> => {
|
||||
const queryOptions = { active: true, currentWindow: true };
|
||||
const [tab] = await browser.tabs.query(queryOptions);
|
||||
return tab;
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the client tab.
|
||||
*/
|
||||
const openClientTab = async () : Promise<void> => {
|
||||
const settingClientUrl = await storage.getItem('local:clientUrl') as string;
|
||||
let clientUrl = AppInfo.DEFAULT_CLIENT_URL;
|
||||
if (settingClientUrl && settingClientUrl.length > 0) {
|
||||
clientUrl = settingClientUrl;
|
||||
}
|
||||
|
||||
window.open(clientUrl, '_blank');
|
||||
};
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
{!PopoutUtility.isPopup() && (
|
||||
<>
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title={t('settings.openInNewWindow')}
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<HeaderButton
|
||||
onClick={openClientTab}
|
||||
title={t('settings.openWebApp')}
|
||||
iconType={HeaderIconType.EXTERNAL_LINK}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
return () => setHeaderButtons(null);
|
||||
}, [setHeaderButtons, t]);
|
||||
|
||||
/**
|
||||
* Load settings.
|
||||
*/
|
||||
const loadSettings = useCallback(async () : Promise<void> => {
|
||||
const tab = await getCurrentTab();
|
||||
const currentUrl = new URL(tab.url ?? '').hostname;
|
||||
|
||||
// Load settings local storage.
|
||||
const disabledUrls = await storage.getItem(DISABLED_SITES_KEY) as string[] ?? [];
|
||||
const temporaryDisabledUrls = await storage.getItem(TEMPORARY_DISABLED_SITES_KEY) as Record<string, number> ?? {};
|
||||
const isGloballyEnabled = await storage.getItem(GLOBAL_AUTOFILL_POPUP_ENABLED_KEY) !== false; // Default to true if not set
|
||||
const isContextMenuEnabled = await storage.getItem(GLOBAL_CONTEXT_MENU_ENABLED_KEY) !== false; // Default to true if not set
|
||||
|
||||
// Clean up expired temporary disables
|
||||
const now = Date.now();
|
||||
const cleanedTemporaryDisabledUrls = Object.fromEntries(
|
||||
Object.entries(temporaryDisabledUrls).filter(([_, expiry]) => expiry > now)
|
||||
);
|
||||
|
||||
if (Object.keys(cleanedTemporaryDisabledUrls).length !== Object.keys(temporaryDisabledUrls).length) {
|
||||
await storage.setItem(TEMPORARY_DISABLED_SITES_KEY, cleanedTemporaryDisabledUrls);
|
||||
}
|
||||
|
||||
// Load API URL
|
||||
await loadApiUrl();
|
||||
|
||||
setSettings({
|
||||
disabledUrls,
|
||||
temporaryDisabledUrls: cleanedTemporaryDisabledUrls,
|
||||
currentUrl,
|
||||
isEnabled: !disabledUrls.includes(currentUrl) && !(currentUrl in cleanedTemporaryDisabledUrls),
|
||||
isGloballyEnabled,
|
||||
isContextMenuEnabled
|
||||
});
|
||||
setIsInitialLoading(false);
|
||||
}, [setIsInitialLoading, loadApiUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
}, [loadSettings]);
|
||||
|
||||
/**
|
||||
* Toggle current site.
|
||||
*/
|
||||
const toggleCurrentSite = async () : Promise<void> => {
|
||||
const { currentUrl, disabledUrls, temporaryDisabledUrls, isEnabled } = settings;
|
||||
|
||||
let newDisabledUrls = [...disabledUrls];
|
||||
let newTemporaryDisabledUrls = { ...temporaryDisabledUrls };
|
||||
|
||||
if (isEnabled) {
|
||||
// When disabling, add to permanent disabled list
|
||||
if (!newDisabledUrls.includes(currentUrl)) {
|
||||
newDisabledUrls.push(currentUrl);
|
||||
}
|
||||
// Also remove from temporary disabled list if present
|
||||
delete newTemporaryDisabledUrls[currentUrl];
|
||||
} else {
|
||||
// When enabling, remove from both permanent and temporary disabled lists
|
||||
newDisabledUrls = newDisabledUrls.filter(url => url !== currentUrl);
|
||||
delete newTemporaryDisabledUrls[currentUrl];
|
||||
}
|
||||
|
||||
await storage.setItem(DISABLED_SITES_KEY, newDisabledUrls);
|
||||
await storage.setItem(TEMPORARY_DISABLED_SITES_KEY, newTemporaryDisabledUrls);
|
||||
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
disabledUrls: newDisabledUrls,
|
||||
temporaryDisabledUrls: newTemporaryDisabledUrls,
|
||||
isEnabled: !isEnabled
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset settings.
|
||||
*/
|
||||
const resetSettings = async () : Promise<void> => {
|
||||
await storage.setItem(DISABLED_SITES_KEY, []);
|
||||
await storage.setItem(TEMPORARY_DISABLED_SITES_KEY, {});
|
||||
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
disabledUrls: [],
|
||||
temporaryDisabledUrls: {},
|
||||
isEnabled: true
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle global popup.
|
||||
*/
|
||||
const toggleGlobalPopup = async () : Promise<void> => {
|
||||
const newGloballyEnabled = !settings.isGloballyEnabled;
|
||||
|
||||
await storage.setItem(GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, newGloballyEnabled);
|
||||
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
isGloballyEnabled: newGloballyEnabled
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle context menu.
|
||||
*/
|
||||
const toggleContextMenu = async () : Promise<void> => {
|
||||
const newContextMenuEnabled = !settings.isContextMenuEnabled;
|
||||
|
||||
await storage.setItem(GLOBAL_CONTEXT_MENU_ENABLED_KEY, newContextMenuEnabled);
|
||||
await sendMessage('TOGGLE_CONTEXT_MENU', { enabled: newContextMenuEnabled }, 'background');
|
||||
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
isContextMenuEnabled: newContextMenuEnabled
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Set theme preference.
|
||||
*/
|
||||
const setThemePreference = async (newTheme: 'system' | 'light' | 'dark') : Promise<void> => {
|
||||
// Use the ThemeContext to apply the theme
|
||||
setTheme(newTheme);
|
||||
|
||||
// Update local state
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
theme: newTheme
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Open keyboard shortcuts configuration page.
|
||||
*/
|
||||
const openKeyboardShortcuts = async (): Promise<void> => {
|
||||
// Detect browser type using user agent
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
const isFirefox = userAgent.includes('firefox');
|
||||
const isSafari = userAgent.includes('safari') && !userAgent.includes('chrome');
|
||||
|
||||
if (isFirefox) {
|
||||
await browser.tabs.create({ url: 'about:addons' });
|
||||
} else if (isSafari) {
|
||||
await browser.tabs.create({ url: 'safari-extension://shortcuts' });
|
||||
} else {
|
||||
// Chrome and other Chromium-based browsers
|
||||
await browser.tabs.create({ url: 'chrome://extensions/shortcuts' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logout.
|
||||
*/
|
||||
const handleLogout = async () : Promise<void> => {
|
||||
navigate('/logout', { replace: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">{t('settings.title')}</h2>
|
||||
</div>
|
||||
|
||||
{/* User Menu Section */}
|
||||
<section>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||||
<span className="text-primary-600 dark:text-primary-400 text-lg font-medium">
|
||||
{authContext.username?.[0]?.toUpperCase() || '?'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{authContext.username}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{t('settings.loggedIn')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-4 py-2 text-sm font-medium text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
||||
>
|
||||
{t('settings.logout')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Global Settings Section */}
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.globalSettings')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.autofillPopup')}</p>
|
||||
<p className={`text-sm mt-1 ${settings.isGloballyEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{settings.isGloballyEnabled ? t('settings.activeOnAllSites') : t('settings.disabledOnAllSites')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleGlobalPopup}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
settings.isGloballyEnabled
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isGloballyEnabled ? t('settings.enabled') : t('settings.disabled')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.rightClickContextMenu')}</p>
|
||||
<p className={`text-sm mt-1 ${settings.isContextMenuEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{settings.isContextMenuEnabled ? t('settings.enabled') : t('settings.disabled')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleContextMenu}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
settings.isContextMenuEnabled
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isContextMenuEnabled ? t('settings.enabled') : t('settings.disabled')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Site-Specific Settings Section */}
|
||||
{settings.isGloballyEnabled && (
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.siteSpecificSettings')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.autofillPopupOn')}{settings.currentUrl}</p>
|
||||
<p className={`text-sm mt-1 ${settings.isEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{settings.isEnabled ? t('settings.enabledForThisSite') : t('settings.disabledForThisSite')}
|
||||
</p>
|
||||
{!settings.isEnabled && settings.temporaryDisabledUrls[settings.currentUrl] && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{t('settings.temporarilyDisabledUntil')}{new Date(settings.temporaryDisabledUrls[settings.currentUrl]).toLocaleTimeString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{settings.isGloballyEnabled && (
|
||||
<button
|
||||
onClick={toggleCurrentSite}
|
||||
className={`px-4 py-2 ml-1 rounded-md transition-colors ${
|
||||
settings.isEnabled
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isEnabled ? t('settings.enabled') : t('settings.disabled')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={resetSettings}
|
||||
className="w-full px-4 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md text-gray-700 dark:text-gray-300 transition-colors text-sm"
|
||||
>
|
||||
{t('settings.resetAllSiteSettings')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Appearance Settings Section */}
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.appearance')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="mb-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mb-3">{t('settings.language')}</p>
|
||||
<LanguageSwitcher variant="dropdown" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">{t('settings.theme')}</p>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="system"
|
||||
checked={theme === 'system'}
|
||||
onChange={() => setThemePreference('system')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.useDefault')}</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="light"
|
||||
checked={theme === 'light'}
|
||||
onChange={() => setThemePreference('light')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.light')}</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="dark"
|
||||
checked={theme === 'dark'}
|
||||
onChange={() => setThemePreference('dark')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.dark')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Keyboard Shortcuts Section */}
|
||||
{import.meta.env.CHROME && (
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.keyboardShortcuts')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.configureKeyboardShortcuts')}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={openKeyboardShortcuts}
|
||||
className="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md transition-colors"
|
||||
>
|
||||
{t('settings.configure')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="text-center text-gray-400 dark:text-gray-600">
|
||||
{t('settings.versionPrefix')}{AppInfo.VERSION} ({getDisplayUrl()})
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -176,13 +176,13 @@ const AuthSettings: React.FC = () => {
|
||||
{/* Language Settings Section */}
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('common.language')}</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('common.language')}</p>
|
||||
<LanguageSwitcher variant="dropdown" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label htmlFor="api-connection" className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
<label htmlFor="api-connection" className="block font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
{t('settings.serverUrl')}
|
||||
</label>
|
||||
<select
|
||||
@@ -201,7 +201,7 @@ const AuthSettings: React.FC = () => {
|
||||
{selectedOption === 'custom' && (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<label htmlFor="custom-client-url" className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
<label htmlFor="custom-client-url" className="block font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
Custom client URL
|
||||
</label>
|
||||
<input
|
||||
@@ -217,7 +217,7 @@ const AuthSettings: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label htmlFor="custom-api-url" className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
<label htmlFor="custom-api-url" className="block font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
Custom API URL
|
||||
</label>
|
||||
<input
|
||||
@@ -238,7 +238,7 @@ const AuthSettings: React.FC = () => {
|
||||
{/* Autofill Popup Settings Section */}
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.autofillEnabled')}</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.autofillEnabled')}</p>
|
||||
<button
|
||||
onClick={toggleGlobalPopup}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
@@ -6,13 +6,14 @@ import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import { HeaderIcon, HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import LoginServerInfo from '@/entrypoints/popup/components/LoginServerInfo';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import ConversionUtility from '@/entrypoints/popup/utils/ConversionUtility';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
import SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
|
||||
|
||||
@@ -21,8 +22,6 @@ import type { VaultResponse, LoginResponse } from '@/utils/dist/shared/models/we
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import { ApiAuthError } from '@/utils/types/errors/ApiAuthError';
|
||||
|
||||
import ConversionUtility from '../utils/ConversionUtility';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
/**
|
||||
@@ -40,6 +39,7 @@ const Login: React.FC = () => {
|
||||
});
|
||||
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
|
||||
const [rememberMe, setRememberMe] = useState(true);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loginResponse, setLoginResponse] = useState<LoginResponse | null>(null);
|
||||
const [passwordHashString, setPasswordHashString] = useState<string | null>(null);
|
||||
const [passwordHashBase64, setPasswordHashBase64] = useState<string | null>(null);
|
||||
@@ -50,6 +50,66 @@ const Login: React.FC = () => {
|
||||
const webApi = useWebApi();
|
||||
const srpUtil = new SrpUtility(webApi);
|
||||
|
||||
/**
|
||||
* Handle successful authentication by storing tokens and initializing the database
|
||||
*/
|
||||
const handleSuccessfulAuth = async (
|
||||
username: string,
|
||||
token: string,
|
||||
refreshToken: string,
|
||||
passwordHashBase64: string,
|
||||
loginResponse: LoginResponse
|
||||
) : Promise<void> => {
|
||||
// Try to get latest vault manually providing auth token.
|
||||
const vaultResponseJson = await webApi.authFetch<VaultResponse>('Vault', { method: 'GET', headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
} });
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson, t);
|
||||
if (vaultError) {
|
||||
setError(vaultError);
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// All is good. Store auth info which is required to make requests to the web API.
|
||||
await authContext.setAuthTokens(username, token, refreshToken);
|
||||
|
||||
// Store the encryption key and derivation params separately
|
||||
await dbContext.storeEncryptionKey(passwordHashBase64);
|
||||
await dbContext.storeEncryptionKeyDerivationParams({
|
||||
salt: loginResponse.salt,
|
||||
encryptionType: loginResponse.encryptionType,
|
||||
encryptionSettings: loginResponse.encryptionSettings
|
||||
});
|
||||
|
||||
// Initialize the SQLite context with the new vault data.
|
||||
const sqliteClient = await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
|
||||
// Set logged in status to true which refreshes the app.
|
||||
await authContext.login();
|
||||
|
||||
// If there are pending migrations, redirect to the upgrade page.
|
||||
try {
|
||||
if (await sqliteClient.hasPendingMigrations()) {
|
||||
navigate('/upgrade', { replace: true });
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
await authContext.logout();
|
||||
setError(err instanceof Error ? err.message : t('auth.errors.migrationError'));
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to reinitialize page which will take care of the proper redirect.
|
||||
navigate('/reinitialize', { replace: true });
|
||||
|
||||
// Show app.
|
||||
hideLoading();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Load the client URL from the storage.
|
||||
@@ -143,46 +203,14 @@ const Login: React.FC = () => {
|
||||
throw new Error(t('auth.errors.noToken'));
|
||||
}
|
||||
|
||||
// Try to get latest vault manually providing auth token.
|
||||
const vaultResponseJson = await webApi.authFetch<VaultResponse>('Vault', { method: 'GET', headers: {
|
||||
'Authorization': `Bearer ${validationResponse.token.token}`
|
||||
} });
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson, t);
|
||||
if (vaultError) {
|
||||
setError(vaultError);
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// All is good. Store auth info which is required to make requests to the web API.
|
||||
await authContext.setAuthTokens(ConversionUtility.normalizeUsername(credentials.username), validationResponse.token.token, validationResponse.token.refreshToken);
|
||||
|
||||
// Initialize the SQLite context with the new vault data.
|
||||
const sqliteClient = await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
|
||||
// Set logged in status to true which refreshes the app.
|
||||
await authContext.login();
|
||||
|
||||
// If there are pending migrations, redirect to the upgrade page.
|
||||
try {
|
||||
if (await sqliteClient.hasPendingMigrations()) {
|
||||
navigate('/upgrade', { replace: true });
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
await authContext.logout();
|
||||
setError(err instanceof Error ? err.message : t('auth.errors.migrationError'));
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to reinitialize page which will take care of the proper redirect.
|
||||
navigate('/reinitialize', { replace: true });
|
||||
|
||||
// Show app.
|
||||
hideLoading();
|
||||
// Handle successful authentication
|
||||
await handleSuccessfulAuth(
|
||||
ConversionUtility.normalizeUsername(credentials.username),
|
||||
validationResponse.token.token,
|
||||
validationResponse.token.refreshToken,
|
||||
passwordHashBase64,
|
||||
loginResponse
|
||||
);
|
||||
} catch (err) {
|
||||
// Show API authentication errors as-is.
|
||||
if (err instanceof ApiAuthError) {
|
||||
@@ -227,43 +255,14 @@ const Login: React.FC = () => {
|
||||
throw new Error(t('auth.errors.noToken'));
|
||||
}
|
||||
|
||||
// Try to get latest vault manually providing auth token.
|
||||
const vaultResponseJson = await webApi.authFetch<VaultResponse>('Vault', { method: 'GET', headers: {
|
||||
'Authorization': `Bearer ${validationResponse.token.token}`
|
||||
} });
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson, t);
|
||||
if (vaultError) {
|
||||
setError(vaultError);
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// All is good. Store auth info which is required to make requests to the web API.
|
||||
await authContext.setAuthTokens(ConversionUtility.normalizeUsername(credentials.username), validationResponse.token.token, validationResponse.token.refreshToken);
|
||||
|
||||
// Initialize the SQLite context with the new vault data.
|
||||
const sqliteClient = await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
|
||||
// Set logged in status to true which refreshes the app.
|
||||
await authContext.login();
|
||||
|
||||
// If there are pending migrations, redirect to the upgrade page.
|
||||
try {
|
||||
if (await sqliteClient.hasPendingMigrations()) {
|
||||
navigate('/upgrade', { replace: true });
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
await authContext.logout();
|
||||
setError(err instanceof Error ? err.message : t('auth.errors.migrationError'));
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to reinitialize page which will take care of the proper redirect.
|
||||
navigate('/reinitialize', { replace: true });
|
||||
// Handle successful authentication
|
||||
await handleSuccessfulAuth(
|
||||
ConversionUtility.normalizeUsername(credentials.username),
|
||||
validationResponse.token.token,
|
||||
validationResponse.token.refreshToken,
|
||||
passwordHashBase64,
|
||||
loginResponse
|
||||
);
|
||||
|
||||
// Reset 2FA state and login response as it's no longer needed
|
||||
setTwoFactorRequired(false);
|
||||
@@ -271,7 +270,6 @@ const Login: React.FC = () => {
|
||||
setPasswordHashString(null);
|
||||
setPasswordHashBase64(null);
|
||||
setLoginResponse(null);
|
||||
hideLoading();
|
||||
} catch (err) {
|
||||
// Show API authentication errors as-is.
|
||||
console.error('2FA error:', err);
|
||||
@@ -364,11 +362,11 @@ const Login: React.FC = () => {
|
||||
<h2 className="text-xl font-bold dark:text-gray-200">{t('auth.loginTitle')}</h2>
|
||||
<LoginServerInfo />
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="username">
|
||||
<label className="block text-gray-700 dark:text-gray-200 font-bold mb-2" htmlFor="username">
|
||||
{t('auth.username')}
|
||||
</label>
|
||||
<input
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 leading-tight focus:outline-none focus:shadow-outline"
|
||||
className="shadow text-sm appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="username"
|
||||
type="text"
|
||||
name="username"
|
||||
@@ -379,19 +377,29 @@ const Login: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="password">
|
||||
<label className="block text-gray-700 dark:text-gray-200 font-bold mb-2" htmlFor="password">
|
||||
{t('auth.password')}
|
||||
</label>
|
||||
<input
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 mb-3 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder={t('auth.passwordPlaceholder')}
|
||||
value={credentials.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
className="shadow text-sm appearance-none border rounded w-full py-2 px-3 pr-10 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 mb-3 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
name="password"
|
||||
placeholder={t('auth.passwordPlaceholder')}
|
||||
value={credentials.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<HeaderIcon type={showPassword ? HeaderIconType.EYE_OFF : HeaderIconType.EYE} className="w-5 h-5 text-gray-400 dark:text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="flex items-center">
|
||||
@@ -410,7 +418,7 @@ const Login: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="text-center text-gray-600 dark:text-gray-400">
|
||||
{t('auth.noAccount')}{' '}
|
||||
<a
|
||||
href={clientUrl ?? ''}
|
||||
@@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import { HeaderIcon, HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
@@ -35,6 +35,7 @@ const Unlock: React.FC = () => {
|
||||
const srpUtil = new SrpUtility(webApi);
|
||||
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
|
||||
|
||||
@@ -46,7 +47,7 @@ const Unlock: React.FC = () => {
|
||||
const statusResponse = await webApi.getStatus();
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
await webApi.logout(t('common.apiErrors.' + statusError));
|
||||
await webApi.logout(t('common.errors.' + statusError));
|
||||
navigate('/logout');
|
||||
}
|
||||
setIsInitialLoading(false);
|
||||
@@ -105,6 +106,9 @@ const Unlock: React.FC = () => {
|
||||
// Get the derived key as base64 string required for decryption.
|
||||
const passwordHashBase64 = Buffer.from(passwordHash).toString('base64');
|
||||
|
||||
// Store the encryption key in session storage.
|
||||
await dbContext.storeEncryptionKey(passwordHashBase64);
|
||||
|
||||
// Initialize the SQLite context with the new vault data.
|
||||
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
|
||||
@@ -141,10 +145,10 @@ const Unlock: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{authContext.username}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('auth.loggedIn')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -156,31 +160,42 @@ const Unlock: React.FC = () => {
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
|
||||
<div className="mb-4 text-red-500 dark:text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-2">
|
||||
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="password">
|
||||
<label className="block text-gray-700 dark:text-gray-200 font-bold mb-2" htmlFor="password">
|
||||
{t('auth.masterPassword')}
|
||||
</label>
|
||||
<input
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 mb-3 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t('auth.passwordPlaceholder')}
|
||||
required
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 pr-10 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 mb-3 leading-tight focus:outline-none focus:shadow-outline"
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t('auth.passwordPlaceholder')}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<HeaderIcon type={showPassword ? HeaderIconType.EYE_OFF : HeaderIconType.EYE} className="w-5 h-5 text-gray-400 dark:text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="submit">
|
||||
{t('auth.unlockVault')}
|
||||
</Button>
|
||||
|
||||
<div className="text-sm font-medium text-gray-500 dark:text-gray-200 mt-6">
|
||||
<div className="font-medium text-gray-500 dark:text-gray-200 mt-6">
|
||||
{t('auth.switchAccounts')} <button onClick={handleLogout} className="text-primary-700 hover:underline dark:text-primary-500">{t('auth.logout')}</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -239,7 +239,7 @@ const Upgrade: React.FC = () => {
|
||||
title={t('upgrade.alerts.selfHostedServer')}
|
||||
message={t('upgrade.alerts.selfHostedWarning')}
|
||||
confirmText={t('upgrade.alerts.continueUpgrade')}
|
||||
cancelText={t('upgrade.alerts.cancel')}
|
||||
cancelText={t('common.cancel')}
|
||||
/>
|
||||
|
||||
{/* Version info modal */}
|
||||
@@ -253,7 +253,7 @@ const Upgrade: React.FC = () => {
|
||||
|
||||
<form className="w-full px-2 pt-2 pb-2 mb-4">
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
|
||||
<div className="mb-4 text-red-500 dark:text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@@ -268,7 +268,7 @@ const Upgrade: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{username}
|
||||
</p>
|
||||
</div>
|
||||
@@ -277,12 +277,12 @@ const Upgrade: React.FC = () => {
|
||||
<h2 className="text-xl font-bold dark:text-gray-200 mb-4">{t('upgrade.title')}</h2>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-700 dark:text-gray-200 text-sm mb-4">
|
||||
<p className="text-gray-700 dark:text-gray-200 mb-4">
|
||||
{t('upgrade.subtitle')}
|
||||
</p>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded p-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">{t('upgrade.versionInformation')}</span>
|
||||
<span className="font-medium text-gray-700 dark:text-gray-200">{t('upgrade.versionInformation')}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={showVersionDialog}
|
||||
@@ -9,6 +9,7 @@ import { sendMessage } from 'webext-bridge/popup';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import AttachmentUploader from '@/entrypoints/popup/components/CredentialDetails/AttachmentUploader';
|
||||
import EmailDomainField from '@/entrypoints/popup/components/EmailDomainField';
|
||||
import { FormInput } from '@/entrypoints/popup/components/FormInput';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
@@ -22,9 +23,13 @@ import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
|
||||
|
||||
import { SKIP_FORM_RESTORE_KEY } from '@/utils/Constants';
|
||||
import { IdentityHelperUtils, CreateIdentityGenerator, CreateUsernameEmailGenerator, Identity, Gender } from '@/utils/dist/shared/identity-generator';
|
||||
import type { Attachment, Credential } from '@/utils/dist/shared/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
import { ServiceDetectionUtility } from '@/utils/serviceDetection/ServiceDetectionUtility';
|
||||
|
||||
import { browser } from '#imports';
|
||||
|
||||
type CredentialMode = 'random' | 'manual';
|
||||
|
||||
@@ -52,7 +57,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
const credentialSchema = useMemo(() => Yup.object().shape({
|
||||
Id: Yup.string(),
|
||||
ServiceName: Yup.string().required(t('credentials.validation.serviceNameRequired')),
|
||||
ServiceUrl: Yup.string().url(t('credentials.validation.invalidUrl')).nullable().optional(),
|
||||
ServiceUrl: Yup.string().nullable().optional(),
|
||||
Alias: Yup.object().shape({
|
||||
FirstName: Yup.string().nullable().optional(),
|
||||
LastName: Yup.string().nullable().optional(),
|
||||
@@ -89,6 +94,13 @@ const CredentialAddEdit: React.FC = () => {
|
||||
const [originalAttachmentIds, setOriginalAttachmentIds] = useState<string[]>([]);
|
||||
const webApi = useWebApi();
|
||||
|
||||
// Track last generated values to avoid overwriting manual entries
|
||||
const [lastGeneratedValues, setLastGeneratedValues] = useState<{
|
||||
username: string | null;
|
||||
password: string | null;
|
||||
email: string | null;
|
||||
}>({ username: null, password: null, email: null });
|
||||
|
||||
const serviceNameRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { handleSubmit, setValue, watch, formState: { errors } } = useForm<Credential>({
|
||||
@@ -98,7 +110,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
Username: "",
|
||||
Password: "",
|
||||
ServiceName: "",
|
||||
ServiceUrl: "",
|
||||
ServiceUrl: "https://",
|
||||
Notes: "",
|
||||
Alias: {
|
||||
FirstName: "",
|
||||
@@ -222,20 +234,80 @@ const CredentialAddEdit: React.FC = () => {
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
// On create mode, focus the service name field after a short delay to ensure the component is mounted.
|
||||
// On create mode, check for URL parameters first, then fallback to tab detection
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const serviceName = urlParams.get('serviceName');
|
||||
const serviceUrl = urlParams.get('serviceUrl');
|
||||
const currentUrl = urlParams.get('currentUrl');
|
||||
|
||||
/**
|
||||
* Initialize service detection from URL parameters or current tab
|
||||
*/
|
||||
const initializeServiceDetection = async (): Promise<void> => {
|
||||
try {
|
||||
// If URL parameters are present (e.g., from content script popout), use them
|
||||
if (serviceName || serviceUrl || currentUrl) {
|
||||
if (serviceName) {
|
||||
setValue('ServiceName', decodeURIComponent(serviceName));
|
||||
}
|
||||
if (serviceUrl) {
|
||||
setValue('ServiceUrl', decodeURIComponent(serviceUrl));
|
||||
}
|
||||
|
||||
// If we have currentUrl but missing serviceName or serviceUrl, derive them
|
||||
if (currentUrl && (!serviceName || !serviceUrl)) {
|
||||
const decodedCurrentUrl = decodeURIComponent(currentUrl);
|
||||
const serviceInfo = ServiceDetectionUtility.getServiceInfoFromTab(decodedCurrentUrl);
|
||||
|
||||
if (!serviceName && serviceInfo.suggestedNames.length > 0) {
|
||||
setValue('ServiceName', serviceInfo.suggestedNames[0]);
|
||||
}
|
||||
if (!serviceUrl && serviceInfo.serviceUrl) {
|
||||
setValue('ServiceUrl', serviceInfo.serviceUrl);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, detect from current active tab (for dashboard case)
|
||||
const [activeTab] = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
if (activeTab?.url) {
|
||||
const serviceInfo = ServiceDetectionUtility.getServiceInfoFromTab(
|
||||
activeTab.url,
|
||||
activeTab.title
|
||||
);
|
||||
|
||||
if (serviceInfo.suggestedNames.length > 0) {
|
||||
setValue('ServiceName', serviceInfo.suggestedNames[0]);
|
||||
}
|
||||
if (serviceInfo.serviceUrl) {
|
||||
setValue('ServiceUrl', serviceInfo.serviceUrl);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error detecting service information:', error);
|
||||
}
|
||||
};
|
||||
|
||||
initializeServiceDetection();
|
||||
|
||||
// Focus the service name field after a short delay to ensure the component is mounted.
|
||||
setTimeout(() => {
|
||||
serviceNameRef.current?.focus();
|
||||
}, 100);
|
||||
setIsInitialLoading(false);
|
||||
|
||||
// Load persisted form values if they exist.
|
||||
loadPersistedValues().then(() => {
|
||||
// Generate default password if no persisted password exists
|
||||
if (!watch('Password')) {
|
||||
const passwordSettings = dbContext.sqliteClient!.getPasswordSettings();
|
||||
const passwordGenerator = CreatePasswordGenerator(passwordSettings);
|
||||
const defaultPassword = passwordGenerator.generateRandomPassword();
|
||||
setValue('Password', defaultPassword);
|
||||
// Check if we should skip form restoration (e.g., when opened from popout button)
|
||||
browser.storage.local.get([SKIP_FORM_RESTORE_KEY]).then((result) => {
|
||||
if (result[SKIP_FORM_RESTORE_KEY]) {
|
||||
// Clear the flag after using it
|
||||
browser.storage.local.remove([SKIP_FORM_RESTORE_KEY]);
|
||||
// Don't load persisted values, but set local loading to false
|
||||
setLocalLoading(false);
|
||||
} else {
|
||||
// Load persisted form values normally
|
||||
loadPersistedValues();
|
||||
}
|
||||
});
|
||||
return;
|
||||
@@ -270,7 +342,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
console.error('Error loading credential:', err);
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue, loadPersistedValues, watch]);
|
||||
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue, loadPersistedValues, clearPersistedValues]);
|
||||
|
||||
/**
|
||||
* Handle the delete button click.
|
||||
@@ -330,35 +402,63 @@ const CredentialAddEdit: React.FC = () => {
|
||||
const defaultEmailDomain = dbContext.sqliteClient!.getDefaultEmailDomain(privateEmailDomains, publicEmailDomains);
|
||||
const email = defaultEmailDomain ? `${identity.emailPrefix}@${defaultEmailDomain}` : identity.emailPrefix;
|
||||
|
||||
setValue('Alias.Email', email);
|
||||
// Check current values
|
||||
const currentUsername = watch('Username') ?? '';
|
||||
const currentPassword = watch('Password') ?? '';
|
||||
const currentEmail = watch('Alias.Email') ?? '';
|
||||
|
||||
// Only overwrite email if it's empty or matches the last generated value
|
||||
if (!currentEmail || currentEmail === lastGeneratedValues.email) {
|
||||
setValue('Alias.Email', email);
|
||||
}
|
||||
setValue('Alias.FirstName', identity.firstName);
|
||||
setValue('Alias.LastName', identity.lastName);
|
||||
setValue('Alias.NickName', identity.nickName);
|
||||
setValue('Alias.Gender', identity.gender);
|
||||
setValue('Alias.BirthDate', IdentityHelperUtils.normalizeBirthDateForDisplay(identity.birthDate.toISOString()));
|
||||
|
||||
// In edit mode, preserve existing username and password if they exist
|
||||
if (isEditMode && watch('Username')) {
|
||||
// Keep the existing username in edit mode, so don't do anything here.
|
||||
} else {
|
||||
// Use the newly generated username
|
||||
// Only overwrite username if it's empty or matches the last generated value
|
||||
if (!currentUsername || currentUsername === lastGeneratedValues.username) {
|
||||
setValue('Username', identity.nickName);
|
||||
}
|
||||
|
||||
if (isEditMode && watch('Password')) {
|
||||
// Keep the existing password in edit mode, so don't do anything here.
|
||||
} else {
|
||||
// Use the newly generated password
|
||||
// Only overwrite password if it's empty or matches the last generated value
|
||||
if (!currentPassword || currentPassword === lastGeneratedValues.password) {
|
||||
setValue('Password', password);
|
||||
}
|
||||
}, [isEditMode, watch, setValue, initializeGenerators, dbContext]);
|
||||
|
||||
// Update tracking with new generated values
|
||||
setLastGeneratedValues({
|
||||
username: identity.nickName,
|
||||
password: password,
|
||||
email: email
|
||||
});
|
||||
}, [watch, setValue, initializeGenerators, dbContext, lastGeneratedValues, setLastGeneratedValues]);
|
||||
|
||||
/**
|
||||
* Clear all alias fields.
|
||||
*/
|
||||
const clearAliasFields = useCallback(() => {
|
||||
setValue('Alias.FirstName', '');
|
||||
setValue('Alias.LastName', '');
|
||||
setValue('Alias.NickName', '');
|
||||
setValue('Alias.Gender', '');
|
||||
setValue('Alias.BirthDate', '');
|
||||
}, [setValue]);
|
||||
|
||||
// Check if any alias fields have values.
|
||||
const hasAliasValues = !!(watch('Alias.FirstName') || watch('Alias.LastName') || watch('Alias.NickName') || watch('Alias.Gender') || watch('Alias.BirthDate'));
|
||||
|
||||
/**
|
||||
* Handle the generate random alias button press.
|
||||
*/
|
||||
const handleGenerateRandomAlias = useCallback(() => {
|
||||
void generateRandomAlias();
|
||||
}, [generateRandomAlias]);
|
||||
if (hasAliasValues) {
|
||||
clearAliasFields();
|
||||
} else {
|
||||
void generateRandomAlias();
|
||||
}
|
||||
}, [generateRandomAlias, clearAliasFields, hasAliasValues]);
|
||||
|
||||
const generateRandomUsername = useCallback(async () => {
|
||||
try {
|
||||
@@ -381,15 +481,17 @@ const CredentialAddEdit: React.FC = () => {
|
||||
};
|
||||
|
||||
const username = usernameEmailGenerator.generateUsername(identity);
|
||||
setValue('Username', username);
|
||||
const currentUsername = watch('Username') ?? '';
|
||||
// Only overwrite username if it's empty or matches the last generated value
|
||||
if (!currentUsername || currentUsername === lastGeneratedValues.username) {
|
||||
setValue('Username', username);
|
||||
// Update the tracking for username
|
||||
setLastGeneratedValues(prev => ({ ...prev, username: username }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating random username:', error);
|
||||
}
|
||||
}, [setValue, watch]);
|
||||
|
||||
const initialPasswordSettings = useMemo(() => {
|
||||
return dbContext.sqliteClient?.getPasswordSettings();
|
||||
}, [dbContext.sqliteClient]);
|
||||
}, [setValue, watch, lastGeneratedValues, setLastGeneratedValues]);
|
||||
|
||||
/**
|
||||
* Handle form submission.
|
||||
@@ -401,6 +503,11 @@ const CredentialAddEdit: React.FC = () => {
|
||||
birthdate = IdentityHelperUtils.normalizeBirthDateForDb(birthdate);
|
||||
}
|
||||
|
||||
// Clean up empty protocol-only URLs
|
||||
if (data.ServiceUrl === 'http://' || data.ServiceUrl === 'https://') {
|
||||
data.ServiceUrl = '';
|
||||
}
|
||||
|
||||
// If we're creating a new credential and mode is random, generate random values here
|
||||
if (!isEditMode && mode === 'random') {
|
||||
// Generate random values now and then read them from the form fields to manually assign to the credentialToSave object
|
||||
@@ -413,6 +520,9 @@ const CredentialAddEdit: React.FC = () => {
|
||||
data.Alias.BirthDate = birthdate;
|
||||
data.Alias.Gender = watch('Alias.Gender');
|
||||
data.Alias.Email = watch('Alias.Email');
|
||||
// Clean up ServiceUrl for random mode too
|
||||
const serviceUrl = watch('ServiceUrl');
|
||||
data.ServiceUrl = (serviceUrl === 'http://' || serviceUrl === 'https://') ? '' : serviceUrl;
|
||||
}
|
||||
|
||||
// Extract favicon from service URL if the credential has one
|
||||
@@ -527,8 +637,8 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode('random')}
|
||||
className={`flex-1 py-2 px-4 rounded flex items-center justify-center gap-2 ${
|
||||
mode === 'random' ? 'bg-primary-500 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
className={`flex-1 py-2 text-sm px-4 rounded flex items-center justify-center gap-2 ${
|
||||
mode === 'random' ? 'bg-primary-500 text-white font-medium' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<svg className='w-5 h-5' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
@@ -544,8 +654,8 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode('manual')}
|
||||
className={`flex-1 py-2 px-4 rounded flex items-center justify-center gap-2 ${
|
||||
mode === 'manual' ? 'bg-primary-500 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
className={`flex-1 py-2 text-sm px-4 rounded flex items-center justify-center gap-2 ${
|
||||
mode === 'manual' ? 'bg-primary-500 text-white font-medium' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
@@ -585,11 +695,11 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">{t('credentials.loginCredentials')}</h2>
|
||||
<div className="space-y-4">
|
||||
<FormInput
|
||||
<EmailDomainField
|
||||
id="email"
|
||||
label={t('common.email')}
|
||||
value={watch('Alias.Email') ?? ''}
|
||||
onChange={(value) => setValue('Alias.Email', value)}
|
||||
onChange={(value: string) => setValue('Alias.Email', value)}
|
||||
error={errors.Alias?.Email?.message}
|
||||
/>
|
||||
<UsernameField
|
||||
@@ -600,18 +710,15 @@ const CredentialAddEdit: React.FC = () => {
|
||||
error={errors.Username?.message}
|
||||
onRegenerate={generateRandomUsername}
|
||||
/>
|
||||
{initialPasswordSettings && (
|
||||
<PasswordField
|
||||
id="password"
|
||||
label={t('common.password')}
|
||||
value={watch('Password') ?? ''}
|
||||
onChange={(value) => setValue('Password', value)}
|
||||
error={errors.Password?.message}
|
||||
showPassword={showPassword}
|
||||
onShowPasswordChange={setShowPassword}
|
||||
initialSettings={initialPasswordSettings}
|
||||
/>
|
||||
)}
|
||||
<PasswordField
|
||||
id="password"
|
||||
label={t('common.password')}
|
||||
value={watch('Password') ?? ''}
|
||||
onChange={(value) => setValue('Password', value)}
|
||||
error={errors.Password?.message}
|
||||
showPassword={showPassword}
|
||||
onShowPasswordChange={setShowPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -621,17 +728,33 @@ const CredentialAddEdit: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerateRandomAlias}
|
||||
className="w-full bg-primary-500 text-white py-2 px-4 rounded hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 flex items-center justify-center gap-2"
|
||||
className={`w-full text-sm py-2 px-4 rounded focus:outline-none focus:ring-2 focus:ring-offset-2 flex items-center justify-center gap-2 ${
|
||||
hasAliasValues
|
||||
? 'bg-gray-500 text-white hover:bg-gray-600 focus:ring-gray-500'
|
||||
: 'bg-primary-500 text-white hover:bg-primary-600 focus:ring-primary-500'
|
||||
}`}
|
||||
>
|
||||
<svg className='w-5 h-5 inline-block' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
<circle cx="8" cy="8" r="1"/>
|
||||
<circle cx="16" cy="8" r="1"/>
|
||||
<circle cx="12" cy="12" r="1"/>
|
||||
<circle cx="8" cy="16" r="1"/>
|
||||
<circle cx="16" cy="16" r="1"/>
|
||||
</svg>
|
||||
<span>{t('credentials.generateRandomAlias')}</span>
|
||||
{hasAliasValues ? (
|
||||
<>
|
||||
<svg className='w-5 h-5 inline-block' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
<span>{t('credentials.clearAliasFields')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className='w-5 h-5 inline-block' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
<circle cx="8" cy="8" r="1"/>
|
||||
<circle cx="16" cy="8" r="1"/>
|
||||
<circle cx="12" cy="12" r="1"/>
|
||||
<circle cx="8" cy="16" r="1"/>
|
||||
<circle cx="16" cy="16" r="1"/>
|
||||
</svg>
|
||||
<span>{t('credentials.generateRandomAlias')}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<FormInput
|
||||
id="firstName"
|
||||
@@ -172,24 +172,26 @@ const CredentialsList: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{credentials.length > 0 ? (
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('credentials.searchPlaceholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
autoFocus
|
||||
className="w-full p-2 mb-4 border dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
<div className="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder={`${t('content.searchVault')}`}
|
||||
autoFocus
|
||||
className="w-full p-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{credentials.length === 0 ? (
|
||||
<div className="text-gray-500 dark:text-gray-400 space-y-2 mb-10">
|
||||
<p className="text-sm">
|
||||
<p>
|
||||
{t('credentials.welcomeTitle')}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
<p>
|
||||
{t('credentials.welcomeDescription')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -16,8 +16,8 @@ import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
|
||||
import HeaderButton from '../components/HeaderButton';
|
||||
import { HeaderIconType } from '../components/Icons/HeaderIcons';
|
||||
import HeaderButton from '../../components/HeaderButton';
|
||||
import { HeaderIconType } from '../../components/Icons/HeaderIcons';
|
||||
|
||||
/**
|
||||
* Email details page.
|
||||
@@ -32,6 +32,7 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
const [email, setEmail] = useState<Email | null>(null);
|
||||
const [isLoading, setIsLoading] = useMinDurationLoading(true, 150);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [showMetadata, setShowMetadata] = useState(false);
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const [headerButtonsConfigured, setHeaderButtonsConfigured] = useState(false);
|
||||
@@ -207,21 +208,44 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
variant="danger"
|
||||
/>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md">
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">{email.subject}</h1>
|
||||
</div>
|
||||
<div className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<p>{t('emails.from')} {email.fromDisplay} ({email.fromLocal}@{email.fromDomain})</p>
|
||||
<p>{t('emails.to')} {email.toLocal}@{email.toDomain}</p>
|
||||
<p>{t('emails.date')} {new Date(email.dateSystem).toLocaleString()}</p>
|
||||
<div>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-bold text-gray-900 dark:text-white">{email.subject}</h1>
|
||||
<button
|
||||
onClick={() => setShowMetadata(!showMetadata)}
|
||||
className="p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
title={showMetadata ? t('common.hideDetails') : t('common.showDetails')}
|
||||
>
|
||||
<svg
|
||||
className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform ${showMetadata ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{showMetadata && (
|
||||
<div className="space-y-1 text-sm text-gray-600 dark:text-gray-400 mt-2">
|
||||
<p><span className="font-bold">{t('emails.from')}</span> <span title={email.fromLocal + "@" + email.fromDomain}>{email.fromDisplay}</span></p>
|
||||
<p><span className="font-bold">{t('emails.to')}</span> <span title={email.toLocal + "@" + email.toDomain}>{email.toLocal}@{email.toDomain}</span></p>
|
||||
<p><span className="font-bold">{t('emails.date')}</span> {new Date(email.dateSystem).toLocaleString()}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Email Body */}
|
||||
<div className="bg-white">
|
||||
<div className="bg-white mt-4">
|
||||
{email.messageHtml ? (
|
||||
<iframe
|
||||
srcDoc={ConversionUtility.convertAnchorTagsToOpenInNewTab(email.messageHtml)}
|
||||
@@ -177,14 +177,14 @@ const EmailsList: React.FC = () => {
|
||||
className="block p-4 bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div className="text-sm text-gray-900 dark:text-white mb-1 font-bold">
|
||||
<div className="text-gray-900 dark:text-white mb-1 font-bold">
|
||||
{email.subject}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{formatEmailDate(email.dateSystem)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300 line-clamp-2">
|
||||
<div className="text-gray-600 text-sm dark:text-gray-300 line-clamp-2">
|
||||
{email.messagePreview}
|
||||
</div>
|
||||
</Link>
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import HelpModal from '@/entrypoints/popup/components/HelpModal';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
import { AUTO_LOCK_TIMEOUT_KEY } from '@/utils/Constants';
|
||||
|
||||
import { storage } from "#imports";
|
||||
|
||||
/**
|
||||
* Auto-lock settings page component.
|
||||
*/
|
||||
const AutoLockSettings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const [autoLockTimeout, setAutoLockTimeout] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Load auto-lock settings.
|
||||
*/
|
||||
const loadSettings = async () : Promise<void> => {
|
||||
// Load auto-lock timeout
|
||||
const autoLockTimeoutValue = await storage.getItem(AUTO_LOCK_TIMEOUT_KEY) as number ?? 0;
|
||||
setAutoLockTimeout(autoLockTimeoutValue);
|
||||
setIsInitialLoading(false);
|
||||
};
|
||||
|
||||
loadSettings();
|
||||
}, [setIsInitialLoading]);
|
||||
|
||||
/**
|
||||
* Set auto-lock timeout.
|
||||
*/
|
||||
const setAutoLockTimeoutSetting = async (timeout: number) : Promise<void> => {
|
||||
await storage.setItem(AUTO_LOCK_TIMEOUT_KEY, timeout);
|
||||
await sendMessage('SET_AUTO_LOCK_TIMEOUT', timeout, 'background');
|
||||
setAutoLockTimeout(timeout);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.autoLockTimeout')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<div className="flex items-center mb-2">
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.autoLockTimeout')}</p>
|
||||
<HelpModal
|
||||
titleKey="settings.autoLockTimeout"
|
||||
contentKey="settings.autoLockTimeoutHelp"
|
||||
className="ml-2"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">{t('settings.autoLockTimeoutDescription')}</p>
|
||||
<select
|
||||
value={autoLockTimeout}
|
||||
onChange={(e) => setAutoLockTimeoutSetting(Number(e.target.value))}
|
||||
className="w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-gray-900 dark:text-white focus:ring-primary-500 focus:border-primary-500"
|
||||
>
|
||||
<option value="0">{t('settings.autoLockNever')}</option>
|
||||
<option value="15">{t('settings.autoLock15Seconds')}</option>
|
||||
<option value="60">{t('settings.autoLock1Minute')}</option>
|
||||
<option value="300">{t('settings.autoLock5Minutes')}</option>
|
||||
<option value="900">{t('settings.autoLock15Minutes')}</option>
|
||||
<option value="1800">{t('settings.autoLock30Minutes')}</option>
|
||||
<option value="3600">{t('settings.autoLock1Hour')}</option>
|
||||
<option value="14400">{t('settings.autoLock4Hours')}</option>
|
||||
<option value="28800">{t('settings.autoLock8Hours')}</option>
|
||||
<option value="86400">{t('settings.autoLock24Hours')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoLockSettings;
|
||||
@@ -0,0 +1,260 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { AutofillMatchingMode } from '@/entrypoints/contentScript/Filter';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
import {
|
||||
DISABLED_SITES_KEY,
|
||||
GLOBAL_AUTOFILL_POPUP_ENABLED_KEY,
|
||||
TEMPORARY_DISABLED_SITES_KEY,
|
||||
AUTOFILL_MATCHING_MODE_KEY
|
||||
} from '@/utils/Constants';
|
||||
|
||||
import { storage, browser } from "#imports";
|
||||
|
||||
/**
|
||||
* Autofill settings type.
|
||||
*/
|
||||
type AutofillSettingsType = {
|
||||
disabledUrls: string[];
|
||||
temporaryDisabledUrls: Record<string, number>;
|
||||
currentUrl: string;
|
||||
isEnabled: boolean;
|
||||
isGloballyEnabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Autofill settings page component.
|
||||
*/
|
||||
const AutofillSettings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const [settings, setSettings] = useState<AutofillSettingsType>({
|
||||
disabledUrls: [],
|
||||
temporaryDisabledUrls: {},
|
||||
currentUrl: '',
|
||||
isEnabled: true,
|
||||
isGloballyEnabled: true
|
||||
});
|
||||
const [autofillMatchingMode, setAutofillMatchingMode] = useState<AutofillMatchingMode>(AutofillMatchingMode.DEFAULT);
|
||||
|
||||
/**
|
||||
* Get current tab in browser.
|
||||
*/
|
||||
const getCurrentTab = async () : Promise<chrome.tabs.Tab> => {
|
||||
const queryOptions = { active: true, currentWindow: true };
|
||||
const [tab] = await browser.tabs.query(queryOptions);
|
||||
return tab;
|
||||
};
|
||||
|
||||
/**
|
||||
* Load settings.
|
||||
*/
|
||||
const loadSettings = useCallback(async () : Promise<void> => {
|
||||
const tab = await getCurrentTab();
|
||||
const currentUrl = new URL(tab.url ?? '').hostname;
|
||||
|
||||
// Load settings local storage.
|
||||
const disabledUrls = await storage.getItem(DISABLED_SITES_KEY) as string[] ?? [];
|
||||
const temporaryDisabledUrls = await storage.getItem(TEMPORARY_DISABLED_SITES_KEY) as Record<string, number> ?? {};
|
||||
const isGloballyEnabled = await storage.getItem(GLOBAL_AUTOFILL_POPUP_ENABLED_KEY) !== false; // Default to true if not set
|
||||
|
||||
// Clean up expired temporary disables
|
||||
const now = Date.now();
|
||||
const cleanedTemporaryDisabledUrls = Object.fromEntries(
|
||||
Object.entries(temporaryDisabledUrls).filter(([_, expiry]) => expiry > now)
|
||||
);
|
||||
|
||||
if (Object.keys(cleanedTemporaryDisabledUrls).length !== Object.keys(temporaryDisabledUrls).length) {
|
||||
await storage.setItem(TEMPORARY_DISABLED_SITES_KEY, cleanedTemporaryDisabledUrls);
|
||||
}
|
||||
|
||||
// Load autofill matching mode
|
||||
const matchingModeValue = await storage.getItem(AUTOFILL_MATCHING_MODE_KEY) as AutofillMatchingMode ?? AutofillMatchingMode.DEFAULT;
|
||||
setAutofillMatchingMode(matchingModeValue);
|
||||
|
||||
setSettings({
|
||||
disabledUrls,
|
||||
temporaryDisabledUrls: cleanedTemporaryDisabledUrls,
|
||||
currentUrl,
|
||||
isEnabled: !disabledUrls.includes(currentUrl) && !(currentUrl in cleanedTemporaryDisabledUrls),
|
||||
isGloballyEnabled
|
||||
});
|
||||
setIsInitialLoading(false);
|
||||
}, [setIsInitialLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
}, [loadSettings]);
|
||||
|
||||
/**
|
||||
* Toggle current site.
|
||||
*/
|
||||
const toggleCurrentSite = async () : Promise<void> => {
|
||||
const { currentUrl, disabledUrls, temporaryDisabledUrls, isEnabled } = settings;
|
||||
|
||||
let newDisabledUrls = [...disabledUrls];
|
||||
let newTemporaryDisabledUrls = { ...temporaryDisabledUrls };
|
||||
|
||||
if (isEnabled) {
|
||||
// When disabling, add to permanent disabled list
|
||||
if (!newDisabledUrls.includes(currentUrl)) {
|
||||
newDisabledUrls.push(currentUrl);
|
||||
}
|
||||
// Also remove from temporary disabled list if present
|
||||
delete newTemporaryDisabledUrls[currentUrl];
|
||||
} else {
|
||||
// When enabling, remove from both permanent and temporary disabled lists
|
||||
newDisabledUrls = newDisabledUrls.filter(url => url !== currentUrl);
|
||||
delete newTemporaryDisabledUrls[currentUrl];
|
||||
}
|
||||
|
||||
await storage.setItem(DISABLED_SITES_KEY, newDisabledUrls);
|
||||
await storage.setItem(TEMPORARY_DISABLED_SITES_KEY, newTemporaryDisabledUrls);
|
||||
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
disabledUrls: newDisabledUrls,
|
||||
temporaryDisabledUrls: newTemporaryDisabledUrls,
|
||||
isEnabled: !isEnabled
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset settings.
|
||||
*/
|
||||
const resetSettings = async () : Promise<void> => {
|
||||
await storage.setItem(DISABLED_SITES_KEY, []);
|
||||
await storage.setItem(TEMPORARY_DISABLED_SITES_KEY, {});
|
||||
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
disabledUrls: [],
|
||||
temporaryDisabledUrls: {},
|
||||
isEnabled: true
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle global popup.
|
||||
*/
|
||||
const toggleGlobalPopup = async () : Promise<void> => {
|
||||
const newGloballyEnabled = !settings.isGloballyEnabled;
|
||||
|
||||
await storage.setItem(GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, newGloballyEnabled);
|
||||
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
isGloballyEnabled: newGloballyEnabled
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Set autofill matching mode.
|
||||
*/
|
||||
const setAutofillMatchingModeSetting = async (mode: AutofillMatchingMode) : Promise<void> => {
|
||||
await storage.setItem(AUTOFILL_MATCHING_MODE_KEY, mode);
|
||||
setAutofillMatchingMode(mode);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Global Settings Section */}
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.globalSettings')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.autofillPopup')}</p>
|
||||
<p className={`text-sm mt-1 ${settings.isGloballyEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{settings.isGloballyEnabled ? t('settings.activeOnAllSites') : t('settings.disabledOnAllSites')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleGlobalPopup}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
settings.isGloballyEnabled
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isGloballyEnabled ? t('settings.enabled') : t('settings.disabled')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Site-Specific Settings Section */}
|
||||
{settings.isGloballyEnabled && (
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.siteSpecificSettings')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.autofillPopupOn')}{settings.currentUrl}</p>
|
||||
<p className={`text-sm mt-1 ${settings.isEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{settings.isEnabled ? t('settings.enabledForThisSite') : t('settings.disabledForThisSite')}
|
||||
</p>
|
||||
{!settings.isEnabled && settings.temporaryDisabledUrls[settings.currentUrl] && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{t('settings.temporarilyDisabledUntil')}{new Date(settings.temporaryDisabledUrls[settings.currentUrl]).toLocaleTimeString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{settings.isGloballyEnabled && (
|
||||
<button
|
||||
onClick={toggleCurrentSite}
|
||||
className={`px-4 py-2 ml-1 rounded-md transition-colors ${
|
||||
settings.isEnabled
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isEnabled ? t('settings.enabled') : t('settings.disabled')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={resetSettings}
|
||||
className="w-full px-4 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md text-gray-700 dark:text-gray-300 transition-colors text-sm"
|
||||
>
|
||||
{t('settings.resetAllSiteSettings')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Autofill Matching Settings Section */}
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.autofillMatching')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white mb-2">{t('settings.autofillMatchingMode')}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">{t('settings.autofillMatchingModeDescription')}</p>
|
||||
<select
|
||||
value={autofillMatchingMode}
|
||||
onChange={(e) => setAutofillMatchingModeSetting(e.target.value as AutofillMatchingMode)}
|
||||
className="w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-gray-900 dark:text-white focus:ring-primary-500 focus:border-primary-500"
|
||||
>
|
||||
<option value={AutofillMatchingMode.DEFAULT}>{t('settings.autofillMatchingDefault')}</option>
|
||||
<option value={AutofillMatchingMode.URL_SUBDOMAIN}>{t('settings.autofillMatchingUrlSubdomain')}</option>
|
||||
<option value={AutofillMatchingMode.URL_EXACT}>{t('settings.autofillMatchingUrlExact')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutofillSettings;
|
||||
@@ -0,0 +1,69 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
import { CLIPBOARD_CLEAR_TIMEOUT_KEY } from '@/utils/Constants';
|
||||
|
||||
import { storage } from "#imports";
|
||||
|
||||
/**
|
||||
* Clipboard settings page component.
|
||||
*/
|
||||
const ClipboardSettings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const [clipboardTimeout, setClipboardTimeout] = useState<number>(10);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Load clipboard settings.
|
||||
*/
|
||||
const loadSettings = async () : Promise<void> => {
|
||||
// Load clipboard clear timeout
|
||||
const timeout = await storage.getItem(CLIPBOARD_CLEAR_TIMEOUT_KEY) as number ?? 10;
|
||||
setClipboardTimeout(timeout);
|
||||
setIsInitialLoading(false);
|
||||
};
|
||||
|
||||
loadSettings();
|
||||
}, [setIsInitialLoading]);
|
||||
|
||||
/**
|
||||
* Set clipboard clear timeout.
|
||||
*/
|
||||
const setClipboardClearTimeout = async (timeout: number) : Promise<void> => {
|
||||
await storage.setItem(CLIPBOARD_CLEAR_TIMEOUT_KEY, timeout);
|
||||
await sendMessage('SET_CLIPBOARD_CLEAR_TIMEOUT', timeout, 'background');
|
||||
setClipboardTimeout(timeout);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.clipboardSettings')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white mb-2">{t('settings.clipboardClearTimeout')}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">{t('settings.clipboardClearTimeoutDescription')}</p>
|
||||
<select
|
||||
value={clipboardTimeout}
|
||||
onChange={(e) => setClipboardClearTimeout(Number(e.target.value))}
|
||||
className="w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-gray-900 dark:text-white focus:ring-primary-500 focus:border-primary-500"
|
||||
>
|
||||
<option value="0">{t('settings.clipboardClearDisabled')}</option>
|
||||
<option value="5">{t('settings.clipboardClear5Seconds')}</option>
|
||||
<option value="10">{t('settings.clipboardClear10Seconds')}</option>
|
||||
<option value="15">{t('settings.clipboardClear15Seconds')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClipboardSettings;
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
import { GLOBAL_CONTEXT_MENU_ENABLED_KEY } from '@/utils/Constants';
|
||||
|
||||
import { storage } from "#imports";
|
||||
|
||||
/**
|
||||
* Context menu settings page component.
|
||||
*/
|
||||
const ContextMenuSettings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const [isContextMenuEnabled, setIsContextMenuEnabled] = useState<boolean>(true);
|
||||
|
||||
/**
|
||||
* Load settings.
|
||||
*/
|
||||
const loadSettings = useCallback(async () : Promise<void> => {
|
||||
const isEnabled = await storage.getItem(GLOBAL_CONTEXT_MENU_ENABLED_KEY) !== false; // Default to true if not set
|
||||
setIsContextMenuEnabled(isEnabled);
|
||||
setIsInitialLoading(false);
|
||||
}, [setIsInitialLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
}, [loadSettings]);
|
||||
|
||||
/**
|
||||
* Toggle context menu.
|
||||
*/
|
||||
const toggleContextMenu = async () : Promise<void> => {
|
||||
const newContextMenuEnabled = !isContextMenuEnabled;
|
||||
|
||||
await storage.setItem(GLOBAL_CONTEXT_MENU_ENABLED_KEY, newContextMenuEnabled);
|
||||
await sendMessage('TOGGLE_CONTEXT_MENU', { enabled: newContextMenuEnabled }, 'background');
|
||||
|
||||
setIsContextMenuEnabled(newContextMenuEnabled);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.contextMenu')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.rightClickContextMenu')}</p>
|
||||
<p className={`text-sm mt-1 ${isContextMenuEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{isContextMenuEnabled ? t('settings.contextMenuEnabled') : t('settings.contextMenuDisabled')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
|
||||
{t('settings.contextMenuDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleContextMenu}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
isContextMenuEnabled
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{isContextMenuEnabled ? t('settings.enabled') : t('settings.disabled')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContextMenuSettings;
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import LanguageSwitcher from '@/entrypoints/popup/components/LanguageSwitcher';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
/**
|
||||
* Language settings page component.
|
||||
*/
|
||||
const LanguageSettings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
// Mark initial loading as complete
|
||||
setIsInitialLoading(false);
|
||||
}, [setIsInitialLoading]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.language')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white mb-3">{t('settings.selectLanguage')}</p>
|
||||
<LanguageSwitcher variant="dropdown" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSettings;
|
||||
@@ -0,0 +1,447 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useTheme } from '@/entrypoints/popup/context/ThemeContext';
|
||||
import { useApiUrl } from '@/entrypoints/popup/utils/ApiUrlUtility';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
|
||||
import { browser } from "#imports";
|
||||
|
||||
/**
|
||||
* Settings page component.
|
||||
*/
|
||||
const Settings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const authContext = useAuth();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const { loadApiUrl, getDisplayUrl } = useApiUrl();
|
||||
const navigate = useNavigate();
|
||||
|
||||
/**
|
||||
* Open the client tab.
|
||||
*/
|
||||
const openClientTab = async () : Promise<void> => {
|
||||
const settingClientUrl = await browser.storage.local.get('clientUrl');
|
||||
let clientUrl = AppInfo.DEFAULT_CLIENT_URL;
|
||||
if (settingClientUrl?.clientUrl && settingClientUrl.clientUrl.length > 0) {
|
||||
clientUrl = settingClientUrl.clientUrl;
|
||||
}
|
||||
|
||||
window.open(clientUrl, '_blank');
|
||||
};
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
{!PopoutUtility.isPopup() && (
|
||||
<>
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title={t('settings.openInNewWindow')}
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<HeaderButton
|
||||
onClick={openClientTab}
|
||||
title={t('settings.openWebApp')}
|
||||
iconType={HeaderIconType.EXTERNAL_LINK}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
return () => setHeaderButtons(null);
|
||||
}, [setHeaderButtons, t]);
|
||||
|
||||
/**
|
||||
* Load settings.
|
||||
*/
|
||||
const loadSettings = useCallback(async () : Promise<void> => {
|
||||
// Load API URL
|
||||
await loadApiUrl();
|
||||
setIsInitialLoading(false);
|
||||
}, [setIsInitialLoading, loadApiUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
}, [loadSettings]);
|
||||
|
||||
/**
|
||||
* Set theme preference.
|
||||
*/
|
||||
const setThemePreference = async (newTheme: 'system' | 'light' | 'dark') : Promise<void> => {
|
||||
// Use the ThemeContext to apply the theme
|
||||
setTheme(newTheme);
|
||||
};
|
||||
|
||||
/**
|
||||
* Open keyboard shortcuts configuration page.
|
||||
*/
|
||||
const openKeyboardShortcuts = async (): Promise<void> => {
|
||||
// Detect browser type using user agent
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
const isFirefox = userAgent.includes('firefox');
|
||||
const isSafari = userAgent.includes('safari') && !userAgent.includes('chrome');
|
||||
|
||||
if (isFirefox) {
|
||||
await browser.tabs.create({ url: 'about:addons' });
|
||||
} else if (isSafari) {
|
||||
await browser.tabs.create({ url: 'safari-extension://shortcuts' });
|
||||
} else {
|
||||
// Chrome and other Chromium-based browsers
|
||||
await browser.tabs.create({ url: 'chrome://extensions/shortcuts' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logout.
|
||||
*/
|
||||
const handleLogout = async () : Promise<void> => {
|
||||
navigate('/logout', { replace: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate to autofill settings.
|
||||
*/
|
||||
const navigateToAutofillSettings = () : void => {
|
||||
navigate('/settings/autofill');
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate to clipboard settings.
|
||||
*/
|
||||
const navigateToClipboardSettings = () : void => {
|
||||
navigate('/settings/clipboard');
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate to language settings.
|
||||
*/
|
||||
const navigateToLanguageSettings = () : void => {
|
||||
navigate('/settings/language');
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate to auto-lock settings.
|
||||
*/
|
||||
const navigateToAutoLockSettings = () : void => {
|
||||
navigate('/settings/auto-lock');
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate to context menu settings.
|
||||
*/
|
||||
const navigateToContextMenuSettings = () : void => {
|
||||
navigate('/settings/context-menu');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">{t('settings.title')}</h2>
|
||||
</div>
|
||||
|
||||
{/* User Menu Section */}
|
||||
<section>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||||
<span className="text-primary-600 dark:text-primary-400 text-lg font-medium">
|
||||
{authContext.username?.[0]?.toUpperCase() || '?'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text font-medium text-gray-900 dark:text-white">
|
||||
{authContext.username}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('settings.loggedIn')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
title={t('settings.logout')}
|
||||
className="p-2 bg-red-100 hover:bg-red-200 dark:bg-red-900/30 dark:hover:bg-red-900/50 text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 rounded-md transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
aria-label={t('settings.logout')}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Settings Navigation Section */}
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.preferences')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{/* Autofill Settings */}
|
||||
<button
|
||||
onClick={navigateToAutofillSettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-gray-900 dark:text-white">{t('settings.autofillSettings')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Context Menu Settings */}
|
||||
<button
|
||||
onClick={navigateToContextMenuSettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4 6h16M4 12h16m-7 6h7"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-gray-900 dark:text-white">{t('settings.contextMenuSettings')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Auto-lock Settings */}
|
||||
<button
|
||||
onClick={navigateToAutoLockSettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-gray-900 dark:text-white">{t('settings.autoLockTimeout')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Clipboard Settings */}
|
||||
<button
|
||||
onClick={navigateToClipboardSettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-gray-900 dark:text-white">{t('settings.clipboardSettings')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Language Settings */}
|
||||
<button
|
||||
onClick={navigateToLanguageSettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-gray-900 dark:text-white">{t('settings.language')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Appearance Settings Section */}
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.appearance')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white mb-2">{t('settings.theme')}</p>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="system"
|
||||
checked={theme === 'system'}
|
||||
onChange={() => setThemePreference('system')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.useDefault')}</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="light"
|
||||
checked={theme === 'light'}
|
||||
onChange={() => setThemePreference('light')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.light')}</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="dark"
|
||||
checked={theme === 'dark'}
|
||||
onChange={() => setThemePreference('dark')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.dark')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Keyboard Shortcuts Section */}
|
||||
{import.meta.env.CHROME && (
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.keyboardShortcuts')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.configureKeyboardShortcuts')}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={openKeyboardShortcuts}
|
||||
className="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md transition-colors"
|
||||
>
|
||||
{t('settings.configure')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="text-center text-gray-400 dark:text-gray-600">
|
||||
{t('settings.versionPrefix')}{AppInfo.VERSION} ({getDisplayUrl()})
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -1,3 +1,7 @@
|
||||
body {
|
||||
font-size: 75%;
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
@@ -3,20 +3,44 @@
|
||||
* Add new languages here to make them available throughout the application
|
||||
*/
|
||||
|
||||
import deTranslations from './locales/de.json';
|
||||
import enTranslations from './locales/en.json';
|
||||
import fiTranslations from './locales/fi.json';
|
||||
import heTranslations from './locales/he.json';
|
||||
import itTranslations from './locales/it.json';
|
||||
import nlTranslations from './locales/nl.json';
|
||||
import ukTranslations from './locales/uk.json';
|
||||
import zhTranslations from './locales/zh.json';
|
||||
|
||||
/**
|
||||
* Create a map of all available languages and their resources for i18n.
|
||||
* When adding a new language, add the translation JSON file to the locales folder and add the language to the map here.
|
||||
*/
|
||||
export const LANGUAGE_RESOURCES = {
|
||||
de: {
|
||||
translation: deTranslations
|
||||
},
|
||||
en: {
|
||||
translation: enTranslations
|
||||
},
|
||||
fi: {
|
||||
translation: fiTranslations
|
||||
},
|
||||
he: {
|
||||
translation: heTranslations
|
||||
},
|
||||
it: {
|
||||
translation: itTranslations
|
||||
},
|
||||
nl: {
|
||||
translation: nlTranslations
|
||||
}
|
||||
},
|
||||
uk: {
|
||||
translation: ukTranslations
|
||||
},
|
||||
zh: {
|
||||
translation: zhTranslations
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -24,25 +48,55 @@ export const LANGUAGE_RESOURCES = {
|
||||
* When adding a new language, add the language to the map here.
|
||||
*/
|
||||
export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
|
||||
{
|
||||
code: 'de',
|
||||
name: 'German',
|
||||
nativeName: 'Deutsch',
|
||||
flag: '🇩🇪'
|
||||
},
|
||||
{
|
||||
code: 'en',
|
||||
name: 'English',
|
||||
nativeName: 'English',
|
||||
flag: '🇺🇸'
|
||||
},
|
||||
{
|
||||
code: 'fi',
|
||||
name: 'Finnish',
|
||||
nativeName: 'Suomi',
|
||||
flag: '🇫🇮'
|
||||
},
|
||||
{
|
||||
code: 'he',
|
||||
name: 'Hebrew',
|
||||
nativeName: 'עברית',
|
||||
flag: '🇮🇱'
|
||||
},
|
||||
{
|
||||
code: 'it',
|
||||
name: 'Italian',
|
||||
nativeName: 'Italiano',
|
||||
flag: '🇮🇹'
|
||||
},
|
||||
{
|
||||
code: 'nl',
|
||||
name: 'Dutch',
|
||||
nativeName: 'Nederlands',
|
||||
flag: '🇳🇱'
|
||||
},
|
||||
{
|
||||
code: 'uk',
|
||||
name: 'Ukrainian',
|
||||
nativeName: 'Українська',
|
||||
flag: '🇺🇦'
|
||||
},
|
||||
{
|
||||
code: 'zh',
|
||||
name: 'Chinese',
|
||||
nativeName: '简体中文',
|
||||
flag: '🇨🇳'
|
||||
},
|
||||
/*
|
||||
* {
|
||||
* code: 'de',
|
||||
* name: 'German',
|
||||
* nativeName: 'Deutsch',
|
||||
* flag: '🇩🇪'
|
||||
* },
|
||||
* {
|
||||
* code: 'es',
|
||||
* name: 'Spanish',
|
||||
@@ -55,12 +109,6 @@ export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
|
||||
* nativeName: 'Français',
|
||||
* flag: '🇫🇷'
|
||||
* },
|
||||
* {
|
||||
* code: 'uk',
|
||||
* name: 'Ukrainian',
|
||||
* nativeName: 'Українська',
|
||||
* flag: '🇺🇦'
|
||||
* }
|
||||
*/
|
||||
];
|
||||
|
||||
|
||||
393
apps/browser-extension/src/i18n/locales/ca.json
Normal file
393
apps/browser-extension/src/i18n/locales/ca.json
Normal file
@@ -0,0 +1,393 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "Log in to AliasVault",
|
||||
"username": "Username or email",
|
||||
"usernamePlaceholder": "name / name@company.com",
|
||||
"password": "Contrasenya",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"rememberMe": "Remember me",
|
||||
"loginButton": "Login",
|
||||
"noAccount": "No account yet?",
|
||||
"createVault": "Create new vault",
|
||||
"twoFactorTitle": "Please enter the authentication code from your authenticator app.",
|
||||
"authCode": "Codi d'autenticació",
|
||||
"authCodePlaceholder": "Introduïu el codi de 6 dígits",
|
||||
"verify": "Verifica",
|
||||
"cancel": "Cancel",
|
||||
"twoFactorNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.",
|
||||
"masterPassword": "Contrasenya Mestra",
|
||||
"unlockVault": "Unlock Vault",
|
||||
"unlockTitle": "Unlock Your Vault",
|
||||
"unlockDescription": "Enter your master password to unlock your vault.",
|
||||
"logout": "Tanca la sessió",
|
||||
"logoutConfirm": "Are you sure you want to logout?",
|
||||
"sessionExpired": "Your session has expired. Please log in again.",
|
||||
"unlockSuccess": "Vault unlocked successfully!",
|
||||
"unlockSuccessTitle": "Your vault is successfully unlocked",
|
||||
"unlockSuccessDescription": "You can now use autofill in login forms in your browser.",
|
||||
"closePopup": "Close this popup",
|
||||
"browseVault": "Browse vault contents",
|
||||
"connectingTo": "Connectant a",
|
||||
"switchAccounts": "Switch accounts?",
|
||||
"loggedIn": "Logged in",
|
||||
"errors": {
|
||||
"invalidCode": "Please enter a valid 6-digit authentication code.",
|
||||
"serverError": "Could not reach AliasVault server. Please try again later or contact support if the problem persists.",
|
||||
"noToken": "Login failed -- no token returned",
|
||||
"migrationError": "An error occurred while checking for pending migrations.",
|
||||
"wrongPassword": "Incorrect password. Please try again.",
|
||||
"accountLocked": "Account temporarily locked due to too many failed attempts.",
|
||||
"networkError": "Network error. Please check your connection and try again.",
|
||||
"loginDataMissing": "Login session expired. Please try again."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "Credentials",
|
||||
"emails": "Emails",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "S'està carregant...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"cancel": "Cancel",
|
||||
"use": "Utilitza",
|
||||
"delete": "Suprimeix",
|
||||
"close": "Tanca",
|
||||
"copied": "Copied!",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"language": "Language",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"showPassword": "Mostra la contrasenya",
|
||||
"hidePassword": "Amaga la contrasenya",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"loadingEmails": "Loading emails...",
|
||||
"loadingTotpCodes": "Loading TOTP codes...",
|
||||
"attachments": "Attachments",
|
||||
"loadingAttachments": "Loading attachments...",
|
||||
"settings": "Settings",
|
||||
"recentEmails": "Recent emails",
|
||||
"loginCredentials": "Login credentials",
|
||||
"twoFactorAuthentication": "Two-factor authentication",
|
||||
"alias": "Alias",
|
||||
"notes": "Notes",
|
||||
"fullName": "Full Name",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"birthDate": "Birth Date",
|
||||
"nickname": "Nickname",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"syncingVault": "Syncing vault",
|
||||
"savingChangesToVault": "Saving changes to vault",
|
||||
"uploadingVaultToServer": "Uploading vault to server",
|
||||
"checkingVaultUpdates": "Checking for vault updates",
|
||||
"syncingUpdatedVault": "Syncing updated vault",
|
||||
"executingOperation": "Executing operation...",
|
||||
"loadMore": "Load more",
|
||||
"errors": {
|
||||
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
|
||||
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
|
||||
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
|
||||
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
|
||||
"unknownError": "An unknown error occurred",
|
||||
"failedToStoreVault": "Failed to store vault",
|
||||
"vaultNotAvailable": "Vault not available",
|
||||
"failedToRetrieveData": "Failed to retrieve data",
|
||||
"vaultIsLocked": "Vault is locked",
|
||||
"failedToUploadVault": "Failed to upload vault",
|
||||
"passwordChanged": "Your password has changed since the last time you logged in. Please login again for security reasons."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",
|
||||
"ACCOUNT_LOCKED": "Account temporarily locked due to too many failed attempts. Please try again later.",
|
||||
"ACCOUNT_BLOCKED": "Your account has been disabled. If you believe this is a mistake, please contact support.",
|
||||
"USER_NOT_FOUND": "Invalid username or password. Please try again.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Invalid authenticator code. Please try again.",
|
||||
"INVALID_RECOVERY_CODE": "Invalid recovery code. Please try again.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Refresh token is required.",
|
||||
"INVALID_REFRESH_TOKEN": "Invalid refresh token.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token revoked successfully.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "New account registration is currently disabled on this server. Please contact the administrator.",
|
||||
"USERNAME_REQUIRED": "Username is required.",
|
||||
"USERNAME_ALREADY_IN_USE": "Username is already in use.",
|
||||
"USERNAME_AVAILABLE": "Username is available.",
|
||||
"USERNAME_MISMATCH": "Username does not match the current user.",
|
||||
"PASSWORD_MISMATCH": "The provided password does not match your current password.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Account successfully deleted.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Username cannot be empty or whitespace.",
|
||||
"USERNAME_TOO_SHORT": "Username too short: must be at least 3 characters long.",
|
||||
"USERNAME_TOO_LONG": "Username too long: cannot be longer than 40 characters.",
|
||||
"USERNAME_INVALID_EMAIL": "Invalid email address.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Username is invalid, can only contain letters or digits.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Your vault is not up-to-date. Please synchronize your vault and try again.",
|
||||
"INTERNAL_SERVER_ERROR": "Internal server error.",
|
||||
"VAULT_ERROR": "The local vault is not up-to-date. Please synchronize your vault by refreshing the page and try again."
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"or": "or",
|
||||
"new": "New",
|
||||
"cancel": "Cancel",
|
||||
"search": "Search",
|
||||
"vaultLocked": "AliasVault is locked.",
|
||||
"creatingNewAlias": "Creating new alias...",
|
||||
"noMatchesFound": "No matches found",
|
||||
"searchVault": "Search vault...",
|
||||
"serviceName": "Service name",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"enterServiceName": "Enter service name",
|
||||
"enterEmailAddress": "Enter email address",
|
||||
"enterUsername": "Enter username",
|
||||
"hideFor1Hour": "Hide for 1 hour (current site)",
|
||||
"hidePermanently": "Hide permanently (current site)",
|
||||
"createRandomAlias": "Create random alias",
|
||||
"createUsernamePassword": "Create username/password",
|
||||
"randomAlias": "Random alias",
|
||||
"usernamePassword": "Username/password",
|
||||
"createAndSaveAlias": "Create and save alias",
|
||||
"createAndSaveCredential": "Create and save credential",
|
||||
"randomIdentityDescription": "Generate a random identity with a random email address accessible in AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Random identity with random email",
|
||||
"manualCredentialDescription": "Specify your own email address and username.",
|
||||
"manualCredentialDescriptionDropdown": "Manual username and password",
|
||||
"failedToCreateIdentity": "Failed to create identity. Please try again.",
|
||||
"enterEmailAndOrUsername": "Enter email and/or username",
|
||||
"autofillWithAliasVault": "Autofill with AliasVault",
|
||||
"generateRandomPassword": "Generate random password (copy to clipboard)",
|
||||
"generateNewPassword": "Generate new password",
|
||||
"togglePasswordVisibility": "Toggle password visibility",
|
||||
"passwordCopiedToClipboard": "Password copied to clipboard",
|
||||
"enterEmailAndOrUsernameError": "Enter email and/or username",
|
||||
"openAliasVaultToUpgrade": "Open AliasVault to upgrade",
|
||||
"vaultUpgradeRequired": "Vault upgrade required.",
|
||||
"dismissPopup": "Dismiss popup"
|
||||
},
|
||||
"credentials": {
|
||||
"title": "Credentials",
|
||||
"addCredential": "Add Credential",
|
||||
"editCredential": "Edit Credential",
|
||||
"deleteCredential": "Delete Credential",
|
||||
"credentialDetails": "Credential Details",
|
||||
"serviceName": "Service Name",
|
||||
"serviceNamePlaceholder": "e.g., Gmail, Facebook, Bank",
|
||||
"website": "Website",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "Enter username",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"generatePassword": "Generate Password",
|
||||
"copyPassword": "Copy Password",
|
||||
"showPassword": "Show Password",
|
||||
"hidePassword": "Hide Password",
|
||||
"notes": "Notes",
|
||||
"notesPlaceholder": "Additional notes...",
|
||||
"totp": "Two-Factor Authentication",
|
||||
"totpCode": "TOTP Code",
|
||||
"copyTotp": "Copy TOTP",
|
||||
"totpSecret": "TOTP Secret",
|
||||
"totpSecretPlaceholder": "Enter TOTP secret key",
|
||||
"noCredentials": "No credentials found",
|
||||
"noCredentialsDescription": "Add your first credential to get started",
|
||||
"searchPlaceholder": "Search credentials...",
|
||||
"welcomeTitle": "Welcome to AliasVault!",
|
||||
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Fill Form",
|
||||
"deleteConfirm": "Are you sure you want to delete this credential?",
|
||||
"saveSuccess": "Credential saved successfully",
|
||||
"tags": "Tags",
|
||||
"addTag": "Add Tag",
|
||||
"removeTag": "Remove Tag",
|
||||
"folder": "Folder",
|
||||
"selectFolder": "Select Folder",
|
||||
"createFolder": "Create Folder",
|
||||
"saveCredential": "Save credential",
|
||||
"deleteCredentialTitle": "Delete Credential",
|
||||
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
|
||||
"randomAlias": "Random Alias",
|
||||
"manual": "Manual",
|
||||
"service": "Service",
|
||||
"serviceUrl": "Service URL",
|
||||
"loginCredentials": "Login Credentials",
|
||||
"generateRandomUsername": "Generate random username",
|
||||
"generateRandomPassword": "Generate random password",
|
||||
"changePasswordComplexity": "Change password complexity",
|
||||
"passwordLength": "Password length",
|
||||
"includeLowercase": "Include lowercase letters",
|
||||
"includeUppercase": "Include uppercase letters",
|
||||
"includeNumbers": "Include numbers",
|
||||
"includeSpecialChars": "Include special characters",
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"nickName": "Nick Name",
|
||||
"gender": "Gender",
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidEmail": "Invalid email format",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
|
||||
},
|
||||
"privateEmailTitle": "Private Email",
|
||||
"privateEmailAliasVaultServer": "AliasVault server",
|
||||
"privateEmailDescription": "E2E encrypted, fully private.",
|
||||
"publicEmailTitle": "Public Temp Email Providers",
|
||||
"publicEmailDescription": "Anonymous but limited privacy. Email content is readable by anyone that knows the address.",
|
||||
"useDomainChooser": "Use domain chooser",
|
||||
"enterCustomDomain": "Enter custom domain",
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
"deleteEmailTitle": "Delete Email",
|
||||
"deleteEmailConfirm": "Are you sure you want to permanently delete this email?",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"date": "Date",
|
||||
"emailContent": "Email content",
|
||||
"attachments": "Attachments",
|
||||
"emailNotFound": "Email not found",
|
||||
"noEmails": "No emails found",
|
||||
"noEmailsDescription": "You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.",
|
||||
"dateFormat": {
|
||||
"justNow": "just now",
|
||||
"minutesAgo_single": "{{count}} min ago",
|
||||
"minutesAgo_plural": "{{count}} mins ago",
|
||||
"hoursAgo_single": "{{count}} hr ago",
|
||||
"hoursAgo_plural": "{{count}} hrs ago",
|
||||
"yesterday": "yesterday"
|
||||
},
|
||||
"errors": {
|
||||
"emailLoadError": "An error occurred while loading emails. Please try again later.",
|
||||
"emailUnexpectedError": "An unexpected error occurred while loading emails. Please try again later."
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "The current chosen email address is already in use. Please change the email address by editing this credential.",
|
||||
"CLAIM_DOES_NOT_EXIST": "An error occurred while trying to load the emails. Please try to edit and save the credential entry to synchronize the database, then try again."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"serverUrl": "Server URL",
|
||||
"language": "Language",
|
||||
"autofillEnabled": "Enable Autofill",
|
||||
"version": "Version",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"openWebApp": "Open web app",
|
||||
"loggedIn": "Logged in",
|
||||
"logout": "Logout",
|
||||
"globalSettings": "Global Settings",
|
||||
"autofillPopup": "Autofill popup",
|
||||
"activeOnAllSites": "Active on all sites (unless disabled below)",
|
||||
"disabledOnAllSites": "Disabled on all sites",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"rightClickContextMenu": "Right-click context menu",
|
||||
"autofillMatching": "Autofill Matching",
|
||||
"autofillMatchingMode": "Autofill matching mode",
|
||||
"autofillMatchingModeDescription": "Determines which credentials are considered a match and shown as suggestions in the autofill popup for a given website.",
|
||||
"autofillMatchingDefault": "URL + subdomain + name wildcard",
|
||||
"autofillMatchingUrlSubdomain": "URL + subdomain",
|
||||
"autofillMatchingUrlExact": "Exact URL domain only",
|
||||
"siteSpecificSettings": "Site-Specific Settings",
|
||||
"autofillPopupOn": "Autofill popup on: ",
|
||||
"enabledForThisSite": "Enabled for this site",
|
||||
"disabledForThisSite": "Disabled for this site",
|
||||
"temporarilyDisabledUntil": "Temporarily disabled until ",
|
||||
"resetAllSiteSettings": "Reset all site-specific settings",
|
||||
"appearance": "Appearance",
|
||||
"theme": "Theme",
|
||||
"useDefault": "Use default",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||
"configureKeyboardShortcuts": "Configure keyboard shortcuts",
|
||||
"configure": "Configure",
|
||||
"security": "Security",
|
||||
"clipboardClearTimeout": "Clear clipboard after copying",
|
||||
"clipboardClearTimeoutDescription": "Automatically clear the clipboard after copying sensitive data",
|
||||
"clipboardClearDisabled": "Never clear",
|
||||
"clipboardClear5Seconds": "Clear after 5 seconds",
|
||||
"clipboardClear10Seconds": "Clear after 10 seconds",
|
||||
"clipboardClear15Seconds": "Clear after 15 seconds",
|
||||
"autoLockTimeout": "Auto-lock Timeout",
|
||||
"autoLockTimeoutDescription": "Automatically lock the vault after a period of inactivity",
|
||||
"autoLockTimeoutHelp": "The vault will only lock after the specified period of inactivity (no autofill usage or extension popup opened). The vault will always lock when the browser is closed, regardless of this setting.",
|
||||
"autoLockNever": "Never",
|
||||
"autoLock15Seconds": "15 seconds",
|
||||
"autoLock1Minute": "1 minute",
|
||||
"autoLock5Minutes": "5 minutes",
|
||||
"autoLock15Minutes": "15 minutes",
|
||||
"autoLock30Minutes": "30 minutes",
|
||||
"autoLock1Hour": "1 hour",
|
||||
"autoLock4Hours": "4 hours",
|
||||
"autoLock8Hours": "8 hours",
|
||||
"autoLock24Hours": "24 hours",
|
||||
"versionPrefix": "Version ",
|
||||
"preferences": "Preferences",
|
||||
"autofillSettings": "Autofill Settings",
|
||||
"clipboardSettings": "Clipboard Settings",
|
||||
"contextMenuSettings": "Context Menu Settings",
|
||||
"contextMenu": "Context Menu",
|
||||
"contextMenuEnabled": "Context menu is enabled",
|
||||
"contextMenuDisabled": "Context menu is disabled",
|
||||
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
|
||||
"selectLanguage": "Select Language",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL is required",
|
||||
"apiUrlInvalid": "Please enter a valid API URL",
|
||||
"clientUrlRequired": "Client URL is required",
|
||||
"clientUrlInvalid": "Please enter a valid client URL"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "Upgrade Vault",
|
||||
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
|
||||
"versionInformation": "Version Information",
|
||||
"yourVault": "Your vault:",
|
||||
"newVersion": "New version:",
|
||||
"upgrade": "Upgrade Vault",
|
||||
"upgrading": "Upgrading...",
|
||||
"logout": "Logout",
|
||||
"whatsNew": "What's New",
|
||||
"whatsNewDescription": "An upgrade is required to support the following changes:",
|
||||
"noDescriptionAvailable": "No description available for this version.",
|
||||
"okay": "Ok",
|
||||
"status": {
|
||||
"preparingUpgrade": "Preparing upgrade...",
|
||||
"vaultAlreadyUpToDate": "Vault is already up to date",
|
||||
"startingDatabaseTransaction": "Starting database transaction...",
|
||||
"applyingDatabaseMigrations": "Applying database migrations...",
|
||||
"applyingMigration": "Applying migration {{current}} of {{total}}...",
|
||||
"committingChanges": "Committing changes..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Error",
|
||||
"unableToGetVersionInfo": "Unable to get version information. Please try again.",
|
||||
"selfHostedServer": "Self-Hosted Server",
|
||||
"selfHostedWarning": "If you're using a self-hosted server, make sure to also update your self-hosted instance as otherwise logging in to the web client will stop working.",
|
||||
"cancel": "Cancel",
|
||||
"continueUpgrade": "Continue Upgrade",
|
||||
"upgradeFailed": "Upgrade Failed",
|
||||
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
|
||||
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,375 +1,393 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "Log in to AliasVault",
|
||||
"username": "Username or email",
|
||||
"usernamePlaceholder": "name / name@company.com",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"rememberMe": "Remember me",
|
||||
"loginButton": "Login",
|
||||
"noAccount": "No account yet?",
|
||||
"createVault": "Create new vault",
|
||||
"twoFactorTitle": "Please enter the authentication code from your authenticator app.",
|
||||
"authCode": "Authentication Code",
|
||||
"authCodePlaceholder": "Enter 6-digit code",
|
||||
"verify": "Verify",
|
||||
"cancel": "Cancel",
|
||||
"twoFactorNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.",
|
||||
"masterPassword": "Master Password",
|
||||
"unlockVault": "Unlock Vault",
|
||||
"unlockTitle": "Unlock Your Vault",
|
||||
"unlockDescription": "Enter your master password to unlock your vault.",
|
||||
"logout": "Logout",
|
||||
"logoutConfirm": "Are you sure you want to logout?",
|
||||
"sessionExpired": "Your session has expired. Please log in again.",
|
||||
"unlockSuccess": "Vault unlocked successfully!",
|
||||
"unlockSuccessTitle": "Your vault is successfully unlocked",
|
||||
"unlockSuccessDescription": "You can now use autofill in login forms in your browser.",
|
||||
"closePopup": "Close this popup",
|
||||
"browseVault": "Browse vault contents",
|
||||
"connectingTo": "Connecting to",
|
||||
"switchAccounts": "Switch accounts?",
|
||||
"loggedIn": "Logged in",
|
||||
"loginTitle": "Bei AliasVault anmelden",
|
||||
"username": "Benutzername oder E-Mail-Adresse",
|
||||
"usernamePlaceholder": "Name / name@unternehmen.com",
|
||||
"password": "Passwort",
|
||||
"passwordPlaceholder": "Gib Dein Passwort ein",
|
||||
"rememberMe": "Angemeldet bleiben",
|
||||
"loginButton": "Anmelden",
|
||||
"noAccount": "Noch kein Konto?",
|
||||
"createVault": "Neuen Tresor erstellen",
|
||||
"twoFactorTitle": "Bitte gib den Sicherheits-Code aus Deiner Authentifizierungs-App ein.",
|
||||
"authCode": "Sicherheits-Code",
|
||||
"authCodePlaceholder": "Gib den 6-stelligen Sicherheits-Code ein.",
|
||||
"verify": "Bestätige",
|
||||
"cancel": "Abbrechen",
|
||||
"twoFactorNote": "Hinweis: Wenn Du keinen Zugriff auf Dein Authentifizierungsgerät hast, kannst Du Deine Zwei-Faktor-Authentifizierung (2FA) mit einem Wiederherstellungscode zurücksetzen, indem Du Dich über die Website anmeldest.",
|
||||
"masterPassword": "Master-Passwort",
|
||||
"unlockVault": "Tresor entsperren",
|
||||
"unlockTitle": "Entsperre Deinen Tresor",
|
||||
"unlockDescription": "Bitte gib Dein Master-Passwort zum Entsperren des Tresors ein.",
|
||||
"logout": "Abmelden",
|
||||
"logoutConfirm": "Bist Du sicher, dass Du Dich abmelden möchtest?",
|
||||
"sessionExpired": "Deine Sitzung ist abgelaufen. Bitte melde Dich erneut an.",
|
||||
"unlockSuccess": "Tresor erfolgreich entsperrt!",
|
||||
"unlockSuccessTitle": "Ihr Tresor wurde erfolgreich entsperrt",
|
||||
"unlockSuccessDescription": "Du kannst jetzt die Autofill-Funktion in Anmeldeformularen in Deinem Browser nutzen.",
|
||||
"closePopup": "Popup schließen",
|
||||
"browseVault": "Tresor durchsuchen",
|
||||
"connectingTo": "Verbinde zu",
|
||||
"switchAccounts": "Konto wechseln?",
|
||||
"loggedIn": "Angemeldet",
|
||||
"errors": {
|
||||
"invalidCode": "Please enter a valid 6-digit authentication code.",
|
||||
"serverError": "Could not reach AliasVault server. Please try again later or contact support if the problem persists.",
|
||||
"noToken": "Login failed -- no token returned",
|
||||
"migrationError": "An error occurred while checking for pending migrations.",
|
||||
"wrongPassword": "Incorrect password. Please try again.",
|
||||
"accountLocked": "Account temporarily locked due to too many failed attempts.",
|
||||
"networkError": "Network error. Please check your connection and try again.",
|
||||
"loginDataMissing": "Login session expired. Please try again."
|
||||
"invalidCode": "Bitte gib einen gültigen 6-stelligen Sicherheits-Code ein.",
|
||||
"serverError": "Der AliasVault-Server konnte nicht erreicht werden. Bitte versuche es später noch einmal oder kontaktiere den Support, falls das Problem weiterhin besteht.",
|
||||
"noToken": "Anmeldung fehlgeschlagen -- es wurde kein Token zurückgegeben",
|
||||
"migrationError": "Beim Prüfen auf ausstehende Migrationen ist ein Fehler aufgetreten.",
|
||||
"wrongPassword": "Falsches Passwort. Bitte versuche es erneut.",
|
||||
"accountLocked": "Das Konto wurde wegen zu vieler fehlgeschlagener Anmeldeversuche vorübergehend gesperrt.",
|
||||
"networkError": "Netzwerkfehler. Bitte überprüfe Deine Verbindung und versuche es erneut.",
|
||||
"loginDataMissing": "Deine Anmelde-Sitzung ist abgelaufen. Bitte versuche es erneut."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "Credentials",
|
||||
"emails": "Emails",
|
||||
"settings": "Settings"
|
||||
"credentials": "Zugangsdaten",
|
||||
"emails": "E-Mails",
|
||||
"settings": "Einstellungen"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"cancel": "Cancel",
|
||||
"use": "Use",
|
||||
"delete": "Delete",
|
||||
"close": "Close",
|
||||
"copied": "Copied!",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"language": "Language",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"loadingEmails": "Loading emails...",
|
||||
"loadingTotpCodes": "Loading TOTP codes...",
|
||||
"attachments": "Attachments",
|
||||
"loadingAttachments": "Loading attachments...",
|
||||
"settings": "Settings",
|
||||
"recentEmails": "Recent emails",
|
||||
"loginCredentials": "Login credentials",
|
||||
"twoFactorAuthentication": "Two-factor authentication",
|
||||
"loading": "Laden...",
|
||||
"error": "Fehler",
|
||||
"success": "Aktion erfolgreich",
|
||||
"cancel": "Abbrechen",
|
||||
"use": "Benutzen",
|
||||
"delete": "Löschen",
|
||||
"close": "Schließen",
|
||||
"copied": "Kopiert!",
|
||||
"openInNewWindow": "In neuem Fenster öffnen",
|
||||
"language": "Sprache",
|
||||
"enabled": "Aktiviert",
|
||||
"disabled": "Deaktiviert",
|
||||
"showPassword": "Passwort anzeigen",
|
||||
"hidePassword": "Passwort verbergen",
|
||||
"copyToClipboard": "In die Zwischenablage kopieren",
|
||||
"loadingEmails": "E-Mails werden geladen...",
|
||||
"loadingTotpCodes": "TOTP-Codes werden geladen...",
|
||||
"attachments": "Anhänge",
|
||||
"loadingAttachments": "Anhänge werden geladen...",
|
||||
"settings": "Einstellungen",
|
||||
"recentEmails": "Neueste E-Mails",
|
||||
"loginCredentials": "Zugangsdaten",
|
||||
"twoFactorAuthentication": "Zwei-Faktor-Authentifizierung",
|
||||
"alias": "Alias",
|
||||
"notes": "Notes",
|
||||
"fullName": "Full Name",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"birthDate": "Birth Date",
|
||||
"nickname": "Nickname",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"syncingVault": "Syncing vault",
|
||||
"savingChangesToVault": "Saving changes to vault",
|
||||
"uploadingVaultToServer": "Uploading vault to server",
|
||||
"checkingVaultUpdates": "Checking for vault updates",
|
||||
"syncingUpdatedVault": "Syncing updated vault",
|
||||
"executingOperation": "Executing operation...",
|
||||
"loadMore": "Load more",
|
||||
"notes": "Notizen",
|
||||
"fullName": "Vor- und Nachname",
|
||||
"firstName": "Vorname",
|
||||
"lastName": "Nachname",
|
||||
"birthDate": "Geburtsdatum",
|
||||
"nickname": "Spitzname",
|
||||
"email": "E-Mail-Adresse",
|
||||
"username": "Benutzername",
|
||||
"password": "Passwort",
|
||||
"syncingVault": "Tresor wird synchronisiert",
|
||||
"savingChangesToVault": "Änderungen werden gespeichert",
|
||||
"uploadingVaultToServer": "Tresor wird auf den Server hochgeladen",
|
||||
"checkingVaultUpdates": "Prüfe auf Tresor-Updates",
|
||||
"syncingUpdatedVault": "Aktualisierter Tresor wird synchronisiert",
|
||||
"executingOperation": "Vorgang wird ausgeführt...",
|
||||
"loadMore": "Mehr laden",
|
||||
"errors": {
|
||||
"VaultMergeRequired": "Your vault needs to be updated. Please login on the AliasVault website and follow the steps.",
|
||||
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
|
||||
"NoVaultFound": "Your account does not have a vault yet. Please complete the tutorial in the AliasVault web client before using the browser extension.",
|
||||
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
|
||||
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
|
||||
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
|
||||
"unknownError": "An unknown error occurred",
|
||||
"failedToStoreVault": "Failed to store vault",
|
||||
"vaultNotAvailable": "Vault not available",
|
||||
"failedToGetVault": "Failed to get vault",
|
||||
"vaultIsLocked": "Vault is locked",
|
||||
"failedToGetCredentials": "Failed to get credentials",
|
||||
"failedToCreateIdentity": "Failed to create identity",
|
||||
"failedToGetDefaultEmailDomain": "Failed to get default email domain",
|
||||
"failedToGetDefaultIdentitySettings": "Failed to get default identity settings",
|
||||
"failedToGetPasswordSettings": "Failed to get password settings",
|
||||
"failedToUploadVault": "Failed to upload vault",
|
||||
"noDerivedKeyAvailable": "No derived key available for encryption",
|
||||
"failedToUploadVaultToServer": "Failed to upload new vault to server",
|
||||
"noVaultOrDerivedKeyFound": "No vault or derived key found"
|
||||
"VaultOutdated": "Dein Tresor ist veraltet. Bitte melde Dich auf der AliasVault-Webseite an und folge den Anweisungen.",
|
||||
"serverNotAvailable": "Der AliasVault-Server konnte nicht erreicht werden. Bitte versuche es später noch einmal oder kontaktiere den Support, falls das Problem weiterhin besteht.",
|
||||
"clientVersionNotSupported": "Diese Version der AliasVault-Browser-Erweiterung wird vom Server nicht mehr unterstützt. Bitte aktualisiere Deine Browser-Erweiterung auf die neueste Version.",
|
||||
"serverVersionNotSupported": "Der AliasVault-Server muss auf eine neuere Version aktualisiert werden, um diese Browser-Erweiterung nutzen zu können. Bitte kontaktiere den Support, falls Du Hilfe benötigst.",
|
||||
"unknownError": "Ein unbekannter Fehler ist aufgetreten",
|
||||
"failedToStoreVault": "Fehler beim Speichern des Tresors",
|
||||
"vaultNotAvailable": "Tresor nicht verfügbar",
|
||||
"failedToRetrieveData": "Abruf der Daten fehlgeschlagen",
|
||||
"vaultIsLocked": "Der Tresor ist gesperrt.",
|
||||
"failedToUploadVault": "Das Hochladen des Tresors ist fehlgeschlagen",
|
||||
"passwordChanged": "Dein Passwort hat sich seit Deiner letzten Anmeldung geändert. Bitte melden Dich aus Sicherheitsgründen erneut an."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",
|
||||
"ACCOUNT_LOCKED": "Account temporarily locked due to too many failed attempts. Please try again later.",
|
||||
"ACCOUNT_BLOCKED": "Your account has been disabled. If you believe this is a mistake, please contact support.",
|
||||
"USER_NOT_FOUND": "Invalid username or password. Please try again.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Invalid authenticator code. Please try again.",
|
||||
"INVALID_RECOVERY_CODE": "Invalid recovery code. Please try again.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Refresh token is required.",
|
||||
"USER_NOT_FOUND_IN_TOKEN": "User not found in token.",
|
||||
"USER_NOT_FOUND_IN_DATABASE": "User not found in database.",
|
||||
"INVALID_REFRESH_TOKEN": "Invalid refresh token.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token revoked successfully.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "New account registration is currently disabled on this server. Please contact the administrator.",
|
||||
"USERNAME_REQUIRED": "Username is required.",
|
||||
"USERNAME_ALREADY_IN_USE": "Username is already in use.",
|
||||
"USERNAME_AVAILABLE": "Username is available.",
|
||||
"USERNAME_MISMATCH": "Username does not match the current user.",
|
||||
"PASSWORD_MISMATCH": "The provided password does not match your current password.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Account successfully deleted.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Username cannot be empty or whitespace.",
|
||||
"USERNAME_TOO_SHORT": "Username too short: must be at least 3 characters long.",
|
||||
"USERNAME_TOO_LONG": "Username too long: cannot be longer than 40 characters.",
|
||||
"USERNAME_INVALID_EMAIL": "Invalid email address.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Username is invalid, can only contain letters or digits.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Your vault is not up-to-date. Please synchronize your vault and try again.",
|
||||
"UNKNOWN_ERROR": "Ein unbekannter Fehler ist aufgetreten. Bitte versuche es erneut.",
|
||||
"ACCOUNT_LOCKED": "Das Konto wurde wegen zu vieler fehlgeschlagener Anmeldeversuche vorübergehend gesperrt. Bitte versuche es später erneut.",
|
||||
"ACCOUNT_BLOCKED": "Dein Konto wurde deaktiviert. Wenn Du glaubst, dass dies ein Fehler ist, kontaktiere bitte den Support.",
|
||||
"USER_NOT_FOUND": "Ungültiger Benutzername oder Passwort. Bitte versuche es erneut.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Ungültiger Sicherheits-Code. Bitte versuche es erneut.",
|
||||
"INVALID_RECOVERY_CODE": "Ungültiger Wiederherstellungscode. Bitte versuche es erneut.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Aktualisierungstoken ist erforderlich.",
|
||||
"INVALID_REFRESH_TOKEN": "Ungültiger Aktualisierungstoken.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Aktualisierungstoken wurde erfolgreich widerrufen.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "Die Registrierung eines neuen Kontos ist auf diesem Server derzeit deaktiviert. Bitte kontaktiere den Administrator.",
|
||||
"USERNAME_REQUIRED": "Der Benutzername ist erforderlich.",
|
||||
"USERNAME_ALREADY_IN_USE": "Benutzername ist bereits vergeben.",
|
||||
"USERNAME_AVAILABLE": "Der Benutzername ist verfügbar.",
|
||||
"USERNAME_MISMATCH": "Der Benutzername stimmt nicht mit dem aktuellen Benutzer überein.",
|
||||
"PASSWORD_MISMATCH": "Das angegebene Passwort stimmt nicht mit Deinem aktuellen Passwort überein.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Konto erfolgreich gelöscht.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Der Benutzername darf nicht leer sein.",
|
||||
"USERNAME_TOO_SHORT": "Der Benutzername ist zu kurz. Er muss mindestens 3 Zeichen lang sein.",
|
||||
"USERNAME_TOO_LONG": "Der Benutzername ist zu lang. Er darf höchstens 40 Zeichen lang sein.",
|
||||
"USERNAME_INVALID_EMAIL": "Ungültige E-Mail-Adresse.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Der Benutzername ist ungültig. Er darf nur aus Buchstaben oder Ziffern bestehen.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Dein Tresor ist nicht aktuell. Bitte synchronisiere Deinen Tresor und versuche es erneut.",
|
||||
"INTERNAL_SERVER_ERROR": "Internal server error.",
|
||||
"VAULT_ERROR": "The local vault is not up-to-date. Please synchronize your vault by refreshing the page and try again."
|
||||
"VAULT_ERROR": "Der lokale Tresor ist nicht aktuell. Bitte synchronisiere Deinen Tresor, indem Du die Seite aktualisierst, und versuche es erneut."
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"or": "or",
|
||||
"new": "New",
|
||||
"cancel": "Cancel",
|
||||
"search": "Search",
|
||||
"vaultLocked": "AliasVault is locked.",
|
||||
"creatingNewAlias": "Creating new alias...",
|
||||
"noMatchesFound": "No matches found",
|
||||
"searchVault": "Search vault...",
|
||||
"serviceName": "Service name",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"enterServiceName": "Enter service name",
|
||||
"enterEmailAddress": "Enter email address",
|
||||
"enterUsername": "Enter username",
|
||||
"hideFor1Hour": "Hide for 1 hour (current site)",
|
||||
"hidePermanently": "Hide permanently (current site)",
|
||||
"createRandomAlias": "Create random alias",
|
||||
"createUsernamePassword": "Create username/password",
|
||||
"randomAlias": "Random alias",
|
||||
"usernamePassword": "Username/password",
|
||||
"createAndSaveAlias": "Create and save alias",
|
||||
"createAndSaveCredential": "Create and save credential",
|
||||
"randomIdentityDescription": "Generate a random identity with a random email address accessible in AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Random identity with random email",
|
||||
"manualCredentialDescription": "Specify your own email address and username.",
|
||||
"manualCredentialDescriptionDropdown": "Manual username and password",
|
||||
"failedToCreateIdentity": "Failed to create identity. Please try again.",
|
||||
"enterEmailAndOrUsername": "Enter email and/or username",
|
||||
"autofillWithAliasVault": "Autofill with AliasVault",
|
||||
"generateRandomPassword": "Generate random password (copy to clipboard)",
|
||||
"generateNewPassword": "Generate new password",
|
||||
"togglePasswordVisibility": "Toggle password visibility",
|
||||
"passwordCopiedToClipboard": "Password copied to clipboard",
|
||||
"enterEmailAndOrUsernameError": "Enter email and/or username",
|
||||
"openAliasVaultToUpgrade": "Open AliasVault to upgrade",
|
||||
"vaultUpgradeRequired": "Vault upgrade required.",
|
||||
"dismissPopup": "Dismiss popup"
|
||||
"or": "oder",
|
||||
"new": "Neu",
|
||||
"cancel": "Abbrechen",
|
||||
"search": "Suche",
|
||||
"vaultLocked": "AliasVault ist gesperrt.",
|
||||
"creatingNewAlias": "Neuen Alias erstellen...",
|
||||
"noMatchesFound": "Keine Treffer gefunden",
|
||||
"searchVault": "Tresor durchsuchen...",
|
||||
"serviceName": "Name des Dienstes",
|
||||
"email": "E-Mail-Adresse",
|
||||
"username": "Benutzername",
|
||||
"password": "Passwort",
|
||||
"enterServiceName": "Name des Dienstes eingeben",
|
||||
"enterEmailAddress": "E-Mail-Adresse eingeben",
|
||||
"enterUsername": "Benutzername eingeben",
|
||||
"hideFor1Hour": "Für 1 Stunde ausblenden (aktuelle Seite)",
|
||||
"hidePermanently": "Dauerhaft ausblenden (aktuelle Seite)",
|
||||
"createRandomAlias": "Zufälligen Alias generieren",
|
||||
"createUsernamePassword": "Benutzername/Passwort erstellen",
|
||||
"randomAlias": "Zufälliger Alias",
|
||||
"usernamePassword": "Benutzername/Passwort",
|
||||
"createAndSaveAlias": "Alias erstellen und speichern",
|
||||
"createAndSaveCredential": "Zugang erstellen und speichern",
|
||||
"randomIdentityDescription": "Generiere eine zufällige Identität mit einer zufälligen E-Mail-Adresse von AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Zufällige Identität mit zufälliger E-Mail-Adresse",
|
||||
"manualCredentialDescription": "Gebe Deine eigene E-Mail-Adresse und Benutzernamen an.",
|
||||
"manualCredentialDescriptionDropdown": "Manueller Benutzername und Passwort",
|
||||
"failedToCreateIdentity": "Das Erstellen der Identität ist fehlgeschlagen. Bitte versuche es erneut.",
|
||||
"enterEmailAndOrUsername": "E-Mail-Adresse und/oder Benutzername eingeben",
|
||||
"autofillWithAliasVault": "Autofill mit AliasVault",
|
||||
"generateRandomPassword": "Zufälliges Passwort erzeugen (wird in die Zwischenablage kopiert)",
|
||||
"generateNewPassword": "Neues Passwort erzeugen",
|
||||
"togglePasswordVisibility": "Passwort ein-/ausblenden",
|
||||
"passwordCopiedToClipboard": "Passwort in die Zwischenablage kopiert",
|
||||
"enterEmailAndOrUsernameError": "E-Mail-Adresse und/oder Benutzername eingeben",
|
||||
"openAliasVaultToUpgrade": "Zum Aktualisieren AliasVault öffnen ",
|
||||
"vaultUpgradeRequired": "Aktualisierung des Tresors erforderlich.",
|
||||
"dismissPopup": "Popup schliessen"
|
||||
},
|
||||
"credentials": {
|
||||
"title": "Credentials",
|
||||
"addCredential": "Add Credential",
|
||||
"editCredential": "Edit Credential",
|
||||
"deleteCredential": "Delete Credential",
|
||||
"credentialDetails": "Credential Details",
|
||||
"serviceName": "Service Name",
|
||||
"serviceNamePlaceholder": "e.g., Gmail, Facebook, Bank",
|
||||
"website": "Website",
|
||||
"title": "Zugangsdaten",
|
||||
"addCredential": "Zugang hinzufügen",
|
||||
"editCredential": "Zugang bearbeiten",
|
||||
"deleteCredential": "Zugang löschen",
|
||||
"credentialDetails": "Details zum Zugang",
|
||||
"serviceName": "Name des Dienstes",
|
||||
"serviceNamePlaceholder": "z. B. Gmail, Facebook, Bank",
|
||||
"website": "Webseite",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "Enter username",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"generatePassword": "Generate Password",
|
||||
"copyPassword": "Copy Password",
|
||||
"showPassword": "Show Password",
|
||||
"hidePassword": "Hide Password",
|
||||
"notes": "Notes",
|
||||
"notesPlaceholder": "Additional notes...",
|
||||
"totp": "Two-Factor Authentication",
|
||||
"totpCode": "TOTP Code",
|
||||
"copyTotp": "Copy TOTP",
|
||||
"totpSecret": "TOTP Secret",
|
||||
"totpSecretPlaceholder": "Enter TOTP secret key",
|
||||
"noCredentials": "No credentials found",
|
||||
"noCredentialsDescription": "Add your first credential to get started",
|
||||
"searchCredentials": "Search credentials...",
|
||||
"searchPlaceholder": "Search credentials...",
|
||||
"welcomeTitle": "Welcome to AliasVault!",
|
||||
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
|
||||
"lastUsed": "Last used",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"username": "Benutzername",
|
||||
"usernamePlaceholder": "Benutzername eingeben",
|
||||
"password": "Passwort",
|
||||
"passwordPlaceholder": "Passwort eingeben",
|
||||
"generatePassword": "Passwort generieren",
|
||||
"copyPassword": "Passwort kopieren",
|
||||
"showPassword": "Passwort anzeigen",
|
||||
"hidePassword": "Passwort verbergen",
|
||||
"notes": "Notizen",
|
||||
"notesPlaceholder": "Zusätzliche Notizen...",
|
||||
"totp": "Zwei-Faktor-Authentifizierung",
|
||||
"totpCode": "TOTP-Code",
|
||||
"copyTotp": "TOTP kopieren",
|
||||
"totpSecret": "TOTP-Geheimcode",
|
||||
"totpSecretPlaceholder": "TOTP-Geheimcode eingeben",
|
||||
"noCredentials": "Keine Zugangsdaten gefunden",
|
||||
"noCredentialsDescription": "Erstelle Deinen ersten Zugang, um loszulegen",
|
||||
"searchPlaceholder": "Zugangsdaten suchen...",
|
||||
"welcomeTitle": "Willkommen bei AliasVault!",
|
||||
"welcomeDescription": "Du möchtest die AliasVault-Browser-Erweiterung verwenden? Navigiere zu einer Website und verwende das AliasVault-Popup-Fenster um einen neuen Zugang zu erstellen.",
|
||||
"createdAt": "Erstellt",
|
||||
"updatedAt": "Zuletzt aktualisiert",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Fill Form",
|
||||
"copyUsername": "Copy Username",
|
||||
"openWebsite": "Open Website",
|
||||
"favorite": "Favorite",
|
||||
"unfavorite": "Remove from Favorites",
|
||||
"deleteConfirm": "Are you sure you want to delete this credential?",
|
||||
"deleteSuccess": "Credential deleted successfully",
|
||||
"saveSuccess": "Credential saved successfully",
|
||||
"copySuccess": "Copied to clipboard",
|
||||
"tags": "Tags",
|
||||
"addTag": "Add Tag",
|
||||
"removeTag": "Remove Tag",
|
||||
"folder": "Folder",
|
||||
"selectFolder": "Select Folder",
|
||||
"createFolder": "Create Folder",
|
||||
"saveCredential": "Save credential",
|
||||
"deleteCredentialTitle": "Delete Credential",
|
||||
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
|
||||
"randomAlias": "Random Alias",
|
||||
"manual": "Manual",
|
||||
"service": "Service",
|
||||
"serviceUrl": "Service URL",
|
||||
"loginCredentials": "Login Credentials",
|
||||
"generateRandomUsername": "Generate random username",
|
||||
"generateRandomPassword": "Generate random password",
|
||||
"changePasswordComplexity": "Change password complexity",
|
||||
"passwordLength": "Password length",
|
||||
"includeLowercase": "Include lowercase letters",
|
||||
"includeUppercase": "Include uppercase letters",
|
||||
"includeNumbers": "Include numbers",
|
||||
"includeSpecialChars": "Include special characters",
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"fillForm": "Formular ausfüllen",
|
||||
"deleteConfirm": "Bist Du sicher, dass Du diesen Zugang löschen möchtest?",
|
||||
"saveSuccess": "Zugang erfolgreich gespeichert.",
|
||||
"tags": "Schlagwörter",
|
||||
"addTag": "Schlagwort hinzufügen",
|
||||
"removeTag": "Schlagwort entfernen",
|
||||
"folder": "Ordner",
|
||||
"selectFolder": "Ordner auswählen",
|
||||
"createFolder": "Ordner erstellen",
|
||||
"saveCredential": "Zugang speichern",
|
||||
"deleteCredentialTitle": "Zugang löschen",
|
||||
"deleteCredentialConfirm": "Bist Du sicher, dass Du diesen Zugang löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"randomAlias": "Zufälliger Alias",
|
||||
"manual": "Manuell",
|
||||
"service": "Dienst",
|
||||
"serviceUrl": "URL des Dienstes",
|
||||
"loginCredentials": "Zugangsdaten",
|
||||
"generateRandomUsername": "Zufälligen Benutzernamen generieren",
|
||||
"generateRandomPassword": "Zufälliges Passwort generieren",
|
||||
"changePasswordComplexity": "Komplexität des Passworts ändern",
|
||||
"passwordLength": "Passwortlänge",
|
||||
"includeLowercase": "Kleinbuchstaben (a-z)",
|
||||
"includeUppercase": "Großbuchstaben (A-Z)",
|
||||
"includeNumbers": "Ziffern (0-9)",
|
||||
"includeSpecialChars": "Sonderzeichen (!@#$%^&*)",
|
||||
"avoidAmbiguousChars": "Mehrdeutige Zeichen (1, l, I, 0, O, etc.) vermeiden",
|
||||
"generateNewPreview": "Neue Vorschau erstellen",
|
||||
"generateRandomAlias": "Zufälligen Alias generieren",
|
||||
"clearAliasFields": "Alias-Felder löschen",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"nickName": "Nick Name",
|
||||
"gender": "Gender",
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"errors": {
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"saveError": "Failed to save credential",
|
||||
"loadError": "Failed to load credentials",
|
||||
"deleteError": "Failed to delete credential",
|
||||
"copyError": "Failed to copy to clipboard"
|
||||
},
|
||||
"firstName": "Vorname",
|
||||
"lastName": "Nachname",
|
||||
"nickName": "Spitzname",
|
||||
"gender": "Geschlecht",
|
||||
"birthDate": "Geburtsdatum",
|
||||
"birthDatePlaceholder": "JJJJ-MM-TT",
|
||||
"metadata": "Metadaten",
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidUrl": "Invalid URL format",
|
||||
"invalidEmail": "Invalid email format",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
|
||||
}
|
||||
"required": "Dieses Feld ist ein Pflichtfeld",
|
||||
"serviceNameRequired": "Name des Dienstes ist erforderlich",
|
||||
"invalidEmail": "Ungültiges E-Mail-Format",
|
||||
"invalidDateFormat": "Bitte gib das Datum im Format JJJJ-MM-TT ein."
|
||||
},
|
||||
"privateEmailTitle": "Private E-Mail-Adresse",
|
||||
"privateEmailAliasVaultServer": "AliasVault-Server",
|
||||
"privateEmailDescription": "Ende-zu-Ende verschlüsselt, vollständig privat.",
|
||||
"publicEmailTitle": "Öffentliche Temp-E-Mail-Anbieter",
|
||||
"publicEmailDescription": "Anonyme, aber beschränkte Privatsphäre. E-Mail-Inhalt ist für jeden lesbar, der die Adresse kennt.",
|
||||
"useDomainChooser": "Domain-Auswahl verwenden",
|
||||
"enterCustomDomain": "Eigene Domain eingeben",
|
||||
"enterFullEmail": "Vollständige E-Mail-Adresse eingeben",
|
||||
"enterEmailPrefix": "E-Mail-Präfix eingeben"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
"deleteEmailTitle": "Delete Email",
|
||||
"deleteEmailConfirm": "Are you sure you want to permanently delete this email?",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"date": "Date",
|
||||
"emailContent": "Email content",
|
||||
"attachments": "Attachments",
|
||||
"emailNotFound": "Email not found",
|
||||
"noEmails": "No emails found",
|
||||
"noEmailsDescription": "You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.",
|
||||
"title": "E-Mails",
|
||||
"deleteEmailTitle": "E-Mail löschen",
|
||||
"deleteEmailConfirm": "Bist Du sicher, dass Du diese E-Mail unwiderruflich löschen möchtest?",
|
||||
"from": "Von",
|
||||
"to": "An",
|
||||
"date": "Datum",
|
||||
"emailContent": "Inhalt der E-Mail",
|
||||
"attachments": "Anhänge",
|
||||
"emailNotFound": "E-Mail nicht gefunden",
|
||||
"noEmails": "Keine E-Mails gefunden",
|
||||
"noEmailsDescription": "Du hast bisher keine E-Mails an Deine privaten E-Mail-Adressen erhalten. Neue E-Mails werden hier angezeigt, sobald sie eintreffen.",
|
||||
"dateFormat": {
|
||||
"justNow": "just now",
|
||||
"minutesAgo_single": "{{count}} min ago",
|
||||
"minutesAgo_plural": "{{count}} mins ago",
|
||||
"hoursAgo_single": "{{count}} hr ago",
|
||||
"hoursAgo_plural": "{{count}} hrs ago",
|
||||
"yesterday": "yesterday"
|
||||
"justNow": "gerade eben",
|
||||
"minutesAgo_single": "vor {{count}} Minute",
|
||||
"minutesAgo_plural": "vor {{count}} Minuten",
|
||||
"hoursAgo_single": "vor {{count}} Stunde",
|
||||
"hoursAgo_plural": "vor {{count}} Stunden",
|
||||
"yesterday": "gestern"
|
||||
},
|
||||
"errors": {
|
||||
"emailLoadError": "An error occurred while loading emails. Please try again later.",
|
||||
"emailUnexpectedError": "An unexpected error occurred while loading emails. Please try again later."
|
||||
"emailLoadError": "Beim Laden der E-Mails ist ein Fehler aufgetreten. Bitte versuche es später erneut.",
|
||||
"emailUnexpectedError": "Beim Laden der E-Mails ist ein unerwarteter Fehler aufgetreten. Bitte versuche es später erneut."
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "The current chosen email address is already in use. Please change the email address by editing this credential.",
|
||||
"CLAIM_DOES_NOT_EXIST": "An error occurred while trying to load the emails. Please try to edit and save the credential entry to synchronize the database, then try again."
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "Die aktuell gewählte E-Mail-Adresse wird bereits verwendet. Bitte ändere die E-Mail-Adresse, indem Du diese Zugangsdaten bearbeitest.",
|
||||
"CLAIM_DOES_NOT_EXIST": "Beim Laden der E-Mails ist ein Fehler aufgetreten. Bitte bearbeite und speichere den Eintrag, um die Datenbank zu synchronisieren, und versuche es dann erneut."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"serverUrl": "Server URL",
|
||||
"language": "Language",
|
||||
"autofillEnabled": "Enable Autofill",
|
||||
"title": "Einstellungen",
|
||||
"serverUrl": "URL des Servers",
|
||||
"language": "Sprache",
|
||||
"autofillEnabled": "Autofill aktivieren",
|
||||
"version": "Version",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"openWebApp": "Open web app",
|
||||
"loggedIn": "Logged in",
|
||||
"logout": "Logout",
|
||||
"globalSettings": "Global Settings",
|
||||
"autofillPopup": "Autofill popup",
|
||||
"activeOnAllSites": "Active on all sites (unless disabled below)",
|
||||
"disabledOnAllSites": "Disabled on all sites",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"rightClickContextMenu": "Right-click context menu",
|
||||
"siteSpecificSettings": "Site-Specific Settings",
|
||||
"autofillPopupOn": "Autofill popup on: ",
|
||||
"enabledForThisSite": "Enabled for this site",
|
||||
"disabledForThisSite": "Disabled for this site",
|
||||
"temporarilyDisabledUntil": "Temporarily disabled until ",
|
||||
"resetAllSiteSettings": "Reset all site-specific settings",
|
||||
"appearance": "Appearance",
|
||||
"theme": "Theme",
|
||||
"useDefault": "Use default",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||
"configureKeyboardShortcuts": "Configure keyboard shortcuts",
|
||||
"configure": "Configure",
|
||||
"openInNewWindow": "In neuem Fenster öffnen",
|
||||
"openWebApp": "Web-App öffnen",
|
||||
"loggedIn": "Angemeldet",
|
||||
"logout": "Abmelden",
|
||||
"globalSettings": "Allgemeine Einstellungen",
|
||||
"autofillPopup": "Autofill-Popup",
|
||||
"activeOnAllSites": "Auf allen Seiten aktiv (sofern nicht unten deaktiviert)",
|
||||
"disabledOnAllSites": "Auf allen Seiten deaktiviert",
|
||||
"enabled": "Aktiviert",
|
||||
"disabled": "Deaktiviert",
|
||||
"rightClickContextMenu": "Kontextmenü mit Rechtsklick",
|
||||
"autofillMatching": "Autofill-Übereinstimmung",
|
||||
"autofillMatchingMode": "Autofill-Übereinstimmungs-Modus",
|
||||
"autofillMatchingModeDescription": "Legt fest, welche Zugangsdaten als Übereinstimmung angesehen werden und wird als Vorschlag im Autofill-Popup für eine bestimmte Website angezeigt.",
|
||||
"autofillMatchingDefault": "URL + Subdomain + Wildcard-Name",
|
||||
"autofillMatchingUrlSubdomain": "URL + Subdomain",
|
||||
"autofillMatchingUrlExact": "Nur exakte URL-Domain",
|
||||
"siteSpecificSettings": "Seitenspezifische Einstellungen",
|
||||
"autofillPopupOn": "Autofill-Popup auf: ",
|
||||
"enabledForThisSite": "Für diese Seite aktiviert",
|
||||
"disabledForThisSite": "Für diese Seite deaktivieren",
|
||||
"temporarilyDisabledUntil": "Vorübergehend deaktiviert bis ",
|
||||
"resetAllSiteSettings": "Alle seitenspezifischen Einstellungen zurücksetzen",
|
||||
"appearance": "Erscheinungsbild",
|
||||
"theme": "Thema",
|
||||
"useDefault": "Standard verwenden",
|
||||
"light": "Hell",
|
||||
"dark": "Dunkel",
|
||||
"keyboardShortcuts": "Tastaturkürzel",
|
||||
"configureKeyboardShortcuts": "Tastaturkürzel konfigurieren",
|
||||
"configure": "Konfigurieren",
|
||||
"security": "Sicherheit",
|
||||
"clipboardClearTimeout": "Zwischenablage nach dem Kopieren automatisch löschen",
|
||||
"clipboardClearTimeoutDescription": "Zwischenablage nach dem Kopieren sensibler Daten automatisch löschen",
|
||||
"clipboardClearDisabled": "Niemals löschen",
|
||||
"clipboardClear5Seconds": "Nach 5 Sekunden löschen",
|
||||
"clipboardClear10Seconds": "Nach 10 Sekunden löschen",
|
||||
"clipboardClear15Seconds": "Nach 15 Sekunden löschen",
|
||||
"autoLockTimeout": "Sperr-Timeout",
|
||||
"autoLockTimeoutDescription": "Tresor bei Inaktivität automatisch sperren",
|
||||
"autoLockTimeoutHelp": "Der Tresor wird erst nach dem angegebenen Zeitraum der Inaktivität gesperrt (keine Nutzung von Autofill oder Öffnen des Erweiterungs-Popups). Der Tresor wird immer gesperrt, wenn der Browser geschlossen wird, unabhängig von dieser Einstellung.",
|
||||
"autoLockNever": "Niemals",
|
||||
"autoLock15Seconds": "15 Sekunden",
|
||||
"autoLock1Minute": "1 Minute",
|
||||
"autoLock5Minutes": "5 Minuten",
|
||||
"autoLock15Minutes": "15 Minuten",
|
||||
"autoLock30Minutes": "30 Minuten",
|
||||
"autoLock1Hour": "1 Stunde",
|
||||
"autoLock4Hours": "4 Stunden",
|
||||
"autoLock8Hours": "8 Stunden",
|
||||
"autoLock24Hours": "24 Stunden",
|
||||
"versionPrefix": "Version ",
|
||||
"preferences": "Einstellungen",
|
||||
"autofillSettings": "Autofill-Einstellungen",
|
||||
"clipboardSettings": "Zwischenablage-Einstellungen",
|
||||
"contextMenuSettings": "Kontextmenü-Einstellungen",
|
||||
"contextMenu": "Kontextmenü",
|
||||
"contextMenuEnabled": "Kontextmenü ist aktiviert",
|
||||
"contextMenuDisabled": "Kontextmenü ist deaktiviert",
|
||||
"contextMenuDescription": "Rechtsklicke auf Eingabefelder, um auf AliasVault-Optionen zuzugreifen",
|
||||
"selectLanguage": "Sprache auswählen",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL is required",
|
||||
"apiUrlInvalid": "Please enter a valid API URL",
|
||||
"clientUrlRequired": "Client URL is required",
|
||||
"clientUrlInvalid": "Please enter a valid client URL"
|
||||
"apiUrlRequired": "API-URL ist erforderlich",
|
||||
"apiUrlInvalid": "Bitte gib eine gültige API-URL ein",
|
||||
"clientUrlRequired": "Client-URL ist erforderlich",
|
||||
"clientUrlInvalid": "Bitte gib eine gültige Client-URL ein"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "Upgrade Vault",
|
||||
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
|
||||
"versionInformation": "Version Information",
|
||||
"yourVault": "Your vault:",
|
||||
"newVersion": "New version:",
|
||||
"upgrade": "Upgrade Vault",
|
||||
"upgrading": "Upgrading...",
|
||||
"logout": "Logout",
|
||||
"whatsNew": "What's New",
|
||||
"whatsNewDescription": "An upgrade is required to support the following changes:",
|
||||
"noDescriptionAvailable": "No description available for this version.",
|
||||
"okay": "Ok",
|
||||
"title": "Tresor aktualisieren",
|
||||
"subtitle": "AliasVault wurde aktualisiert. Dadurch muss auch Dein Tresor aktualisiert werden. Dies sollte nur wenige Sekunden dauern.",
|
||||
"versionInformation": "Versionsinformationen",
|
||||
"yourVault": "Dein Tresor:",
|
||||
"newVersion": "Neue Version:",
|
||||
"upgrade": "Tresor aktualisieren",
|
||||
"upgrading": "Aktualisieren...",
|
||||
"logout": "Abmelden",
|
||||
"whatsNew": "Neu in dieser Version",
|
||||
"whatsNewDescription": "Eine Aktualisierung ist erforderlich, um die folgenden Änderungen zu unterstützen:",
|
||||
"noDescriptionAvailable": "Für diese Version ist keine Beschreibung vorhanden.",
|
||||
"okay": "OK",
|
||||
"status": {
|
||||
"preparingUpgrade": "Preparing upgrade...",
|
||||
"vaultAlreadyUpToDate": "Vault is already up to date",
|
||||
"startingDatabaseTransaction": "Starting database transaction...",
|
||||
"applyingDatabaseMigrations": "Applying database migrations...",
|
||||
"applyingMigration": "Applying migration {{current}} of {{total}}...",
|
||||
"committingChanges": "Committing changes..."
|
||||
"preparingUpgrade": "Aktualisierung wird vorbereitet...",
|
||||
"vaultAlreadyUpToDate": "Tresor ist bereits aktualisiert",
|
||||
"startingDatabaseTransaction": "Datenbanktransaktion wird gestartet...",
|
||||
"applyingDatabaseMigrations": "Datenbankmigration wird durchgeführt...",
|
||||
"applyingMigration": "Führe Migration {{current}} von {{total}} durch...",
|
||||
"committingChanges": "Änderungen werden übernommen..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Error",
|
||||
"unableToGetVersionInfo": "Unable to get version information. Please try again.",
|
||||
"selfHostedServer": "Self-Hosted Server",
|
||||
"selfHostedWarning": "If you're using a self-hosted server, make sure to also update your self-hosted instance as otherwise logging in to the web client will stop working.",
|
||||
"cancel": "Cancel",
|
||||
"continueUpgrade": "Continue Upgrade",
|
||||
"upgradeFailed": "Upgrade Failed",
|
||||
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
|
||||
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
|
||||
"error": "Fehler",
|
||||
"unableToGetVersionInfo": "Versionsinformationen konnten nicht abgerufen werden. Bitte versuche es erneut.",
|
||||
"selfHostedServer": "Selbstgehosteter Server",
|
||||
"selfHostedWarning": "Nutzt Du einen selbst gehosteten Server, musst Du Deine Instanz ebenfalls updaten. Andernfalls kannst Du Dich im Web-Client nicht mehr anmelden.",
|
||||
"cancel": "Abbrechen",
|
||||
"continueUpgrade": "Aktualisierung fortsetzen",
|
||||
"upgradeFailed": "Aktualisierung fehlgeschlagen",
|
||||
"failedToApplyMigration": "Migration fehlgeschlagen ({{current}} von {{total}})",
|
||||
"unknownErrorDuringUpgrade": "Bei der Aktualisierung ist ein unbekannter Fehler aufgetreten. Bitte versuche es erneut."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,26 +89,17 @@
|
||||
"executingOperation": "Executing operation...",
|
||||
"loadMore": "Load more",
|
||||
"errors": {
|
||||
"VaultMergeRequired": "Your vault needs to be updated. Please login on the AliasVault website and follow the steps.",
|
||||
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
|
||||
"NoVaultFound": "Your account does not have a vault yet. Please complete the tutorial in the AliasVault web client before using the browser extension.",
|
||||
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
|
||||
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
|
||||
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
|
||||
"unknownError": "An unknown error occurred",
|
||||
"failedToStoreVault": "Failed to store vault",
|
||||
"vaultNotAvailable": "Vault not available",
|
||||
"failedToGetVault": "Failed to get vault",
|
||||
"failedToRetrieveData": "Failed to retrieve data",
|
||||
"vaultIsLocked": "Vault is locked",
|
||||
"failedToGetCredentials": "Failed to get credentials",
|
||||
"failedToCreateIdentity": "Failed to create identity",
|
||||
"failedToGetDefaultEmailDomain": "Failed to get default email domain",
|
||||
"failedToGetDefaultIdentitySettings": "Failed to get default identity settings",
|
||||
"failedToGetPasswordSettings": "Failed to get password settings",
|
||||
"failedToUploadVault": "Failed to upload vault",
|
||||
"noDerivedKeyAvailable": "No derived key available for encryption",
|
||||
"failedToUploadVaultToServer": "Failed to upload new vault to server",
|
||||
"noVaultOrDerivedKeyFound": "No vault or derived key found"
|
||||
"passwordChanged": "Your password has changed since the last time you logged in. Please login again for security reasons."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",
|
||||
@@ -118,8 +109,6 @@
|
||||
"INVALID_AUTHENTICATOR_CODE": "Invalid authenticator code. Please try again.",
|
||||
"INVALID_RECOVERY_CODE": "Invalid recovery code. Please try again.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Refresh token is required.",
|
||||
"USER_NOT_FOUND_IN_TOKEN": "User not found in token.",
|
||||
"USER_NOT_FOUND_IN_DATABASE": "User not found in database.",
|
||||
"INVALID_REFRESH_TOKEN": "Invalid refresh token.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token revoked successfully.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "New account registration is currently disabled on this server. Please contact the administrator.",
|
||||
@@ -206,23 +195,15 @@
|
||||
"totpSecretPlaceholder": "Enter TOTP secret key",
|
||||
"noCredentials": "No credentials found",
|
||||
"noCredentialsDescription": "Add your first credential to get started",
|
||||
"searchCredentials": "Search credentials...",
|
||||
"searchPlaceholder": "Search credentials...",
|
||||
"welcomeTitle": "Welcome to AliasVault!",
|
||||
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
|
||||
"lastUsed": "Last used",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Fill Form",
|
||||
"copyUsername": "Copy Username",
|
||||
"openWebsite": "Open Website",
|
||||
"favorite": "Favorite",
|
||||
"unfavorite": "Remove from Favorites",
|
||||
"deleteConfirm": "Are you sure you want to delete this credential?",
|
||||
"deleteSuccess": "Credential deleted successfully",
|
||||
"saveSuccess": "Credential saved successfully",
|
||||
"copySuccess": "Copied to clipboard",
|
||||
"tags": "Tags",
|
||||
"addTag": "Add Tag",
|
||||
"removeTag": "Remove Tag",
|
||||
@@ -248,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
@@ -256,20 +238,21 @@
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"errors": {
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"saveError": "Failed to save credential",
|
||||
"loadError": "Failed to load credentials",
|
||||
"deleteError": "Failed to delete credential",
|
||||
"copyError": "Failed to copy to clipboard"
|
||||
},
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidUrl": "Invalid URL format",
|
||||
"invalidEmail": "Invalid email format",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
|
||||
}
|
||||
},
|
||||
"privateEmailTitle": "Private Email",
|
||||
"privateEmailAliasVaultServer": "AliasVault server",
|
||||
"privateEmailDescription": "E2E encrypted, fully private.",
|
||||
"publicEmailTitle": "Public Temp Email Providers",
|
||||
"publicEmailDescription": "Anonymous but limited privacy. Email content is readable by anyone that knows the address.",
|
||||
"useDomainChooser": "Use domain chooser",
|
||||
"enterCustomDomain": "Enter custom domain",
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
@@ -317,6 +300,12 @@
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"rightClickContextMenu": "Right-click context menu",
|
||||
"autofillMatching": "Autofill Matching",
|
||||
"autofillMatchingMode": "Autofill matching mode",
|
||||
"autofillMatchingModeDescription": "Determines which credentials are considered a match and shown as suggestions in the autofill popup for a given website.",
|
||||
"autofillMatchingDefault": "URL + subdomain + name wildcard",
|
||||
"autofillMatchingUrlSubdomain": "URL + subdomain",
|
||||
"autofillMatchingUrlExact": "Exact URL domain only",
|
||||
"siteSpecificSettings": "Site-Specific Settings",
|
||||
"autofillPopupOn": "Autofill popup on: ",
|
||||
"enabledForThisSite": "Enabled for this site",
|
||||
@@ -331,7 +320,36 @@
|
||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||
"configureKeyboardShortcuts": "Configure keyboard shortcuts",
|
||||
"configure": "Configure",
|
||||
"security": "Security",
|
||||
"clipboardClearTimeout": "Clear clipboard after copying",
|
||||
"clipboardClearTimeoutDescription": "Automatically clear the clipboard after copying sensitive data",
|
||||
"clipboardClearDisabled": "Never clear",
|
||||
"clipboardClear5Seconds": "Clear after 5 seconds",
|
||||
"clipboardClear10Seconds": "Clear after 10 seconds",
|
||||
"clipboardClear15Seconds": "Clear after 15 seconds",
|
||||
"autoLockTimeout": "Auto-lock Timeout",
|
||||
"autoLockTimeoutDescription": "Automatically lock the vault after a period of inactivity",
|
||||
"autoLockTimeoutHelp": "The vault will only lock after the specified period of inactivity (no autofill usage or extension popup opened). The vault will always lock when the browser is closed, regardless of this setting.",
|
||||
"autoLockNever": "Never",
|
||||
"autoLock15Seconds": "15 seconds",
|
||||
"autoLock1Minute": "1 minute",
|
||||
"autoLock5Minutes": "5 minutes",
|
||||
"autoLock15Minutes": "15 minutes",
|
||||
"autoLock30Minutes": "30 minutes",
|
||||
"autoLock1Hour": "1 hour",
|
||||
"autoLock4Hours": "4 hours",
|
||||
"autoLock8Hours": "8 hours",
|
||||
"autoLock24Hours": "24 hours",
|
||||
"versionPrefix": "Version ",
|
||||
"preferences": "Preferences",
|
||||
"autofillSettings": "Autofill Settings",
|
||||
"clipboardSettings": "Clipboard Settings",
|
||||
"contextMenuSettings": "Context Menu Settings",
|
||||
"contextMenu": "Context Menu",
|
||||
"contextMenuEnabled": "Context menu is enabled",
|
||||
"contextMenuDisabled": "Context menu is disabled",
|
||||
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
|
||||
"selectLanguage": "Select Language",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL is required",
|
||||
"apiUrlInvalid": "Please enter a valid API URL",
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"loginTitle": "Log in to AliasVault",
|
||||
"username": "Username or email",
|
||||
"usernamePlaceholder": "name / name@company.com",
|
||||
"password": "Password",
|
||||
"password": "Contraseña",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"rememberMe": "Remember me",
|
||||
"loginButton": "Login",
|
||||
"loginButton": "Iniciar sesión",
|
||||
"noAccount": "No account yet?",
|
||||
"createVault": "Create new vault",
|
||||
"twoFactorTitle": "Please enter the authentication code from your authenticator app.",
|
||||
@@ -15,7 +15,7 @@
|
||||
"verify": "Verify",
|
||||
"cancel": "Cancel",
|
||||
"twoFactorNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.",
|
||||
"masterPassword": "Master Password",
|
||||
"masterPassword": "Contraseña maestra",
|
||||
"unlockVault": "Unlock Vault",
|
||||
"unlockTitle": "Unlock Your Vault",
|
||||
"unlockDescription": "Enter your master password to unlock your vault.",
|
||||
@@ -89,26 +89,17 @@
|
||||
"executingOperation": "Executing operation...",
|
||||
"loadMore": "Load more",
|
||||
"errors": {
|
||||
"VaultMergeRequired": "Your vault needs to be updated. Please login on the AliasVault website and follow the steps.",
|
||||
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
|
||||
"NoVaultFound": "Your account does not have a vault yet. Please complete the tutorial in the AliasVault web client before using the browser extension.",
|
||||
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
|
||||
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
|
||||
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
|
||||
"unknownError": "An unknown error occurred",
|
||||
"failedToStoreVault": "Failed to store vault",
|
||||
"vaultNotAvailable": "Vault not available",
|
||||
"failedToGetVault": "Failed to get vault",
|
||||
"failedToRetrieveData": "Failed to retrieve data",
|
||||
"vaultIsLocked": "Vault is locked",
|
||||
"failedToGetCredentials": "Failed to get credentials",
|
||||
"failedToCreateIdentity": "Failed to create identity",
|
||||
"failedToGetDefaultEmailDomain": "Failed to get default email domain",
|
||||
"failedToGetDefaultIdentitySettings": "Failed to get default identity settings",
|
||||
"failedToGetPasswordSettings": "Failed to get password settings",
|
||||
"failedToUploadVault": "Failed to upload vault",
|
||||
"noDerivedKeyAvailable": "No derived key available for encryption",
|
||||
"failedToUploadVaultToServer": "Failed to upload new vault to server",
|
||||
"noVaultOrDerivedKeyFound": "No vault or derived key found"
|
||||
"passwordChanged": "Your password has changed since the last time you logged in. Please login again for security reasons."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",
|
||||
@@ -118,8 +109,6 @@
|
||||
"INVALID_AUTHENTICATOR_CODE": "Invalid authenticator code. Please try again.",
|
||||
"INVALID_RECOVERY_CODE": "Invalid recovery code. Please try again.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Refresh token is required.",
|
||||
"USER_NOT_FOUND_IN_TOKEN": "User not found in token.",
|
||||
"USER_NOT_FOUND_IN_DATABASE": "User not found in database.",
|
||||
"INVALID_REFRESH_TOKEN": "Invalid refresh token.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token revoked successfully.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "New account registration is currently disabled on this server. Please contact the administrator.",
|
||||
@@ -206,23 +195,15 @@
|
||||
"totpSecretPlaceholder": "Enter TOTP secret key",
|
||||
"noCredentials": "No credentials found",
|
||||
"noCredentialsDescription": "Add your first credential to get started",
|
||||
"searchCredentials": "Search credentials...",
|
||||
"searchPlaceholder": "Search credentials...",
|
||||
"welcomeTitle": "Welcome to AliasVault!",
|
||||
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
|
||||
"lastUsed": "Last used",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Fill Form",
|
||||
"copyUsername": "Copy Username",
|
||||
"openWebsite": "Open Website",
|
||||
"favorite": "Favorite",
|
||||
"unfavorite": "Remove from Favorites",
|
||||
"deleteConfirm": "Are you sure you want to delete this credential?",
|
||||
"deleteSuccess": "Credential deleted successfully",
|
||||
"saveSuccess": "Credential saved successfully",
|
||||
"copySuccess": "Copied to clipboard",
|
||||
"tags": "Tags",
|
||||
"addTag": "Add Tag",
|
||||
"removeTag": "Remove Tag",
|
||||
@@ -248,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
@@ -256,20 +238,21 @@
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"errors": {
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"saveError": "Failed to save credential",
|
||||
"loadError": "Failed to load credentials",
|
||||
"deleteError": "Failed to delete credential",
|
||||
"copyError": "Failed to copy to clipboard"
|
||||
},
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidUrl": "Invalid URL format",
|
||||
"invalidEmail": "Invalid email format",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
|
||||
}
|
||||
},
|
||||
"privateEmailTitle": "Private Email",
|
||||
"privateEmailAliasVaultServer": "AliasVault server",
|
||||
"privateEmailDescription": "E2E encrypted, fully private.",
|
||||
"publicEmailTitle": "Public Temp Email Providers",
|
||||
"publicEmailDescription": "Anonymous but limited privacy. Email content is readable by anyone that knows the address.",
|
||||
"useDomainChooser": "Use domain chooser",
|
||||
"enterCustomDomain": "Enter custom domain",
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
@@ -317,6 +300,12 @@
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"rightClickContextMenu": "Right-click context menu",
|
||||
"autofillMatching": "Autofill Matching",
|
||||
"autofillMatchingMode": "Autofill matching mode",
|
||||
"autofillMatchingModeDescription": "Determines which credentials are considered a match and shown as suggestions in the autofill popup for a given website.",
|
||||
"autofillMatchingDefault": "URL + subdomain + name wildcard",
|
||||
"autofillMatchingUrlSubdomain": "URL + subdomain",
|
||||
"autofillMatchingUrlExact": "Exact URL domain only",
|
||||
"siteSpecificSettings": "Site-Specific Settings",
|
||||
"autofillPopupOn": "Autofill popup on: ",
|
||||
"enabledForThisSite": "Enabled for this site",
|
||||
@@ -331,7 +320,36 @@
|
||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||
"configureKeyboardShortcuts": "Configure keyboard shortcuts",
|
||||
"configure": "Configure",
|
||||
"security": "Security",
|
||||
"clipboardClearTimeout": "Clear clipboard after copying",
|
||||
"clipboardClearTimeoutDescription": "Automatically clear the clipboard after copying sensitive data",
|
||||
"clipboardClearDisabled": "Never clear",
|
||||
"clipboardClear5Seconds": "Clear after 5 seconds",
|
||||
"clipboardClear10Seconds": "Clear after 10 seconds",
|
||||
"clipboardClear15Seconds": "Clear after 15 seconds",
|
||||
"autoLockTimeout": "Auto-lock Timeout",
|
||||
"autoLockTimeoutDescription": "Automatically lock the vault after a period of inactivity",
|
||||
"autoLockTimeoutHelp": "The vault will only lock after the specified period of inactivity (no autofill usage or extension popup opened). The vault will always lock when the browser is closed, regardless of this setting.",
|
||||
"autoLockNever": "Never",
|
||||
"autoLock15Seconds": "15 seconds",
|
||||
"autoLock1Minute": "1 minute",
|
||||
"autoLock5Minutes": "5 minutes",
|
||||
"autoLock15Minutes": "15 minutes",
|
||||
"autoLock30Minutes": "30 minutes",
|
||||
"autoLock1Hour": "1 hour",
|
||||
"autoLock4Hours": "4 hours",
|
||||
"autoLock8Hours": "8 hours",
|
||||
"autoLock24Hours": "24 hours",
|
||||
"versionPrefix": "Version ",
|
||||
"preferences": "Preferences",
|
||||
"autofillSettings": "Autofill Settings",
|
||||
"clipboardSettings": "Clipboard Settings",
|
||||
"contextMenuSettings": "Context Menu Settings",
|
||||
"contextMenu": "Context Menu",
|
||||
"contextMenuEnabled": "Context menu is enabled",
|
||||
"contextMenuDisabled": "Context menu is disabled",
|
||||
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
|
||||
"selectLanguage": "Select Language",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL is required",
|
||||
"apiUrlInvalid": "Please enter a valid API URL",
|
||||
|
||||
393
apps/browser-extension/src/i18n/locales/fi.json
Normal file
393
apps/browser-extension/src/i18n/locales/fi.json
Normal file
@@ -0,0 +1,393 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "Kirjaudu sisään AliasVaultiin",
|
||||
"username": "Käyttäjänimi tai sähköposti",
|
||||
"usernamePlaceholder": "nimi / nimi@yritys.fi",
|
||||
"password": "Salasana",
|
||||
"passwordPlaceholder": "Syötä salasanasi",
|
||||
"rememberMe": "Muista minut",
|
||||
"loginButton": "Kirjaudu",
|
||||
"noAccount": "Eikö sinulla ole vielä tiliä?",
|
||||
"createVault": "Luo uusi holvi",
|
||||
"twoFactorTitle": "Ole hyvä ja syötä tunnistautumiskoodi tunnistautumissovelluksestasi.",
|
||||
"authCode": "Tunnistautumiskoodi",
|
||||
"authCodePlaceholder": "Syötä 6-numeroinen koodi",
|
||||
"verify": "Vahvista",
|
||||
"cancel": "Peruuta",
|
||||
"twoFactorNote": "Huomautus: jos sinulla ei ole pääsyä tunnistautumislaitteeseen, voit palauttaa 2FA:n palautuskoodilla kirjautumalla sisään sivuston kautta.",
|
||||
"masterPassword": "Pääsalasana",
|
||||
"unlockVault": "Avaa holvi",
|
||||
"unlockTitle": "Avaa Holvisi",
|
||||
"unlockDescription": "Syötä pääsalasanasi avataksesi holvisi lukituksen.",
|
||||
"logout": "Kirjaudu ulos",
|
||||
"logoutConfirm": "Oletko varma, että haluat kirjautua ulos?",
|
||||
"sessionExpired": "Istuntosi on vanhentunut. Ole hyvä ja kirjaudu uudelleen.",
|
||||
"unlockSuccess": "Holvi avattu onnistuneesti!",
|
||||
"unlockSuccessTitle": "Holvisi lukitus on onnistuneesti avattu",
|
||||
"unlockSuccessDescription": "Voit nyt käyttää selaimessasi olevia kirjautumislomakkeita automaattisesti.",
|
||||
"closePopup": "Sulje tämä ponnahdusikkuna",
|
||||
"browseVault": "Selaa holvin sisältöä",
|
||||
"connectingTo": "Yhdistetään palvelimeen",
|
||||
"switchAccounts": "Vaihdetaanko tiliä?",
|
||||
"loggedIn": "Kirjautuneena",
|
||||
"errors": {
|
||||
"invalidCode": "Anna kelvollinen 6-numeroinen tunnistautumiskoodi.",
|
||||
"serverError": "AliasVault-palvelimeen ei saatu yhteyttä. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.",
|
||||
"noToken": "Kirjautuminen epäonnistui -- tunnusta ei palautettu",
|
||||
"migrationError": "Tapahtui virhe tarkistettaessa odottavia siirtoja.",
|
||||
"wrongPassword": "Virheellinen salasana. Yritä uudelleen.",
|
||||
"accountLocked": "Tili on tilapäisesti lukittu liian monen epäonnistuneen yrityksen vuoksi. Yritä myöhemmin uudelleen.",
|
||||
"networkError": "Verkkovirhe: tarkista yhteytesi ja yritä uudelleen.",
|
||||
"loginDataMissing": "Kirjautumisistunto on vanhentunut. Yritä uudelleen."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "Käyttäjätunnukset",
|
||||
"emails": "Sähköpostit",
|
||||
"settings": "Asetukset"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Ladataan...",
|
||||
"error": "Virhe",
|
||||
"success": "Onnistui",
|
||||
"cancel": "Peruuta",
|
||||
"use": "Käytä",
|
||||
"delete": "Poista",
|
||||
"close": "Sulje",
|
||||
"copied": "Kopioitu!",
|
||||
"openInNewWindow": "Avaa uudessa ikkunassa",
|
||||
"language": "Kieli",
|
||||
"enabled": "Käytössä",
|
||||
"disabled": "Pois käytöstä",
|
||||
"showPassword": "Näytä salasana",
|
||||
"hidePassword": "Piilota salasana",
|
||||
"copyToClipboard": "Kopioi leikepöydälle",
|
||||
"loadingEmails": "Ladataan sähköposteja...",
|
||||
"loadingTotpCodes": "Ladataan TOTP-koodeja...",
|
||||
"attachments": "Liitteet",
|
||||
"loadingAttachments": "Ladataan liitteitä...",
|
||||
"settings": "Asetukset",
|
||||
"recentEmails": "Viimeisimmät sähköpostit",
|
||||
"loginCredentials": "Sisäänkirjautumistiedot",
|
||||
"twoFactorAuthentication": "Kaksivaiheinen tunnistautuminen",
|
||||
"alias": "Alias",
|
||||
"notes": "Muistiinpanot",
|
||||
"fullName": "Koko nimi",
|
||||
"firstName": "Etunimi",
|
||||
"lastName": "Sukunimi",
|
||||
"birthDate": "Syntymäpäivä",
|
||||
"nickname": "Lempinimi",
|
||||
"email": "Sähköposti",
|
||||
"username": "Käyttäjänimi",
|
||||
"password": "Salasana",
|
||||
"syncingVault": "Synkronoidaan holvia",
|
||||
"savingChangesToVault": "Tallennetaan muutoksia holviin",
|
||||
"uploadingVaultToServer": "Lähetetään holvi palvelimelle",
|
||||
"checkingVaultUpdates": "Tarkistetaan holvin päivityksiä",
|
||||
"syncingUpdatedVault": "Synkronoidaan päivitettyä holvia",
|
||||
"executingOperation": "Suoritetaan toimintoa...",
|
||||
"loadMore": "Lataa lisää",
|
||||
"errors": {
|
||||
"VaultOutdated": "Holvisi on vanhentunut. Kirjaudu AliasVaultin kotisivulle ja noudata ohjeita.",
|
||||
"serverNotAvailable": "AliasVault-palvelin ei ole käytettävissä. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.",
|
||||
"clientVersionNotSupported": "Palvelin ei enää tue tätä AliasVault-selainlaajennuksen versiota. Ole hyvä ja päivitä selaimen laajennus uusimpaan versioon.",
|
||||
"serverVersionNotSupported": "AliasVault-palvelin on päivitettävä uudempaan versioon, jotta voit käyttää tätä selainlaajennusta. Ota yhteyttä tukeen, jos tarvitset apua.",
|
||||
"unknownError": "Tapahtui tuntematon virhe",
|
||||
"failedToStoreVault": "Holvin tallentaminen epäonnistui",
|
||||
"vaultNotAvailable": "Holvi ei ole käytettävissä",
|
||||
"failedToRetrieveData": "Tietojen nouto epäonnistui",
|
||||
"vaultIsLocked": "Holvi on lukittu",
|
||||
"failedToUploadVault": "Holvin lataaminen epäonnistui",
|
||||
"passwordChanged": "Salasanasi on muuttunut edellisen kirjautumisen jälkeen. Ole hyvä ja kirjaudu uudelleen turvallisuussyistä."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "Tapahtui tuntematon virhe. Yritä uudelleen.",
|
||||
"ACCOUNT_LOCKED": "Tili on tilapäisesti lukittu liian monen epäonnistuneen yrityksen vuoksi. Yritä myöhemmin uudelleen.",
|
||||
"ACCOUNT_BLOCKED": "Tilisi on poistettu käytöstä. Jos uskot, että tämä on virhe, ota yhteyttä tukeen.",
|
||||
"USER_NOT_FOUND": "Virheellinen käyttäjänimi tai salasana. Yritä uudelleen.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Virheellinen tunnistautumiskoodi. Yritä uudelleen.",
|
||||
"INVALID_RECOVERY_CODE": "Virheellinen palautuskoodi. Yritä uudelleen.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Päivitysavain vaaditaan.",
|
||||
"INVALID_REFRESH_TOKEN": "Virheellinen päivitysavain.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Päivitysavain peruutettu onnistuneesti.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "Uuden tilin rekisteröinti on poistettu käytöstä tällä palvelimella. Ota yhteyttä järjestelmänvalvojaan.",
|
||||
"USERNAME_REQUIRED": "Käyttäjänimi vaaditaan.",
|
||||
"USERNAME_ALREADY_IN_USE": "Käyttäjätunnus on jo käytössä",
|
||||
"USERNAME_AVAILABLE": "Käyttäjänimi on saatavilla.",
|
||||
"USERNAME_MISMATCH": "Käyttäjänimi ei vastaa nykyistä käyttäjää.",
|
||||
"PASSWORD_MISMATCH": "Annettu salasana ei vastaa nykyistä salasanaasi.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Käyttäjätili onnistuneesti poistettu,.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Käyttäjätunnus ei voi olla tyhjä.",
|
||||
"USERNAME_TOO_SHORT": "Käyttäjätunnus on liian lyhyt: sen on oltava vähintään 3 merkkiä pitkä.",
|
||||
"USERNAME_TOO_LONG": "Käyttäjätunnus on liian pitkä: se voi olla enintään 40 merkkiä.",
|
||||
"USERNAME_INVALID_EMAIL": "Virheellinen sähköpostiosoite.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Käyttäjätunnus on virheellinen, voi sisältää vain kirjaimia tai numeroita.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Holvisi ei ole ajan tasalla. Synkronoi holvisi ja yritä uudelleen.",
|
||||
"INTERNAL_SERVER_ERROR": "Sisäinen palvelinvirhe.",
|
||||
"VAULT_ERROR": "Paikallinen holvi ei ole ajan tasalla. Synkronoi holvisi päivittämällä sivu ja yritä uudelleen."
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"or": "tai",
|
||||
"new": "Uusi",
|
||||
"cancel": "Peruuta",
|
||||
"search": "Etsi",
|
||||
"vaultLocked": "AliasVault on lukittu.",
|
||||
"creatingNewAlias": "Luodaan uutta aliasta...",
|
||||
"noMatchesFound": "Hakutuloksia ei löytynyt",
|
||||
"searchVault": "Etsi holvi...",
|
||||
"serviceName": "Palvelun nimi",
|
||||
"email": "Sähköposti",
|
||||
"username": "Käyttäjänimi",
|
||||
"password": "Salasana",
|
||||
"enterServiceName": "Syötä palvelun nimi",
|
||||
"enterEmailAddress": "Syötä sähköpostiosoite",
|
||||
"enterUsername": "Syötä käyttäjänimi",
|
||||
"hideFor1Hour": "Piilota 1 tunniksi (nykyinen sivusto)",
|
||||
"hidePermanently": "Piilota pysyvästi (nykyinen sivu)",
|
||||
"createRandomAlias": "Luo sattumanvarainen alias",
|
||||
"createUsernamePassword": "Luo käyttäjänimi/salasana",
|
||||
"randomAlias": "Sattumanvarainen alias",
|
||||
"usernamePassword": "Käyttäjänimi/Salasana",
|
||||
"createAndSaveAlias": "Luo ja tallenna alias",
|
||||
"createAndSaveCredential": "Luo ja tallenna käyttäjätunnus",
|
||||
"randomIdentityDescription": "Luo satunnainen identiteetti, jolla on satunnainen sähköpostiosoite, johon on pääsy AliasVaultissa.",
|
||||
"randomIdentityDescriptionDropdown": "Satunnainen identiteetti satunnaisella sähköpostiosoitteella",
|
||||
"manualCredentialDescription": "Määritä oma sähköpostiosoitteesi ja käyttäjänimesi.",
|
||||
"manualCredentialDescriptionDropdown": "Manuaalinen käyttäjänimi ja salasana",
|
||||
"failedToCreateIdentity": "Henkilöllisyyden luonti epäonnistui. Yritä uudelleen.",
|
||||
"enterEmailAndOrUsername": "Syötä sähköpostiosoite ja/tai käyttäjänimi",
|
||||
"autofillWithAliasVault": "Automaattinen täyttö AliasVaultilla",
|
||||
"generateRandomPassword": "Luo sattumanvarainen salasana (kopioi leikepöydälle)",
|
||||
"generateNewPassword": "Luo uusi salasana",
|
||||
"togglePasswordVisibility": "Vaihda salasanan näkyvyyttä",
|
||||
"passwordCopiedToClipboard": "Salasana kopioitu leikepöydälle",
|
||||
"enterEmailAndOrUsernameError": "Syötä sähköpostiosoite ja/tai käyttäjänimi",
|
||||
"openAliasVaultToUpgrade": "Avaa AliasVault päivittääksesi",
|
||||
"vaultUpgradeRequired": "Holvin päivitys vaaditaan.",
|
||||
"dismissPopup": "Hylkää ponnahdusikkuna"
|
||||
},
|
||||
"credentials": {
|
||||
"title": "Käyttäjätunnukset",
|
||||
"addCredential": "Lisää käyttäjätunnus",
|
||||
"editCredential": "Muokkaa käyttäjätunnusta",
|
||||
"deleteCredential": "Poista käyttäjätunnus",
|
||||
"credentialDetails": "Käyttäjätunnuksen tiedot",
|
||||
"serviceName": "Palvelun nimi",
|
||||
"serviceNamePlaceholder": "esim. Gmail, Facebook, Pankki",
|
||||
"website": "Verkkosivusto",
|
||||
"websitePlaceholder": "https://esimerkki.fi",
|
||||
"username": "Käyttäjänimi",
|
||||
"usernamePlaceholder": "Syötä käyttäjänimi",
|
||||
"password": "Salasana",
|
||||
"passwordPlaceholder": "Syötä salasana",
|
||||
"generatePassword": "Luo salasana",
|
||||
"copyPassword": "Kopioi salasana",
|
||||
"showPassword": "Näytä salasana",
|
||||
"hidePassword": "Piilota salasana",
|
||||
"notes": "Muistiinpanot",
|
||||
"notesPlaceholder": "Muut huomautukset...",
|
||||
"totp": "Kaksivaiheinen tunnistautuminen",
|
||||
"totpCode": "TOTP koodi",
|
||||
"copyTotp": "Kopioi TOTP-koodi",
|
||||
"totpSecret": "TOTP Salaus",
|
||||
"totpSecretPlaceholder": "Syötä TOTP salainen avain",
|
||||
"noCredentials": "Käyttäjätunnuksia ei löytynyt",
|
||||
"noCredentialsDescription": "Lisää ensimmäinen käyttäjätunnuksesi aloittaaksesi",
|
||||
"searchPlaceholder": "Etsi käyttäjätunnuksia...",
|
||||
"welcomeTitle": "Tervetuloa AliasVaultiin!",
|
||||
"welcomeDescription": "Käyttääksesi AliasVault-selainlaajennusta: Siirry sivustolle ja käytä AliasVaultin automaattisen täytön ponnahdusikkunaa luodaksesi uuden käyttäjätunnuksen.",
|
||||
"createdAt": "Luotu",
|
||||
"updatedAt": "Viimeksi päivitetty",
|
||||
"autofill": "Automaattinen täyttö",
|
||||
"fillForm": "Täytä lomake",
|
||||
"deleteConfirm": "Oletko varma, että haluat poistaa tämän käyttäjätunnuksen?",
|
||||
"saveSuccess": "Käyttäjätunnus tallennettu onnistuneesti.",
|
||||
"tags": "Tunnisteet",
|
||||
"addTag": "Lisää tunniste",
|
||||
"removeTag": "Poista tunniste",
|
||||
"folder": "Kansio",
|
||||
"selectFolder": "Valitse kansio",
|
||||
"createFolder": "Luo kansio",
|
||||
"saveCredential": "Tallenna käyttäjätunnus",
|
||||
"deleteCredentialTitle": "Poista käyttäjätunnus",
|
||||
"deleteCredentialConfirm": "Oletko varma, että haluat poistaa tämän tunnuksen? Tätä toimintoa ei voi perua.",
|
||||
"randomAlias": "Sattumanvarainen Alias",
|
||||
"manual": "Käyttöopas",
|
||||
"service": "Palvelu",
|
||||
"serviceUrl": "Palvelun URL-osoite",
|
||||
"loginCredentials": "Sisäänkirjautumistiedot",
|
||||
"generateRandomUsername": "Luo sattumanvarainen käyttäjätunnus",
|
||||
"generateRandomPassword": "Luo sattumanvarainen salasana",
|
||||
"changePasswordComplexity": "Muuta salasanan monimutkaisuutta",
|
||||
"passwordLength": "Salasanan pituus",
|
||||
"includeLowercase": "Sisällytä pienet kirjaimet",
|
||||
"includeUppercase": "Sisällytä isot kirjaimet",
|
||||
"includeNumbers": "Sisällytä numerot",
|
||||
"includeSpecialChars": "Sisällytä erikoismerkit",
|
||||
"avoidAmbiguousChars": "Vältä epäselviä merkkejä (o, 0 jne.)",
|
||||
"generateNewPreview": "Luo uusi esikatselu",
|
||||
"generateRandomAlias": "Luo sattumanvarainen alias",
|
||||
"clearAliasFields": "Tyhjennä aliaksen kentät",
|
||||
"alias": "Alias",
|
||||
"firstName": "Etunimi",
|
||||
"lastName": "Sukunimi",
|
||||
"nickName": "Lempinimi",
|
||||
"gender": "Sukupuoli",
|
||||
"birthDate": "Syntymäpäivä",
|
||||
"birthDatePlaceholder": "VVVV-KK-PP.",
|
||||
"metadata": "Metatiedot",
|
||||
"validation": {
|
||||
"required": "Tämä kenttä on pakollinen.",
|
||||
"serviceNameRequired": "Palvelun nimi on pakollinen",
|
||||
"invalidEmail": "Virheellinen sähköpostiosoitteen muoto",
|
||||
"invalidDateFormat": "Päivämäärän on oltava muodossa VVVV-KK-PP."
|
||||
},
|
||||
"privateEmailTitle": "Yksityinen sähköposti",
|
||||
"privateEmailAliasVaultServer": "AliasVault-palvelin",
|
||||
"privateEmailDescription": "E2E salattu, täysin yksityinen.",
|
||||
"publicEmailTitle": "Julkiset väliaikaisen sähköpostiosoitteen tarjoajat",
|
||||
"publicEmailDescription": "Anonyymi mutta rajoitettu yksityisyys. Käytettävissä kaikille, jotka tuntevat osoitteen.",
|
||||
"useDomainChooser": "Käytä verkkotunnuksen valintaa",
|
||||
"enterCustomDomain": "Anna oma verkkotunnus",
|
||||
"enterFullEmail": "Syötä täysi sähköpostiosoite",
|
||||
"enterEmailPrefix": "Syötä sähköpostin etuliite"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Sähköpostit",
|
||||
"deleteEmailTitle": "Poista sähköposti",
|
||||
"deleteEmailConfirm": "Oletko varma, että haluat poistaa tämän kuvan pysyvästi?",
|
||||
"from": "Lähettäjä",
|
||||
"to": "Vastaanottaja",
|
||||
"date": "Päivämäärä",
|
||||
"emailContent": "Sähköpostin sisältö",
|
||||
"attachments": "Liitteet",
|
||||
"emailNotFound": "Sähköpostia ei löytynyt",
|
||||
"noEmails": "Sähköposteja ei löytynyt",
|
||||
"noEmailsDescription": "Et ole vielä vastaanottanut sähköposteja yksityisissä sähköpostiosoitteissasi. Kun saat uuden sähköpostiviestin, se näkyy täällä.",
|
||||
"dateFormat": {
|
||||
"justNow": "juuri nyt",
|
||||
"minutesAgo_single": "{{count}} min sitten",
|
||||
"minutesAgo_plural": "{{count}} minuuttia sitten",
|
||||
"hoursAgo_single": "{{count}} h sitten",
|
||||
"hoursAgo_plural": "{{count}} tuntia sitten",
|
||||
"yesterday": "eilen"
|
||||
},
|
||||
"errors": {
|
||||
"emailLoadError": "Sähköpostien lataamisessa tapahtui virhe. Yritä myöhemmin uudelleen.",
|
||||
"emailUnexpectedError": "Odottamaton virhe sähköpostien latauksen aikana. Yritä myöhemmin uudelleen."
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "Nykyinen valittu sähköpostiosoite on jo käytössä. Ole hyvä ja vaihda sähköpostiosoite muokkaamalla tätä tunnusta.",
|
||||
"CLAIM_DOES_NOT_EXIST": "Tapahtui virhe ladattaessa sähköposteja. Yritä muokata ja tallentaa tunnistetiedot synkronoidaksesi tietokannan, ja yritä sitten uudelleen."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Asetukset",
|
||||
"serverUrl": "Palvelimen URL-osoite",
|
||||
"language": "Kieli",
|
||||
"autofillEnabled": "Ota automaattitäyttö käyttöön",
|
||||
"version": "Versio",
|
||||
"openInNewWindow": "Avaa uudessa ikkunassa",
|
||||
"openWebApp": "Avaa verkkosovellus",
|
||||
"loggedIn": "Kirjautuneena",
|
||||
"logout": "Kirjaudu ulos",
|
||||
"globalSettings": "Yleiset asetukset",
|
||||
"autofillPopup": "Automaattisen täytön ponnahdusikkuna",
|
||||
"activeOnAllSites": "Aktiivinen kaikilla sivustoilla (paitsi jos pois päältä alla)",
|
||||
"disabledOnAllSites": "Poistettu käytöstä kaikilla sivustoilla",
|
||||
"enabled": "Käytössä",
|
||||
"disabled": "Pois käytöstä",
|
||||
"rightClickContextMenu": "Oikea-klikkauksen kontekstivalikko",
|
||||
"autofillMatching": "Autofill osuma",
|
||||
"autofillMatchingMode": "Autofill osumat käytössä",
|
||||
"autofillMatchingModeDescription": "Määrittää mitkä käyttäjätunnukset katsotaan osumaksi ja näytetään automaattisen täytön ponnahdusikkunan ehdotuksina tietylle sivustolle.",
|
||||
"autofillMatchingDefault": "URL + alitoimialue + nimi jokerimerkki",
|
||||
"autofillMatchingUrlSubdomain": "URL + alitoimialue",
|
||||
"autofillMatchingUrlExact": "Tarkka URL-verkkotunnus vain",
|
||||
"siteSpecificSettings": "Sivukohtaiset asetukset",
|
||||
"autofillPopupOn": "Automaattisen täytön ponnahdusikkuna päällä: ",
|
||||
"enabledForThisSite": "Käytössä tällä sivustolla",
|
||||
"disabledForThisSite": "Ei käytössä tällä sivustolla",
|
||||
"temporarilyDisabledUntil": "Tilapäisesti pois päältä ",
|
||||
"resetAllSiteSettings": "Nollaa kaikki sivustokohtaiset asetukset",
|
||||
"appearance": "Ulkoasu",
|
||||
"theme": "Teema",
|
||||
"useDefault": "Käytä oletusta",
|
||||
"light": "Vaalea",
|
||||
"dark": "Tumma",
|
||||
"keyboardShortcuts": "Pikanäppäimet",
|
||||
"configureKeyboardShortcuts": "Määritä pikanäppäimet",
|
||||
"configure": "Määritä",
|
||||
"security": "Tietoturva",
|
||||
"clipboardClearTimeout": "Tyhjennä leikepöytä kopioinnin jälkeen",
|
||||
"clipboardClearTimeoutDescription": "Tyhjennä leikepöytä automaattisesti arkaluonteisten tietojen kopioinnin jälkeen",
|
||||
"clipboardClearDisabled": "Älä tyhjennä koskaan",
|
||||
"clipboardClear5Seconds": "Tyhjennä 5 sekunnin jälkeen",
|
||||
"clipboardClear10Seconds": "Tyhjennä 10 sekunnin jälkeen",
|
||||
"clipboardClear15Seconds": "Tyhjennä 15 sekunnin jälkeen",
|
||||
"autoLockTimeout": "Automaattisen lukituksen aikakatkaisu",
|
||||
"autoLockTimeoutDescription": "Lukitse holvi automaattisesti käyttämättä jäämisen jälkeen",
|
||||
"autoLockTimeoutHelp": "Holvi lukittuu vain määritellyn käyttöajan jälkeen (ei automaattisen täytön käyttöä tai laajennuksen ponnahdusikkunaa auki). Holvi lukittuu aina, kun selain on suljettu, tästä asetuksesta riippumatta.",
|
||||
"autoLockNever": "Ei koskaan",
|
||||
"autoLock15Seconds": "15 sekuntia",
|
||||
"autoLock1Minute": "1 minuutti",
|
||||
"autoLock5Minutes": "5 minuuttia",
|
||||
"autoLock15Minutes": "15 minuuttia",
|
||||
"autoLock30Minutes": "30 minuuttia",
|
||||
"autoLock1Hour": "1 tunti",
|
||||
"autoLock4Hours": "4 tuntia",
|
||||
"autoLock8Hours": "8 tuntia",
|
||||
"autoLock24Hours": "24 tuntia",
|
||||
"versionPrefix": "Versio",
|
||||
"preferences": "Määritykset",
|
||||
"autofillSettings": "Automaatisen täytön asetukset",
|
||||
"clipboardSettings": "Leikepöydän asetukset",
|
||||
"contextMenuSettings": "Sisältövalikon asetukset",
|
||||
"contextMenu": "Sisältövalikko",
|
||||
"contextMenuEnabled": "Sisältövalikko käytössä",
|
||||
"contextMenuDisabled": "Sisältövalikko pois käytöstä",
|
||||
"contextMenuDescription": "Napsauta syöttökenttiä hiiren kakkospainikkeella päästäksesi käsiksi AliasVaultin valintoihin",
|
||||
"selectLanguage": "Valitse kieli",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL-osoite vaaditaan",
|
||||
"apiUrlInvalid": "Anna kelvollinen API URL-osoite",
|
||||
"clientUrlRequired": "Asiakkaan URL-osoite vaaditaan",
|
||||
"clientUrlInvalid": "Anna kelvollinen asiakkaan URL-osoite"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "Päivitä holvi",
|
||||
"subtitle": "AliasVault on päivitetty ja holvisi on päivitettävä. Tämän pitäisi kestää vain muutama sekunti.",
|
||||
"versionInformation": "Versiotiedot",
|
||||
"yourVault": "Sinun holvisi:",
|
||||
"newVersion": "Uusi versio:",
|
||||
"upgrade": "Päivitä Holvi",
|
||||
"upgrading": "Päivitetään...",
|
||||
"logout": "Kirjaudu ulos",
|
||||
"whatsNew": "Mitä uutta?",
|
||||
"whatsNewDescription": "Päivitys on tarpeen, jotta voidaan tukea seuraavia muutoksia:",
|
||||
"noDescriptionAvailable": "Kuvausta ei ole saatavilla tälle versiolle.",
|
||||
"okay": "Ok",
|
||||
"status": {
|
||||
"preparingUpgrade": "Valmistellaan päivityksiä...",
|
||||
"vaultAlreadyUpToDate": "Holvi on jo ajan tasalla",
|
||||
"startingDatabaseTransaction": "Aloitetaan tietokannan siirtoa...",
|
||||
"applyingDatabaseMigrations": "Toteutetaan tietokannan siirtoja...",
|
||||
"applyingMigration": "Siirretään tietoja: {{current}} / {{total}}...",
|
||||
"committingChanges": "Suoritetaan muutoksia..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Virhe",
|
||||
"unableToGetVersionInfo": "Versiotietoja ei löytynyt. Yritä uudelleen.",
|
||||
"selfHostedServer": "Itsehallinnoitu palvelin",
|
||||
"selfHostedWarning": "Jos käytät itsehallintoitua palvelina, varmista myös että päivität itsehallinnoidun palvelimesi, jos muutoin kirjautuminen web-asiakkaan kautta lakkaa toimimasta.",
|
||||
"cancel": "Peruuta",
|
||||
"continueUpgrade": "Jatka päivitystä",
|
||||
"upgradeFailed": "Päivitys epäonnistui",
|
||||
"failedToApplyMigration": "Tietojen siirto epäonnistui {{current}} / {{total}} ",
|
||||
"unknownErrorDuringUpgrade": "Päivityksen aikana tapahtui tuntematon virhe. Yritä uudelleen."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,375 +1,393 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "Log in to AliasVault",
|
||||
"username": "Username or email",
|
||||
"usernamePlaceholder": "name / name@company.com",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"rememberMe": "Remember me",
|
||||
"loginButton": "Login",
|
||||
"noAccount": "No account yet?",
|
||||
"createVault": "Create new vault",
|
||||
"twoFactorTitle": "Please enter the authentication code from your authenticator app.",
|
||||
"authCode": "Authentication Code",
|
||||
"authCodePlaceholder": "Enter 6-digit code",
|
||||
"verify": "Verify",
|
||||
"cancel": "Cancel",
|
||||
"twoFactorNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.",
|
||||
"masterPassword": "Master Password",
|
||||
"unlockVault": "Unlock Vault",
|
||||
"unlockTitle": "Unlock Your Vault",
|
||||
"unlockDescription": "Enter your master password to unlock your vault.",
|
||||
"logout": "Logout",
|
||||
"logoutConfirm": "Are you sure you want to logout?",
|
||||
"sessionExpired": "Your session has expired. Please log in again.",
|
||||
"unlockSuccess": "Vault unlocked successfully!",
|
||||
"unlockSuccessTitle": "Your vault is successfully unlocked",
|
||||
"unlockSuccessDescription": "You can now use autofill in login forms in your browser.",
|
||||
"closePopup": "Close this popup",
|
||||
"browseVault": "Browse vault contents",
|
||||
"connectingTo": "Connecting to",
|
||||
"switchAccounts": "Switch accounts?",
|
||||
"loggedIn": "Logged in",
|
||||
"loginTitle": "Se connecter à AliasVault",
|
||||
"username": "Nom d'utilisateur ou email",
|
||||
"usernamePlaceholder": "nom / nom@entreprise.com",
|
||||
"password": "Mot de passe",
|
||||
"passwordPlaceholder": "Saisissez votre mot de passe",
|
||||
"rememberMe": "Se souvenir de moi",
|
||||
"loginButton": "Se connecter",
|
||||
"noAccount": "Pas de compte?",
|
||||
"createVault": "Créer un nouveau coffre",
|
||||
"twoFactorTitle": "Veuillez entrer le code d'authentification de votre application d'authentification.",
|
||||
"authCode": "Code d'authentification",
|
||||
"authCodePlaceholder": "Saisissez le code à 6 chiffres",
|
||||
"verify": "Vérifier",
|
||||
"cancel": "Annuler",
|
||||
"twoFactorNote": "Remarque : si vous n'avez pas accès à votre appareil d'authentification, vous pouvez réinitialiser votre authentification à double facteur avec un code de récupération en vous connectant via le site web.",
|
||||
"masterPassword": "Mot de passe principal",
|
||||
"unlockVault": "Déverrouiller le coffre",
|
||||
"unlockTitle": "Déverrouiller votre coffre",
|
||||
"unlockDescription": "Entrez votre mot de passe principal pour déverrouiller votre coffre-fort.",
|
||||
"logout": "Se déconnecter",
|
||||
"logoutConfirm": "Êtes-vous sûr de vouloir vous déconnecter ?",
|
||||
"sessionExpired": "Votre session a expiré. Veuillez vous reconnecter.",
|
||||
"unlockSuccess": "Parcourir le contenu du coffre",
|
||||
"unlockSuccessTitle": "Votre coffre a été déverrouillé avec succès",
|
||||
"unlockSuccessDescription": "Vous pouvez maintenant utiliser le remplissage automatique des formulaires de connexion dans votre navigateur.",
|
||||
"closePopup": "Fermer cette popup",
|
||||
"browseVault": "Parcourir le contenu du coffre",
|
||||
"connectingTo": "Connexion à",
|
||||
"switchAccounts": "Changer de compte ?",
|
||||
"loggedIn": "Connecté(e)",
|
||||
"errors": {
|
||||
"invalidCode": "Please enter a valid 6-digit authentication code.",
|
||||
"serverError": "Could not reach AliasVault server. Please try again later or contact support if the problem persists.",
|
||||
"noToken": "Login failed -- no token returned",
|
||||
"migrationError": "An error occurred while checking for pending migrations.",
|
||||
"wrongPassword": "Incorrect password. Please try again.",
|
||||
"accountLocked": "Account temporarily locked due to too many failed attempts.",
|
||||
"networkError": "Network error. Please check your connection and try again.",
|
||||
"loginDataMissing": "Login session expired. Please try again."
|
||||
"invalidCode": "Veuillez entrer un code d'authentification valide à 6 chiffres.",
|
||||
"serverError": "Impossible d'accéder au serveur AliasVault. Veuillez réessayer plus tard ou contacter le support si le problème persiste.",
|
||||
"noToken": "Échec de la connexion -- aucun jeton retourné",
|
||||
"migrationError": "Une erreur s'est produite lors de la vérification des migrations en attente.",
|
||||
"wrongPassword": "Mot de passe incorrect, veuillez réessayer.",
|
||||
"accountLocked": "Compte temporairement verrouillé en raison d'un trop grand nombre de tentatives échouées.",
|
||||
"networkError": "Erreur réseau. Vérifiez votre connexion et réessayez.",
|
||||
"loginDataMissing": "La session a expiré. Veuillez réessayer."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "Credentials",
|
||||
"credentials": "Identifiants",
|
||||
"emails": "Emails",
|
||||
"settings": "Settings"
|
||||
"settings": "Réglages"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"cancel": "Cancel",
|
||||
"use": "Use",
|
||||
"delete": "Delete",
|
||||
"close": "Close",
|
||||
"copied": "Copied!",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"loading": "Chargement...",
|
||||
"error": "Erreur",
|
||||
"success": "Succès",
|
||||
"cancel": "Annuler",
|
||||
"use": "Utiliser",
|
||||
"delete": "Supprimer",
|
||||
"close": "Fermer",
|
||||
"copied": "Copié !",
|
||||
"openInNewWindow": "Ouvrir dans une nouvelle fenêtre",
|
||||
"language": "Language",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"loadingEmails": "Loading emails...",
|
||||
"loadingTotpCodes": "Loading TOTP codes...",
|
||||
"attachments": "Attachments",
|
||||
"loadingAttachments": "Loading attachments...",
|
||||
"settings": "Settings",
|
||||
"recentEmails": "Recent emails",
|
||||
"loginCredentials": "Login credentials",
|
||||
"twoFactorAuthentication": "Two-factor authentication",
|
||||
"enabled": "Activé",
|
||||
"disabled": "Désactivé",
|
||||
"showPassword": "Afficher le mot de passe",
|
||||
"hidePassword": "Cacher le mot de passe",
|
||||
"copyToClipboard": "Copier dans le presse-papiers",
|
||||
"loadingEmails": "Chargement des emails...",
|
||||
"loadingTotpCodes": "Chargement des codes TOTP...",
|
||||
"attachments": "Pièces jointes",
|
||||
"loadingAttachments": "Chargement des pièces jointes...",
|
||||
"settings": "Réglages",
|
||||
"recentEmails": "Emails récents",
|
||||
"loginCredentials": "Identifiants de connexion",
|
||||
"twoFactorAuthentication": "Authentification à double facteur",
|
||||
"alias": "Alias",
|
||||
"notes": "Notes",
|
||||
"fullName": "Full Name",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"birthDate": "Birth Date",
|
||||
"nickname": "Nickname",
|
||||
"fullName": "Nom complet",
|
||||
"firstName": "Prénom",
|
||||
"lastName": "Nom",
|
||||
"birthDate": "Date de naissance",
|
||||
"nickname": "Surnom",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"syncingVault": "Syncing vault",
|
||||
"savingChangesToVault": "Saving changes to vault",
|
||||
"uploadingVaultToServer": "Uploading vault to server",
|
||||
"checkingVaultUpdates": "Checking for vault updates",
|
||||
"syncingUpdatedVault": "Syncing updated vault",
|
||||
"executingOperation": "Executing operation...",
|
||||
"username": "Nom d'utilisateur",
|
||||
"password": "Mot de passe",
|
||||
"syncingVault": "Synchronisation du coffre",
|
||||
"savingChangesToVault": "Enregistrement des modifications dans le coffre",
|
||||
"uploadingVaultToServer": "Envoi du coffre vers le serveur",
|
||||
"checkingVaultUpdates": "Vérification des mises à jour du coffre",
|
||||
"syncingUpdatedVault": "Synchronisation du coffre mis à jour",
|
||||
"executingOperation": "Exécution de l'opération...",
|
||||
"loadMore": "Voir plus",
|
||||
"errors": {
|
||||
"VaultMergeRequired": "Your vault needs to be updated. Please login on the AliasVault website and follow the steps.",
|
||||
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
|
||||
"NoVaultFound": "Your account does not have a vault yet. Please complete the tutorial in the AliasVault web client before using the browser extension.",
|
||||
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
|
||||
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
|
||||
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
|
||||
"unknownError": "An unknown error occurred",
|
||||
"failedToStoreVault": "Failed to store vault",
|
||||
"vaultNotAvailable": "Vault not available",
|
||||
"failedToGetVault": "Failed to get vault",
|
||||
"vaultIsLocked": "Vault is locked",
|
||||
"failedToGetCredentials": "Failed to get credentials",
|
||||
"failedToCreateIdentity": "Failed to create identity",
|
||||
"failedToGetDefaultEmailDomain": "Failed to get default email domain",
|
||||
"failedToGetDefaultIdentitySettings": "Failed to get default identity settings",
|
||||
"failedToGetPasswordSettings": "Failed to get password settings",
|
||||
"failedToUploadVault": "Failed to upload vault",
|
||||
"noDerivedKeyAvailable": "No derived key available for encryption",
|
||||
"failedToUploadVaultToServer": "Failed to upload new vault to server",
|
||||
"noVaultOrDerivedKeyFound": "No vault or derived key found"
|
||||
"VaultOutdated": "Votre coffre est obsolète. Veuillez vous connecter sur le site AliasVault et suivre les étapes.",
|
||||
"serverNotAvailable": "Le serveur d'AliasVault n'est pas disponible. Veuillez réessayer plus tard ou contacter le support si le problème persiste.",
|
||||
"clientVersionNotSupported": "Cette version de l'extension de navigateur AliasVault n'est plus prise en charge par le serveur. Veuillez mettre à jour votre extension de navigateur à la dernière version.",
|
||||
"serverVersionNotSupported": "Le serveur d'AliasVault doit être mis à jour vers une version plus récente afin d'utiliser cette extension de navigateur. Veuillez contacter le support si vous avez besoin d'aide.",
|
||||
"unknownError": "Une erreur inconnue s'est produite",
|
||||
"failedToStoreVault": "Échec du stockage du coffre",
|
||||
"vaultNotAvailable": "Coffre non disponible",
|
||||
"failedToRetrieveData": "Échec de la récupération des données",
|
||||
"vaultIsLocked": "Le coffre est verrouillé",
|
||||
"failedToUploadVault": "Échec du téléchargement du coffre",
|
||||
"passwordChanged": "Votre mot de passe a changé depuis la dernière fois que vous vous êtes connecté. Veuillez vous reconnecter pour des raisons de sécurité."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",
|
||||
"ACCOUNT_LOCKED": "Account temporarily locked due to too many failed attempts. Please try again later.",
|
||||
"ACCOUNT_BLOCKED": "Your account has been disabled. If you believe this is a mistake, please contact support.",
|
||||
"USER_NOT_FOUND": "Invalid username or password. Please try again.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Invalid authenticator code. Please try again.",
|
||||
"INVALID_RECOVERY_CODE": "Invalid recovery code. Please try again.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Refresh token is required.",
|
||||
"USER_NOT_FOUND_IN_TOKEN": "User not found in token.",
|
||||
"USER_NOT_FOUND_IN_DATABASE": "User not found in database.",
|
||||
"INVALID_REFRESH_TOKEN": "Invalid refresh token.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token revoked successfully.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "New account registration is currently disabled on this server. Please contact the administrator.",
|
||||
"USERNAME_REQUIRED": "Username is required.",
|
||||
"USERNAME_ALREADY_IN_USE": "Username is already in use.",
|
||||
"USERNAME_AVAILABLE": "Username is available.",
|
||||
"USERNAME_MISMATCH": "Username does not match the current user.",
|
||||
"PASSWORD_MISMATCH": "The provided password does not match your current password.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Account successfully deleted.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Username cannot be empty or whitespace.",
|
||||
"USERNAME_TOO_SHORT": "Username too short: must be at least 3 characters long.",
|
||||
"USERNAME_TOO_LONG": "Username too long: cannot be longer than 40 characters.",
|
||||
"USERNAME_INVALID_EMAIL": "Invalid email address.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Username is invalid, can only contain letters or digits.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Your vault is not up-to-date. Please synchronize your vault and try again.",
|
||||
"INTERNAL_SERVER_ERROR": "Internal server error.",
|
||||
"VAULT_ERROR": "The local vault is not up-to-date. Please synchronize your vault by refreshing the page and try again."
|
||||
"UNKNOWN_ERROR": "Une erreur inconnue s'est produite. Merci de réessayer.",
|
||||
"ACCOUNT_LOCKED": "Compte temporairement verrouillé en raison d'un trop grand nombre de tentatives infructueuses. Veuillez réessayer plus tard.",
|
||||
"ACCOUNT_BLOCKED": "Votre compte a été désactivé. Si vous pensez que c'est une erreur, veuillez contacter le support.",
|
||||
"USER_NOT_FOUND": "Nom d'utilisateur ou mot de passe invalide. Veuillez réessayer.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Code d'authentification invalide. Veuillez réessayer.",
|
||||
"INVALID_RECOVERY_CODE": "Code de récupération invalide. Veuillez réessayer.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Un jeton d'actualisation est requis.",
|
||||
"INVALID_REFRESH_TOKEN": "Jeton d'actualisation invalide.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Le jeton d'actualisation a été révoqué.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "L'enregistrement d'un nouveau compte est actuellement désactivé sur ce serveur. Veuillez contacter l'administrateur.",
|
||||
"USERNAME_REQUIRED": "Nom d’utilisateur requis.",
|
||||
"USERNAME_ALREADY_IN_USE": "Nom d'utilisateur déjà utilisé.",
|
||||
"USERNAME_AVAILABLE": "Ce nom d'utilisateur est disponible.",
|
||||
"USERNAME_MISMATCH": "Le nom d'utilisateur ne correspond pas à l'utilisateur actuel.",
|
||||
"PASSWORD_MISMATCH": "Le mot de passe indiqué ne correspond pas à votre mot de passe actuel.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Compte supprimé avec succès.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Le nom d'utilisateur ne peut pas être vide ou contenir un espace.",
|
||||
"USERNAME_TOO_SHORT": "Le nom d'utilisateur est trop court : il doit comporter au moins 3 caractères.",
|
||||
"USERNAME_TOO_LONG": "Le nom d'utilisateur est trop long : il ne peut pas contenir plus de 40 caractères.",
|
||||
"USERNAME_INVALID_EMAIL": "Adresse e-mail invalide.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Le nom d'utilisateur n'est pas valide, il ne peut contenir que des lettres ou des chiffres.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Votre coffre n'est pas à jour. Veuillez synchroniser votre coffre et réessayer.",
|
||||
"INTERNAL_SERVER_ERROR": "Erreur interne du serveur.",
|
||||
"VAULT_ERROR": "Le coffre local n'est pas à jour. Veuillez synchroniser votre coffre en rafraîchissant la page et réessayez."
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"or": "or",
|
||||
"new": "New",
|
||||
"cancel": "Cancel",
|
||||
"search": "Search",
|
||||
"vaultLocked": "AliasVault is locked.",
|
||||
"creatingNewAlias": "Creating new alias...",
|
||||
"noMatchesFound": "No matches found",
|
||||
"searchVault": "Search vault...",
|
||||
"serviceName": "Service name",
|
||||
"or": "ou",
|
||||
"new": "Nouveautés",
|
||||
"cancel": "Annuler",
|
||||
"search": "Rechercher",
|
||||
"vaultLocked": "AliasVault est verrouillé.",
|
||||
"creatingNewAlias": "Création de nouveaux alias...",
|
||||
"noMatchesFound": "Aucun résultat trouvé",
|
||||
"searchVault": "Rechercher dans le coffre...",
|
||||
"serviceName": "Nom du service",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"enterServiceName": "Enter service name",
|
||||
"enterEmailAddress": "Enter email address",
|
||||
"enterUsername": "Enter username",
|
||||
"hideFor1Hour": "Hide for 1 hour (current site)",
|
||||
"hidePermanently": "Hide permanently (current site)",
|
||||
"createRandomAlias": "Create random alias",
|
||||
"createUsernamePassword": "Create username/password",
|
||||
"randomAlias": "Random alias",
|
||||
"usernamePassword": "Username/password",
|
||||
"createAndSaveAlias": "Create and save alias",
|
||||
"createAndSaveCredential": "Create and save credential",
|
||||
"randomIdentityDescription": "Generate a random identity with a random email address accessible in AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Random identity with random email",
|
||||
"manualCredentialDescription": "Specify your own email address and username.",
|
||||
"manualCredentialDescriptionDropdown": "Manual username and password",
|
||||
"failedToCreateIdentity": "Failed to create identity. Please try again.",
|
||||
"enterEmailAndOrUsername": "Enter email and/or username",
|
||||
"autofillWithAliasVault": "Autofill with AliasVault",
|
||||
"generateRandomPassword": "Generate random password (copy to clipboard)",
|
||||
"generateNewPassword": "Generate new password",
|
||||
"togglePasswordVisibility": "Toggle password visibility",
|
||||
"passwordCopiedToClipboard": "Password copied to clipboard",
|
||||
"enterEmailAndOrUsernameError": "Enter email and/or username",
|
||||
"openAliasVaultToUpgrade": "Open AliasVault to upgrade",
|
||||
"vaultUpgradeRequired": "Vault upgrade required.",
|
||||
"dismissPopup": "Dismiss popup"
|
||||
"username": "Nom d'utilisateur",
|
||||
"password": "Mot de passe",
|
||||
"enterServiceName": "Entrez le nom du service",
|
||||
"enterEmailAddress": "Entrer l'adresse email",
|
||||
"enterUsername": "Entrez le nom d'utilisateur",
|
||||
"hideFor1Hour": "Cacher pendant 1 heure (site actuel)",
|
||||
"hidePermanently": "Masquer définitivement (site actuel)",
|
||||
"createRandomAlias": "Créer un alias aléatoire",
|
||||
"createUsernamePassword": "Créer un nom d'utilisateur/mot de passe",
|
||||
"randomAlias": "Alias aléatoire",
|
||||
"usernamePassword": "Nom d’utilisateur / mot de passe",
|
||||
"createAndSaveAlias": "Créer et enregistrer l'alias",
|
||||
"createAndSaveCredential": "Créer et enregistrer les identifiants",
|
||||
"randomIdentityDescription": "Générer une identité aléatoire avec une adresse email aléatoire accessible dans AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Identité aléatoire avec email aléatoire",
|
||||
"manualCredentialDescription": "Spécifiez votre propre adresse email et nom d'utilisateur.",
|
||||
"manualCredentialDescriptionDropdown": "Identifiant et mot de passe manuels",
|
||||
"failedToCreateIdentity": "Échec de la création de l'identité. Veuillez réessayer.",
|
||||
"enterEmailAndOrUsername": "Entrez l'adresse email et/ou le nom d'utilisateur",
|
||||
"autofillWithAliasVault": "Remplissage automatique avec AliasVault",
|
||||
"generateRandomPassword": "Générer un mot de passe aléatoire (copier dans le presse-papier)",
|
||||
"generateNewPassword": "Générer un nouveau mot de passe",
|
||||
"togglePasswordVisibility": "Afficher ou masquer le mot de passe",
|
||||
"passwordCopiedToClipboard": "Mot de passe copié dans le presse-papiers",
|
||||
"enterEmailAndOrUsernameError": "Entrez l'adresse email et/ou le nom d'utilisateur",
|
||||
"openAliasVaultToUpgrade": "Ouvrez AliasVault pour améliorer",
|
||||
"vaultUpgradeRequired": "Mise à niveau du coffre requise.",
|
||||
"dismissPopup": "Fermer"
|
||||
},
|
||||
"credentials": {
|
||||
"title": "Credentials",
|
||||
"addCredential": "Add Credential",
|
||||
"editCredential": "Edit Credential",
|
||||
"deleteCredential": "Delete Credential",
|
||||
"credentialDetails": "Credential Details",
|
||||
"serviceName": "Service Name",
|
||||
"serviceNamePlaceholder": "e.g., Gmail, Facebook, Bank",
|
||||
"website": "Website",
|
||||
"title": "Identifiants",
|
||||
"addCredential": "Ajouter des identifiants",
|
||||
"editCredential": "Modifier les identifiants",
|
||||
"deleteCredential": "Supprimer les identifiants",
|
||||
"credentialDetails": "Informations sur les identifiants",
|
||||
"serviceName": "Nom du service",
|
||||
"serviceNamePlaceholder": "ex: Gmail, Facebook, Banque",
|
||||
"website": "Site Internet",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "Enter username",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"generatePassword": "Generate Password",
|
||||
"copyPassword": "Copy Password",
|
||||
"showPassword": "Show Password",
|
||||
"hidePassword": "Hide Password",
|
||||
"username": "Nom d'utilisateur",
|
||||
"usernamePlaceholder": "Entrez le nom d'utilisateur",
|
||||
"password": "Mot de passe",
|
||||
"passwordPlaceholder": "Saisir le mot de passe",
|
||||
"generatePassword": "Générer le mot de passe",
|
||||
"copyPassword": "Copier le mot de passe",
|
||||
"showPassword": "Afficher le mot de passe",
|
||||
"hidePassword": "Masquer le mot de passe",
|
||||
"notes": "Notes",
|
||||
"notesPlaceholder": "Additional notes...",
|
||||
"totp": "Two-Factor Authentication",
|
||||
"totpCode": "TOTP Code",
|
||||
"copyTotp": "Copy TOTP",
|
||||
"totpSecret": "TOTP Secret",
|
||||
"totpSecretPlaceholder": "Enter TOTP secret key",
|
||||
"noCredentials": "No credentials found",
|
||||
"noCredentialsDescription": "Add your first credential to get started",
|
||||
"searchCredentials": "Search credentials...",
|
||||
"searchPlaceholder": "Search credentials...",
|
||||
"welcomeTitle": "Welcome to AliasVault!",
|
||||
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
|
||||
"lastUsed": "Last used",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Fill Form",
|
||||
"copyUsername": "Copy Username",
|
||||
"openWebsite": "Open Website",
|
||||
"favorite": "Favorite",
|
||||
"unfavorite": "Remove from Favorites",
|
||||
"deleteConfirm": "Are you sure you want to delete this credential?",
|
||||
"deleteSuccess": "Credential deleted successfully",
|
||||
"saveSuccess": "Credential saved successfully",
|
||||
"copySuccess": "Copied to clipboard",
|
||||
"tags": "Tags",
|
||||
"addTag": "Add Tag",
|
||||
"removeTag": "Remove Tag",
|
||||
"folder": "Folder",
|
||||
"selectFolder": "Select Folder",
|
||||
"createFolder": "Create Folder",
|
||||
"saveCredential": "Save credential",
|
||||
"deleteCredentialTitle": "Delete Credential",
|
||||
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
|
||||
"randomAlias": "Random Alias",
|
||||
"manual": "Manual",
|
||||
"notesPlaceholder": "Notes supplémentaires...",
|
||||
"totp": "Authentification à deux facteurs",
|
||||
"totpCode": "Mot de passe à usage unique",
|
||||
"copyTotp": "Copier le mot de passe à usage unique",
|
||||
"totpSecret": "Mot de passe à usage unique secret",
|
||||
"totpSecretPlaceholder": "Entrez le mot de passe à usage unique",
|
||||
"noCredentials": "Aucun identifiant trouvé",
|
||||
"noCredentialsDescription": "Ajoutez vos premiers identifiants pour commencer",
|
||||
"searchPlaceholder": "Rechercher des identifiants...",
|
||||
"welcomeTitle": "Bienvenue dans AliasVault !",
|
||||
"welcomeDescription": "Pour utiliser l'extension de navigateur AliasVault : accédez à un site web et utilisez la fenêtre de saisie automatique AliasVault pour créer un nouvel identifiant.",
|
||||
"createdAt": "Créé",
|
||||
"updatedAt": "Dernière mise à jour",
|
||||
"autofill": "Remplissage automatique",
|
||||
"fillForm": "Remplir le formulaire",
|
||||
"deleteConfirm": "Êtes-vous sûr de vouloir supprimer cet identifiant ?",
|
||||
"saveSuccess": "Identifiants enregistrés avec succès",
|
||||
"tags": "Mots-clés",
|
||||
"addTag": "Ajouter un mot-clé",
|
||||
"removeTag": "Supprimer un mot-clé",
|
||||
"folder": "Dossier",
|
||||
"selectFolder": "Sélectionner un dossier",
|
||||
"createFolder": "Nouveau dossier",
|
||||
"saveCredential": "Enregistrer les identifiants",
|
||||
"deleteCredentialTitle": "Supprimer les identifiants",
|
||||
"deleteCredentialConfirm": "Êtes-vous sûr de vouloir supprimer ces identifiants ? Cette action est irréversible.",
|
||||
"randomAlias": "Alias aléatoire",
|
||||
"manual": "Manuel",
|
||||
"service": "Service",
|
||||
"serviceUrl": "Service URL",
|
||||
"loginCredentials": "Login Credentials",
|
||||
"generateRandomUsername": "Generate random username",
|
||||
"generateRandomPassword": "Generate random password",
|
||||
"changePasswordComplexity": "Change password complexity",
|
||||
"passwordLength": "Password length",
|
||||
"includeLowercase": "Include lowercase letters",
|
||||
"includeUppercase": "Include uppercase letters",
|
||||
"includeNumbers": "Include numbers",
|
||||
"includeSpecialChars": "Include special characters",
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"serviceUrl": "URL de service",
|
||||
"loginCredentials": "Identifiants de connexion",
|
||||
"generateRandomUsername": "Générer un nom d'utilisateur aléatoire",
|
||||
"generateRandomPassword": "Générer un mot de passe aléatoire",
|
||||
"changePasswordComplexity": "Changer la complexité du mot de passe",
|
||||
"passwordLength": "Longueur du mot de passe",
|
||||
"includeLowercase": "Inclure les lettres minuscules",
|
||||
"includeUppercase": "Inclure les lettres majuscules",
|
||||
"includeNumbers": "Inclure des chiffres",
|
||||
"includeSpecialChars": "Inclure des caractères spéciaux",
|
||||
"avoidAmbiguousChars": "Éviter les caractères ambigus (o, 0, etc.)",
|
||||
"generateNewPreview": "Générer un nouvel aperçu",
|
||||
"generateRandomAlias": "Créer un alias aléatoire",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"nickName": "Nick Name",
|
||||
"gender": "Gender",
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"errors": {
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"saveError": "Failed to save credential",
|
||||
"loadError": "Failed to load credentials",
|
||||
"deleteError": "Failed to delete credential",
|
||||
"copyError": "Failed to copy to clipboard"
|
||||
},
|
||||
"firstName": "Prénom",
|
||||
"lastName": "Nom",
|
||||
"nickName": "Surnom",
|
||||
"gender": "Genre",
|
||||
"birthDate": "Date de naissance",
|
||||
"birthDatePlaceholder": "AAAA-MM-JJ",
|
||||
"metadata": "Métadonnées",
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidUrl": "Invalid URL format",
|
||||
"invalidEmail": "Invalid email format",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
|
||||
}
|
||||
"required": "Ce champ est obligatoire",
|
||||
"serviceNameRequired": "Le nom du service est requis",
|
||||
"invalidEmail": "Format de courriel non valide",
|
||||
"invalidDateFormat": "La date doit être au format AAAA-MM-JJ"
|
||||
},
|
||||
"privateEmailTitle": "Email privé",
|
||||
"privateEmailAliasVaultServer": "Serveur AliasVault",
|
||||
"privateEmailDescription": "Chiffrement bout en bout, entièrement privé.",
|
||||
"publicEmailTitle": "Fournisseurs d'email public temporaires",
|
||||
"publicEmailDescription": "Anonyme mais confidentialité limitée. Le contenu de l'email est lisible par toute personne qui connaît l'adresse.",
|
||||
"useDomainChooser": "Utiliser le sélecteur de domaine",
|
||||
"enterCustomDomain": "Entrez le domaine personnalisé",
|
||||
"enterFullEmail": "Entrez l'adresse email complète",
|
||||
"enterEmailPrefix": "Entrez le préfixe de l'email"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
"deleteEmailTitle": "Delete Email",
|
||||
"deleteEmailConfirm": "Are you sure you want to permanently delete this email?",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"deleteEmailTitle": "Supprimer l'email",
|
||||
"deleteEmailConfirm": "Êtes-vous sûr de vouloir supprimer définitivement cet email ?",
|
||||
"from": "De",
|
||||
"to": "À",
|
||||
"date": "Date",
|
||||
"emailContent": "Email content",
|
||||
"attachments": "Attachments",
|
||||
"emailNotFound": "Email not found",
|
||||
"noEmails": "No emails found",
|
||||
"noEmailsDescription": "You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.",
|
||||
"emailContent": "Contenu de l'email",
|
||||
"attachments": "Pièces jointes",
|
||||
"emailNotFound": "Email introuvable",
|
||||
"noEmails": "Aucun email trouvé",
|
||||
"noEmailsDescription": "Vous n'avez pas encore reçu d'emails dans vos adresses email privées. Quand vous recevez un nouvel email, il apparaîtra ici.",
|
||||
"dateFormat": {
|
||||
"justNow": "just now",
|
||||
"minutesAgo_single": "{{count}} min ago",
|
||||
"minutesAgo_plural": "{{count}} mins ago",
|
||||
"hoursAgo_single": "{{count}} hr ago",
|
||||
"hoursAgo_plural": "{{count}} hrs ago",
|
||||
"yesterday": "yesterday"
|
||||
"justNow": "maintenant",
|
||||
"minutesAgo_single": "Il y a {{count}} minute",
|
||||
"minutesAgo_plural": "Il y a {{count}} minutes",
|
||||
"hoursAgo_single": "Il y a {{count}} heure",
|
||||
"hoursAgo_plural": "Il y a {{count}} heures",
|
||||
"yesterday": "hier"
|
||||
},
|
||||
"errors": {
|
||||
"emailLoadError": "An error occurred while loading emails. Please try again later.",
|
||||
"emailUnexpectedError": "An unexpected error occurred while loading emails. Please try again later."
|
||||
"emailLoadError": "Une erreur s'est produite lors du chargement des emails. Veuillez réessayer plus tard.",
|
||||
"emailUnexpectedError": "Une erreur inattendue s'est produite lors du chargement des emails. Veuillez réessayer plus tard."
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "The current chosen email address is already in use. Please change the email address by editing this credential.",
|
||||
"CLAIM_DOES_NOT_EXIST": "An error occurred while trying to load the emails. Please try to edit and save the credential entry to synchronize the database, then try again."
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "L'adresse email actuelle est déjà utilisée. Veuillez modifier l'adresse email en modifiant cet identifiant.",
|
||||
"CLAIM_DOES_NOT_EXIST": "Une erreur s'est produite en essayant de charger les emails. Veuillez essayer de modifier et enregistrer les informations d'identification pour synchroniser la base de données, puis réessayez."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"serverUrl": "Server URL",
|
||||
"language": "Language",
|
||||
"autofillEnabled": "Enable Autofill",
|
||||
"title": "Réglages",
|
||||
"serverUrl": "URL du serveur",
|
||||
"language": "Langue",
|
||||
"autofillEnabled": "Activer le remplissage automatique",
|
||||
"version": "Version",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"openWebApp": "Open web app",
|
||||
"loggedIn": "Logged in",
|
||||
"logout": "Logout",
|
||||
"globalSettings": "Global Settings",
|
||||
"autofillPopup": "Autofill popup",
|
||||
"activeOnAllSites": "Active on all sites (unless disabled below)",
|
||||
"disabledOnAllSites": "Disabled on all sites",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"rightClickContextMenu": "Right-click context menu",
|
||||
"siteSpecificSettings": "Site-Specific Settings",
|
||||
"autofillPopupOn": "Autofill popup on: ",
|
||||
"enabledForThisSite": "Enabled for this site",
|
||||
"disabledForThisSite": "Disabled for this site",
|
||||
"temporarilyDisabledUntil": "Temporarily disabled until ",
|
||||
"resetAllSiteSettings": "Reset all site-specific settings",
|
||||
"appearance": "Appearance",
|
||||
"theme": "Theme",
|
||||
"useDefault": "Use default",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||
"configureKeyboardShortcuts": "Configure keyboard shortcuts",
|
||||
"configure": "Configure",
|
||||
"openInNewWindow": "Ouvrir dans une nouvelle fenêtre",
|
||||
"openWebApp": "Ouvrir l’application web",
|
||||
"loggedIn": "Connecté(e)",
|
||||
"logout": "Se déconnecter",
|
||||
"globalSettings": "Paramètres généraux",
|
||||
"autofillPopup": "Remplissage automatique de la popup",
|
||||
"activeOnAllSites": "Activé sur tous les sites (sauf si désactivé ci-dessous)",
|
||||
"disabledOnAllSites": "Désactivé sur tous les sites",
|
||||
"enabled": "Activé",
|
||||
"disabled": "Désactivé",
|
||||
"rightClickContextMenu": "Clic-droit sur le menu contextuel",
|
||||
"autofillMatching": "Correspondance de remplissage automatique",
|
||||
"autofillMatchingMode": "Remplir automatiquement le mode correspondant",
|
||||
"autofillMatchingModeDescription": "Détermine quels identifiants sont considérés comme une correspondance et sont affichés comme des suggestions dans la fenêtre de saisie automatique pour un site web donné.",
|
||||
"autofillMatchingDefault": "URL + sous-domaine + nom générique",
|
||||
"autofillMatchingUrlSubdomain": "URL + sous-domaine",
|
||||
"autofillMatchingUrlExact": "Domaine d'URL exact uniquement",
|
||||
"siteSpecificSettings": "Paramètres spécifiques au site",
|
||||
"autofillPopupOn": "Popup de saisie automatique sur: ",
|
||||
"enabledForThisSite": "Activé pour ce site",
|
||||
"disabledForThisSite": "Désactivé pour ce site",
|
||||
"temporarilyDisabledUntil": "Temporairement désactivé jusqu'au ",
|
||||
"resetAllSiteSettings": "Réinitialiser tous les paramètres spécifiques au site",
|
||||
"appearance": "Apparence",
|
||||
"theme": "Thème",
|
||||
"useDefault": "Utiliser par défaut",
|
||||
"light": "Clair",
|
||||
"dark": "Sombre",
|
||||
"keyboardShortcuts": "Raccourcis clavier",
|
||||
"configureKeyboardShortcuts": "Configurer les raccourcis clavier",
|
||||
"configure": "Configurer",
|
||||
"security": "Sécurité",
|
||||
"clipboardClearTimeout": "Effacer le presse-papiers après copie",
|
||||
"clipboardClearTimeoutDescription": "Effacer automatiquement le presse-papiers après copie des données sensibles",
|
||||
"clipboardClearDisabled": "Ne jamais effacer",
|
||||
"clipboardClear5Seconds": "Effacer après 5 secondes",
|
||||
"clipboardClear10Seconds": "Effacer après 10 secondes",
|
||||
"clipboardClear15Seconds": "Effacer après 15 secondes",
|
||||
"autoLockTimeout": "Délai de verrouillage automatique",
|
||||
"autoLockTimeoutDescription": "Verrouiller automatiquement le coffre après une période d'inactivité",
|
||||
"autoLockTimeoutHelp": "Le coffre ne se verrouille qu'après la période d'inactivité spécifiée (aucune fenêtre pop-up de saisie automatique ou d'extension). Le coffre sera toujours verrouillé lorsque le navigateur sera fermé, quel que soit ce paramètre.",
|
||||
"autoLockNever": "Jamais",
|
||||
"autoLock15Seconds": "15 secondes",
|
||||
"autoLock1Minute": "1 minute",
|
||||
"autoLock5Minutes": "5 minutes",
|
||||
"autoLock15Minutes": "15 minutes",
|
||||
"autoLock30Minutes": "30 minutes",
|
||||
"autoLock1Hour": "1 heure",
|
||||
"autoLock4Hours": "4 heures",
|
||||
"autoLock8Hours": "8 heures",
|
||||
"autoLock24Hours": "24 heures",
|
||||
"versionPrefix": "Version ",
|
||||
"preferences": "Préférences",
|
||||
"autofillSettings": "Paramètres du remplissage automatique",
|
||||
"clipboardSettings": "Paramètres du presse-papiers",
|
||||
"contextMenuSettings": "Paramètres du menu contextuel",
|
||||
"contextMenu": "Menu contextuel",
|
||||
"contextMenuEnabled": "Le menu contextuel est activé",
|
||||
"contextMenuDisabled": "Le menu contextuel est désactivé",
|
||||
"contextMenuDescription": "Faites un clic droit sur les champs de saisie pour accéder aux options d'AliasVault",
|
||||
"selectLanguage": "Sélectionner une langue",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL is required",
|
||||
"apiUrlInvalid": "Please enter a valid API URL",
|
||||
"clientUrlRequired": "Client URL is required",
|
||||
"clientUrlInvalid": "Please enter a valid client URL"
|
||||
"apiUrlRequired": "L'URL de l'API est requise",
|
||||
"apiUrlInvalid": "Veuillez entrer une URL d'API valide",
|
||||
"clientUrlRequired": "L'URL du client est requise",
|
||||
"clientUrlInvalid": "Veuillez entrer une URL de client valide"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "Upgrade Vault",
|
||||
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
|
||||
"versionInformation": "Version Information",
|
||||
"yourVault": "Your vault:",
|
||||
"newVersion": "New version:",
|
||||
"upgrade": "Upgrade Vault",
|
||||
"upgrading": "Upgrading...",
|
||||
"logout": "Logout",
|
||||
"whatsNew": "What's New",
|
||||
"whatsNewDescription": "An upgrade is required to support the following changes:",
|
||||
"noDescriptionAvailable": "No description available for this version.",
|
||||
"title": "Mettre à niveau le coffre",
|
||||
"subtitle": "AliasVault a mis à jour et votre coffre doit être mis à niveau. Cela ne devrait prendre que quelques secondes.",
|
||||
"versionInformation": "Informations de version",
|
||||
"yourVault": "Votre coffre :",
|
||||
"newVersion": "Nouvelle version :",
|
||||
"upgrade": "Mettre le coffre à niveau",
|
||||
"upgrading": "Mise à niveau...",
|
||||
"logout": "Se déconnecter",
|
||||
"whatsNew": "Nouveautés",
|
||||
"whatsNewDescription": "Une mise à niveau est nécessaire pour prendre en charge les modifications suivantes :",
|
||||
"noDescriptionAvailable": "Aucune description disponible pour cette version.",
|
||||
"okay": "Ok",
|
||||
"status": {
|
||||
"preparingUpgrade": "Preparing upgrade...",
|
||||
"vaultAlreadyUpToDate": "Vault is already up to date",
|
||||
"startingDatabaseTransaction": "Starting database transaction...",
|
||||
"applyingDatabaseMigrations": "Applying database migrations...",
|
||||
"applyingMigration": "Applying migration {{current}} of {{total}}...",
|
||||
"committingChanges": "Committing changes..."
|
||||
"preparingUpgrade": "Préparation de la mise à niveau...",
|
||||
"vaultAlreadyUpToDate": "Le coffre est déjà à jour",
|
||||
"startingDatabaseTransaction": "Démarrage de la transaction de la base de données...",
|
||||
"applyingDatabaseMigrations": "Application des migrations de base de données...",
|
||||
"applyingMigration": "Application de la migration {{current}} sur {{total}}...",
|
||||
"committingChanges": "Validation des modifications..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Error",
|
||||
"unableToGetVersionInfo": "Unable to get version information. Please try again.",
|
||||
"selfHostedServer": "Self-Hosted Server",
|
||||
"selfHostedWarning": "If you're using a self-hosted server, make sure to also update your self-hosted instance as otherwise logging in to the web client will stop working.",
|
||||
"cancel": "Cancel",
|
||||
"continueUpgrade": "Continue Upgrade",
|
||||
"upgradeFailed": "Upgrade Failed",
|
||||
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
|
||||
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
|
||||
"error": "Erreur",
|
||||
"unableToGetVersionInfo": "Impossible d'obtenir les informations de version. Veuillez réessayer.",
|
||||
"selfHostedServer": "Serveur auto-hébergé",
|
||||
"selfHostedWarning": "Si vous utilisez un serveur auto-hébergé, assurez-vous également de mettre à jour votre instance auto-hébergée, sinon la connexion au client web cessera de fonctionner.",
|
||||
"cancel": "Annuler",
|
||||
"continueUpgrade": "Continuer la mise à jour",
|
||||
"upgradeFailed": "Échec de la mise à niveau",
|
||||
"failedToApplyMigration": "Impossible d'appliquer la migration ({{current}} sur {{total}})",
|
||||
"unknownErrorDuringUpgrade": "Une erreur inconnue s'est produite pendant la mise à niveau. Veuillez réessayer."
|
||||
}
|
||||
}
|
||||
}
|
||||
393
apps/browser-extension/src/i18n/locales/he.json
Normal file
393
apps/browser-extension/src/i18n/locales/he.json
Normal file
@@ -0,0 +1,393 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "כניסה ל־AliasVault",
|
||||
"username": "שם משתמש או דוא״ל",
|
||||
"usernamePlaceholder": "שם / name@company.com",
|
||||
"password": "סיסמה",
|
||||
"passwordPlaceholder": "נא למלא את הסיסמה שלך",
|
||||
"rememberMe": "לזכור אותי",
|
||||
"loginButton": "כניסה",
|
||||
"noAccount": "אין לך חשבון עדיין?",
|
||||
"createVault": "יצירת כספת חדשה",
|
||||
"twoFactorTitle": "נא למלא את קוד האימות מיישומון המאמת שלך.",
|
||||
"authCode": "קוד אימות",
|
||||
"authCodePlaceholder": "נא למלא קוד באורך 6 ספרות",
|
||||
"verify": "אימות",
|
||||
"cancel": "ביטול",
|
||||
"twoFactorNote": "לתשומת ליבך: אם אין לך גישה להתקן המאמת (authenticator) שלך, אפשר לאפס אימות דו־שלבי עם קוד שחזור על ידי כניסה דרך האתר.",
|
||||
"masterPassword": "סיסמת על",
|
||||
"unlockVault": "שחרור נעילת כספת",
|
||||
"unlockTitle": "שחרור נעילת הכספת שלך",
|
||||
"unlockDescription": "נא למלא את סיסמת העל שלך כדי לשחרר את הכספת שלך.",
|
||||
"logout": "יציאה",
|
||||
"logoutConfirm": "לצאת?",
|
||||
"sessionExpired": "תוקף ההפעלה שלך פג. נא להיכנס מחדש.",
|
||||
"unlockSuccess": "נעילת הכספת שוחררה בהצלחה!",
|
||||
"unlockSuccessTitle": "נעילת הכספת שלך נפתחה בהצלחה",
|
||||
"unlockSuccessDescription": "מעתה ניתן להשתמש בהשלמה אוטומטית בטופסי כניסה בדפדפן שלך.",
|
||||
"closePopup": "סגירת החלונית הצצה הזאת",
|
||||
"browseVault": "עיון בתוכן הכספת",
|
||||
"connectingTo": "מתבצעת התחברות אל",
|
||||
"switchAccounts": "להחליף חשבונות?",
|
||||
"loggedIn": "נכנסת",
|
||||
"errors": {
|
||||
"invalidCode": "נא למלא קוד אימות באורך 6 ספרות.",
|
||||
"serverError": "לא ניתן ליצור קשר עם השרת של AliasVault. נא לנסות שוב מאוחר יותר או ליצור קשר עם התמיכה אם הבעיה נשנית.",
|
||||
"noToken": "הכניסה נכשלה - לא הוחזר אסימון",
|
||||
"migrationError": "אירעה שגיאה בעת בדיקה לאיתור הסבות ממתינות.",
|
||||
"wrongPassword": "סיסמה שגויה. נא לנסות שוב.",
|
||||
"accountLocked": "החשבון נעול זמנית עקב ריבוי ניסיונות כושלים.",
|
||||
"networkError": "שגיאת רשת. נא לבדוק את החיבור ולנסות שוב.",
|
||||
"loginDataMissing": "תוקף ההפעלה שלך פג. נא לנסות שוב."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "פרטי גישה",
|
||||
"emails": "הודעות דוא״ל",
|
||||
"settings": "הגדרות"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "בטעינה…",
|
||||
"error": "שגיאה",
|
||||
"success": "הצליח",
|
||||
"cancel": "ביטול",
|
||||
"use": "להשתמש",
|
||||
"delete": "מחיקה",
|
||||
"close": "סגירה",
|
||||
"copied": "הועתק!",
|
||||
"openInNewWindow": "פתיחה בחלון חדש",
|
||||
"language": "שפה",
|
||||
"enabled": "פעיל",
|
||||
"disabled": "כבוי",
|
||||
"showPassword": "הצגת סיסמה",
|
||||
"hidePassword": "הסתרת סיסמה",
|
||||
"copyToClipboard": "העתקה ללוח הגזירים",
|
||||
"loadingEmails": "הודעות הדוא״ל נטענות…",
|
||||
"loadingTotpCodes": "הקודים החד־פעמיים הזמניים נטענים…",
|
||||
"attachments": "צרופות",
|
||||
"loadingAttachments": "הצרופות נטענות…",
|
||||
"settings": "הגדרות",
|
||||
"recentEmails": "הודעות דוא״ל אחרונות",
|
||||
"loginCredentials": "פרטי הגישה",
|
||||
"twoFactorAuthentication": "אימות דו־שלבי",
|
||||
"alias": "כינוי",
|
||||
"notes": "הערות",
|
||||
"fullName": "שם מלא",
|
||||
"firstName": "שם פרטי",
|
||||
"lastName": "שם משפחה",
|
||||
"birthDate": "תאריך לידה",
|
||||
"nickname": "כינוי",
|
||||
"email": "דוא״ל",
|
||||
"username": "שם משתמש",
|
||||
"password": "סיסמה",
|
||||
"syncingVault": "הכספת מסתנכרת",
|
||||
"savingChangesToVault": "השינוים לכספת נשמרים",
|
||||
"uploadingVaultToServer": "הכספת נשלחת לשרת",
|
||||
"checkingVaultUpdates": "מתבצעת בדיקה לשינויים בכספת",
|
||||
"syncingUpdatedVault": "הכספת העדכנית מסתנכרת",
|
||||
"executingOperation": "הפעולה רצה…",
|
||||
"loadMore": "לטעון עוד",
|
||||
"errors": {
|
||||
"VaultOutdated": "הכספת שלך לא עדכנית. נא להיכנס לאתר AliasVault ולעקוב אחר ההנחיות.",
|
||||
"serverNotAvailable": "שרת ה־AliasVault לא זמין. נא לנסות שוב מאוחר יותר או ליצור קשר עם התמיכה אם הבעיה נשנית.",
|
||||
"clientVersionNotSupported": "הגרסה הזאת של הרחבת הדפדפן של AliasVault לא נתמכת עוד על ידי השרת. נא לעדכן את הרחבת הדפדפן שלך לגרסה העדכנית ביותר.",
|
||||
"serverVersionNotSupported": "יש לעדכן את שרת AliasVault לגרסה חדשה יותר כדי להשתמש בהרחבת הדפדפן הזאת. נא ליצור קשר עם התמיכה לקבלת עזרה.",
|
||||
"unknownError": "אירעה שגיאה לא ידועה",
|
||||
"failedToStoreVault": "אחסון הכספת נכשל",
|
||||
"vaultNotAvailable": "הכספת לא זמינה",
|
||||
"failedToRetrieveData": "משיכת הנתונים נכשלה",
|
||||
"vaultIsLocked": "הכספת נעולה",
|
||||
"failedToUploadVault": "העלאת הכספת נכשלה",
|
||||
"passwordChanged": "הסיסמה שלך השתנתה מאז הפעם האחרונה שנכנסת למערכת. נא להיכנס שוב מטעמי אבטחת מידע."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "אירעה שגיאה לא ידועה, נא לנסות שוב.",
|
||||
"ACCOUNT_LOCKED": "החשבון נעול זמנית עקב ריבוי ניסיונות כושלים. נא לנסות שוב מאוחר יותר.",
|
||||
"ACCOUNT_BLOCKED": "החשבון שלך הושבת. אם לדעתך מדובר בטעות, נא ליצור קשר עם התמיכה.",
|
||||
"USER_NOT_FOUND": "שם המשתמש או הסיסמה שגויים. נא לנסות שוב.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "קוד מאמת שגוי. נא לנסות שוב.",
|
||||
"INVALID_RECOVERY_CODE": "קוד שחזור שגוי. נא לנסות שוב.",
|
||||
"REFRESH_TOKEN_REQUIRED": "אסימון ריענון חובה.",
|
||||
"INVALID_REFRESH_TOKEN": "אסימון ריענון שגוי.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "אסימון הריענון נשלל בהצלחה.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "רישום חשבון חדש מושבת כרגע בשרת הזה. נא ליצור קשר עם ההנהלה.",
|
||||
"USERNAME_REQUIRED": "שם משתמש חובה.",
|
||||
"USERNAME_ALREADY_IN_USE": "שם המשתמש כבר תפוס.",
|
||||
"USERNAME_AVAILABLE": "שם המשתמש פנוי.",
|
||||
"USERNAME_MISMATCH": "שם המשתמש לא מתאים למשתמש הנוכחי.",
|
||||
"PASSWORD_MISMATCH": "הסיסמה שסופקה לא תואמת לסיסמה הנוכחית שלך.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "החשבון נמחק בהצלחה.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "שם המשתמש לא יכול להיות ריק או מורכב מרווחים בלבד.",
|
||||
"USERNAME_TOO_SHORT": "שם המשתמש קצר מדי: חייב להיות באורך 3 תווים לפחות.",
|
||||
"USERNAME_TOO_LONG": "שם המשתמש ארוך מדי: לא יכול להיות ארוך מ־40 תווים.",
|
||||
"USERNAME_INVALID_EMAIL": "כתובת דוא״ל שגויה.",
|
||||
"USERNAME_INVALID_CHARACTERS": "שם המשתמש שגוי, יכול להכיל רק תווים או ספרות.",
|
||||
"VAULT_NOT_UP_TO_DATE": "הכספת שלך אינה עדכנית. נא לסנכרן את הכספת שלך ולנסות שוב.",
|
||||
"INTERNAL_SERVER_ERROR": "שגיאת שרת פנימית.",
|
||||
"VAULT_ERROR": "הכספת המקומית אינה עדכנית. נא לסנכרן את הכספת שלך על ידי ריענון העמוד ולנסות שוב."
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"or": "או",
|
||||
"new": "חדש",
|
||||
"cancel": "ביטול",
|
||||
"search": "חיפוש",
|
||||
"vaultLocked": "AliasVault נעול.",
|
||||
"creatingNewAlias": "נוצר כינוי חדש...",
|
||||
"noMatchesFound": "לא נמצאו תוצאות",
|
||||
"searchVault": "חיפוש בכספת…",
|
||||
"serviceName": "שם השירות",
|
||||
"email": "דוא״ל",
|
||||
"username": "שם משתמש",
|
||||
"password": "סיסמה",
|
||||
"enterServiceName": "נא למלא את שם השירות",
|
||||
"enterEmailAddress": "נא למלא כתובת דוא״ל",
|
||||
"enterUsername": "נא למלא שם משתמש",
|
||||
"hideFor1Hour": "הסתרה למשך שעה (האתר הנוכחי)",
|
||||
"hidePermanently": "הסתרה לצמיתות (האתר הנוכחי)",
|
||||
"createRandomAlias": "יצירת כינוי אקראי",
|
||||
"createUsernamePassword": "יצירת שם משתמש/סיסמה",
|
||||
"randomAlias": "כינוי אקראי",
|
||||
"usernamePassword": "שם משתמש/סיסמה",
|
||||
"createAndSaveAlias": "יצירה ושמירה של כינוי",
|
||||
"createAndSaveCredential": "יצירה ושמירה של פרטי גישה",
|
||||
"randomIdentityDescription": "יצירת זהות אקראית עם כתובת דוא״ל אקראית שנגישה דרך AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "זהות אקראיות עם דוא״ל אקראי",
|
||||
"manualCredentialDescription": "נא לציין כתובת דוא״ל ושם משתמש משלך.",
|
||||
"manualCredentialDescriptionDropdown": "שם משתמש וסיסמה ידניים",
|
||||
"failedToCreateIdentity": "יצירת הזהות נכשלה. נא לנסות שוב.",
|
||||
"enterEmailAndOrUsername": "נא למלא דוא״ל ו/או שם משתמש",
|
||||
"autofillWithAliasVault": "השלמה אוטומטית עם AliasVault",
|
||||
"generateRandomPassword": "יצירת סיסמה אקראית (העתקה ללוח הגזירים)",
|
||||
"generateNewPassword": "יצירת סיסמה חדשה",
|
||||
"togglePasswordVisibility": "הצגת/הסתרת סיסמה",
|
||||
"passwordCopiedToClipboard": "הסיסמה הועתקה ללוח הגזירים",
|
||||
"enterEmailAndOrUsernameError": "נא למלא דוא״ל ו/או שם משתמש",
|
||||
"openAliasVaultToUpgrade": "יש לפתוח את AliasVault כדי לשדרג",
|
||||
"vaultUpgradeRequired": "יש לשדרג את הכספת.",
|
||||
"dismissPopup": "התעלמות מחלונית"
|
||||
},
|
||||
"credentials": {
|
||||
"title": "פרטי גישה",
|
||||
"addCredential": "הוספת פרטי גישה",
|
||||
"editCredential": "עריכת פרטי גישה",
|
||||
"deleteCredential": "מחיקת פרטי גישה",
|
||||
"credentialDetails": "פירוט פרטי גישה",
|
||||
"serviceName": "שם השירות",
|
||||
"serviceNamePlaceholder": "למשל: ג׳ימייל, פייסבוק, בנק",
|
||||
"website": "אתר",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "שם משתמש",
|
||||
"usernamePlaceholder": "נא למלא שם משתמש",
|
||||
"password": "סיסמה",
|
||||
"passwordPlaceholder": "נא למלא סיסמה",
|
||||
"generatePassword": "יצירת סיסמה",
|
||||
"copyPassword": "העתקת סיסמה",
|
||||
"showPassword": "הצגת סיסמה",
|
||||
"hidePassword": "הסתרת סיסמה",
|
||||
"notes": "הערות",
|
||||
"notesPlaceholder": "הערות נוספות…",
|
||||
"totp": "אימות דו־שלבי",
|
||||
"totpCode": "קוד חד־פעמי זמני",
|
||||
"copyTotp": "העתקת קוד חד־פעמי זמני",
|
||||
"totpSecret": "סוג סיסמה חד־פעמית זמנית",
|
||||
"totpSecretPlaceholder": "נא למלא מפתח סודי לסיסמה חד־פעמית זמנית",
|
||||
"noCredentials": "לא נמצאו פרטי גישה",
|
||||
"noCredentialsDescription": "יש להוסיף את פרטי הגישה הראשונים שלך כדי להתחיל",
|
||||
"searchPlaceholder": "חיפוש פרטי גישה…",
|
||||
"welcomeTitle": "ברוך בואך ל־AliasVault!",
|
||||
"welcomeDescription": "כדי להשתמש בהרחבת הדפדפן של AliasVault: יש לנווט לאתר ולהשתמש בחלונית ההשלמה האוטומטית של AliasVault כדי ליצור פרטי גישה חדשים.",
|
||||
"createdAt": "יצירה",
|
||||
"updatedAt": "עדכון אחרון",
|
||||
"autofill": "השלמה אוטומטית",
|
||||
"fillForm": "מילוי טופס",
|
||||
"deleteConfirm": "למחוק את פרטי הגישה האלה?",
|
||||
"saveSuccess": "פרטי הגישה נשמרו בהצלחה",
|
||||
"tags": "תגיות",
|
||||
"addTag": "הוספת תגית",
|
||||
"removeTag": "הסרת תגית",
|
||||
"folder": "תיקייה",
|
||||
"selectFolder": "בחירת תיקייה",
|
||||
"createFolder": "יצירת תיקייה",
|
||||
"saveCredential": "שמירת פרטי גישה",
|
||||
"deleteCredentialTitle": "מחיקת פרטי גישה",
|
||||
"deleteCredentialConfirm": "למחוק את פרטי הגישה? זאת פעולה בלתי הפיכה.",
|
||||
"randomAlias": "כינוי אקראי",
|
||||
"manual": "ידני",
|
||||
"service": "שירות",
|
||||
"serviceUrl": "כתובת השירות",
|
||||
"loginCredentials": "פרטי הגישה",
|
||||
"generateRandomUsername": "יצירת שם משתמש אקראי",
|
||||
"generateRandomPassword": "יצירת סיסמה אקראית",
|
||||
"changePasswordComplexity": "החלפת מורכבת הסיסמה",
|
||||
"passwordLength": "אורך הסיסמה",
|
||||
"includeLowercase": "לכלול אותיות קטנות",
|
||||
"includeUppercase": "לכלול אותיות גדולות",
|
||||
"includeNumbers": "לכלול מספרים",
|
||||
"includeSpecialChars": "לכלול תווים מיוחדים",
|
||||
"avoidAmbiguousChars": "עדיף להימנע מאותיות וספרות שדומים זה לזה (o, 0 וכו׳)",
|
||||
"generateNewPreview": "יצירת תצוגה מקדימה חדשה",
|
||||
"generateRandomAlias": "יצירת כינוי אקראי",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "כינוי",
|
||||
"firstName": "שם פרטי",
|
||||
"lastName": "שם משפחה",
|
||||
"nickName": "כינוי",
|
||||
"gender": "מגדר",
|
||||
"birthDate": "תאריך לידה",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "נתוני על",
|
||||
"validation": {
|
||||
"required": "שדה חובה",
|
||||
"serviceNameRequired": "שם השירות חובה",
|
||||
"invalidEmail": "תבנית דוא״ל שגויה",
|
||||
"invalidDateFormat": "התאריך חייב להיות בתבנית YYYY-MM-DD"
|
||||
},
|
||||
"privateEmailTitle": "כתובת דוא״ל פרטית",
|
||||
"privateEmailAliasVaultServer": "שרת AliasVault",
|
||||
"privateEmailDescription": "הצפנה מקצה לקצה, פרטיות מלאה.",
|
||||
"publicEmailTitle": "ספקי תיבת דוא״ל זמנית ציבוריים",
|
||||
"publicEmailDescription": "פרטיות אלמונית אך מוגבלת. תוכן הדוא״ל נגיש לכל מי שיודע את הכתובת.",
|
||||
"useDomainChooser": "להשתמש בבורר שמות התחום",
|
||||
"enterCustomDomain": "נא למלא שם תחום מותאם אישית",
|
||||
"enterFullEmail": "נא למלא כתובת דוא״ל מלאה",
|
||||
"enterEmailPrefix": "נא למלא קידומת דוא״ל"
|
||||
},
|
||||
"emails": {
|
||||
"title": "הודעות דוא״ל",
|
||||
"deleteEmailTitle": "מחיקת הודעת דוא״ל",
|
||||
"deleteEmailConfirm": "למחוק את הודעת הדוא״ל הזאת לצמיתות?",
|
||||
"from": "מאת",
|
||||
"to": "אל",
|
||||
"date": "תאריך",
|
||||
"emailContent": "תוכן הודעת דוא״ל",
|
||||
"attachments": "צרופות",
|
||||
"emailNotFound": "הודעת הדוא״ל לא נמצאה",
|
||||
"noEmails": "לא נמצאו הודעות דוא״ל",
|
||||
"noEmailsDescription": "לא קיבלת הודעות דוא״ל כלשהן לכתובות הדוא״ל הפרטיות שלך עדיין. כשמגיעה הודעה חדשה היא תופיע כאן.",
|
||||
"dateFormat": {
|
||||
"justNow": "ממש הרגע",
|
||||
"minutesAgo_single": "לפני דקה",
|
||||
"minutesAgo_plural": "לפני {{count}} דקות",
|
||||
"hoursAgo_single": "לפני שעה",
|
||||
"hoursAgo_plural": "לפני {{count}} שעות",
|
||||
"yesterday": "אתמול"
|
||||
},
|
||||
"errors": {
|
||||
"emailLoadError": "אירעה שגיאה בטעינת הודעות הדוא״ל. נא לנסות שוב מאוחר יותר.",
|
||||
"emailUnexpectedError": "אירעה שגיאה לא צפויה בטעינת הודעות הדוא״ל. נא לנסות שוב מאוחר יותר."
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "כתובת הדוא״ל שנבחרה תפוסה. נא לשנות את כתובת הדוא״ל על ידי עריכת פרטי הגישה.",
|
||||
"CLAIM_DOES_NOT_EXIST": "אירעה שגיאה בניסיון לטעון את הודעות הדוא״ל. נא לנסות לערוך ולשמור את רשומת פרטי הקשר כדי לסנכרן את מסד הנתונים ואז לנסות שוב."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "הגדרות",
|
||||
"serverUrl": "כתובת שרת",
|
||||
"language": "שפה",
|
||||
"autofillEnabled": "הפעלת השלמה אוטומטית",
|
||||
"version": "גרסה",
|
||||
"openInNewWindow": "פתיחה בחלון חדש",
|
||||
"openWebApp": "פתיחת אתר",
|
||||
"loggedIn": "נכנסת",
|
||||
"logout": "יציאה",
|
||||
"globalSettings": "הגדרות מקיפות",
|
||||
"autofillPopup": "חלונית השלמה אוטומטית",
|
||||
"activeOnAllSites": "פעיל בכל האתרים (למעט אם נכבה להלן)",
|
||||
"disabledOnAllSites": "כבוי בכל האתרים",
|
||||
"enabled": "פעיל",
|
||||
"disabled": "כבוי",
|
||||
"rightClickContextMenu": "תפריט הקשר בלחיצה ימנית",
|
||||
"autofillMatching": "התאמת השלמה אוטומטית",
|
||||
"autofillMatchingMode": "מצב התאמת השלמה אוטומטית",
|
||||
"autofillMatchingModeDescription": "הגדרה אילו פרטי גישה נחשבים תואמים ויופיעו כהצעות בחלונית ההשלמה האוטומטית לאתר מסוים.",
|
||||
"autofillMatchingDefault": "כתובת + שם תת־תחום + תו כל על שם",
|
||||
"autofillMatchingUrlSubdomain": "כתובת + שם תת־תחום",
|
||||
"autofillMatchingUrlExact": "תחום כתובת מדויקת בלבד",
|
||||
"siteSpecificSettings": "הגדרות תואמות אתר",
|
||||
"autofillPopupOn": "חלונית השלמה אוטומטית ב־: ",
|
||||
"enabledForThisSite": "פעיל לאתר הזה",
|
||||
"disabledForThisSite": "כבוי לאתר הזה",
|
||||
"temporarilyDisabledUntil": "כבוי זמנית עד ",
|
||||
"resetAllSiteSettings": "איפוס כל ההגדרות הנקודתיות לאתרים",
|
||||
"appearance": "מראה",
|
||||
"theme": "ערכת עיצוב",
|
||||
"useDefault": "להשתמש בברירת המחדל",
|
||||
"light": "בהירה",
|
||||
"dark": "כהה",
|
||||
"keyboardShortcuts": "קיצורי מקלדת",
|
||||
"configureKeyboardShortcuts": "הגדרת קיצורי מקלדת",
|
||||
"configure": "הגדרה",
|
||||
"security": "אבטחה",
|
||||
"clipboardClearTimeout": "לפנות את לוח הגזירים לאחר העתקה",
|
||||
"clipboardClearTimeoutDescription": "לפנות את לוח הגזירים אוטומטית לאחר העתקת נתונים רגישים",
|
||||
"clipboardClearDisabled": "אף פעם לא לפנות",
|
||||
"clipboardClear5Seconds": "לפנות אחרי 5 שניות",
|
||||
"clipboardClear10Seconds": "לפנות אחרי 10 שניות",
|
||||
"clipboardClear15Seconds": "לפנות אחרי 15 שניות",
|
||||
"autoLockTimeout": "תום המתנה לנעילה אוטומטית",
|
||||
"autoLockTimeoutDescription": "לנעול את הכספת אוטומטית לאחר פרק זמן של חוסר פעילות",
|
||||
"autoLockTimeoutHelp": "הכספת תינעל רק לאחר משך זמן של חוסר פעילות (אין שימוש בהשלמה אוטומטית או פתיחת חלונית הרחבה). הכספת תמיד תינעל עם סגירת הדפדפן, ללא תלות בהגדרה הזאת.",
|
||||
"autoLockNever": "אף פעם",
|
||||
"autoLock15Seconds": "15 שניות",
|
||||
"autoLock1Minute": "דקה",
|
||||
"autoLock5Minutes": "5 דקות",
|
||||
"autoLock15Minutes": "15 דקות",
|
||||
"autoLock30Minutes": "30 דקות",
|
||||
"autoLock1Hour": "שעה",
|
||||
"autoLock4Hours": "4 שעות",
|
||||
"autoLock8Hours": "8 שעות",
|
||||
"autoLock24Hours": "24 שעות",
|
||||
"versionPrefix": "גרסה ",
|
||||
"preferences": "העדפות",
|
||||
"autofillSettings": "הגדרות השלמה אוטומטית",
|
||||
"clipboardSettings": "הגדרות לוח הגזירים",
|
||||
"contextMenuSettings": "הגדרות תפריט הקשר",
|
||||
"contextMenu": "תפריט הקשר",
|
||||
"contextMenuEnabled": "תפריט הקשר פעיל",
|
||||
"contextMenuDisabled": "תפריט הקשר כבוי",
|
||||
"contextMenuDescription": "ניתן ללחוץ על שדה עם הלחצן הימני כדי לגשת לאפשרויות AliasVault",
|
||||
"selectLanguage": "בחירת שפה",
|
||||
"validation": {
|
||||
"apiUrlRequired": "כתובת API חובה",
|
||||
"apiUrlInvalid": "נא למלא כתובת API תקפה",
|
||||
"clientUrlRequired": "כתובת לקוח חובה",
|
||||
"clientUrlInvalid": "נא למלא כתובת לקוח תקפה"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "שדרוג כספת",
|
||||
"subtitle": "AliasVault התעדכן וצריך לשדרג את הכספת שלך. הפעולה הזאת אמורה לארוך מספר שניות.",
|
||||
"versionInformation": "פרטי גרסה",
|
||||
"yourVault": "הכספת שלך:",
|
||||
"newVersion": "גרסה חדשה:",
|
||||
"upgrade": "שדרוג כספת",
|
||||
"upgrading": "משתדרגת…",
|
||||
"logout": "יציאה",
|
||||
"whatsNew": "מה חדש",
|
||||
"whatsNewDescription": "יש לשדרג כדי שתהיה תמיכה בשינויים הבאים:",
|
||||
"noDescriptionAvailable": "אין תיאור זמין לגרסה הזאת.",
|
||||
"okay": "אישור",
|
||||
"status": {
|
||||
"preparingUpgrade": "השדרוג בהכנה…",
|
||||
"vaultAlreadyUpToDate": "הכספת כבר עדכנית",
|
||||
"startingDatabaseTransaction": "הסבת מסד הנתונים מתחילה…",
|
||||
"applyingDatabaseMigrations": "השינויים חלים על מסד הנתונים…",
|
||||
"applyingMigration": "חלה ההסבה {{current}} מתוך {{total}}…",
|
||||
"committingChanges": "השינויים מקובעים…"
|
||||
},
|
||||
"alerts": {
|
||||
"error": "שגיאה",
|
||||
"unableToGetVersionInfo": "לא ניתן לקבל את פרטי הגרסה. נא לנסות שוב מאוחר יותר.",
|
||||
"selfHostedServer": "שרת באירוח עצמי",
|
||||
"selfHostedWarning": "אם מדובר בשרת שמתארח עצמאית, נא לוודא שהעותק שמתארח אצלך גם כן מתעדכן כי אחרת הכניסה לאתר תפסיק לעבוד.",
|
||||
"cancel": "ביטול",
|
||||
"continueUpgrade": "להמשיך בשדרוג",
|
||||
"upgradeFailed": "השדרוג נכשל",
|
||||
"failedToApplyMigration": "החלת ההסבה נכשלה ({{current}} מתוך {{total}})",
|
||||
"unknownErrorDuringUpgrade": "אירעה שגיאה בלתי ידועה במהלך השדרוג. נא לנסות שוב."
|
||||
}
|
||||
}
|
||||
}
|
||||
393
apps/browser-extension/src/i18n/locales/it.json
Normal file
393
apps/browser-extension/src/i18n/locales/it.json
Normal file
@@ -0,0 +1,393 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "Accedi ad AliasVaultriem",
|
||||
"username": "Nome utente o mail",
|
||||
"usernamePlaceholder": "nome / nome@azienda.com",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Inserisci la tua password",
|
||||
"rememberMe": "Ricordati di me",
|
||||
"loginButton": "Accedi",
|
||||
"noAccount": "Non hai ancora un account?",
|
||||
"createVault": "Crea una nuova cassaforte",
|
||||
"twoFactorTitle": "Inserisci il codice di autenticazione dalla tua app di autenticazione.",
|
||||
"authCode": "Codice di Autenticazione",
|
||||
"authCodePlaceholder": "Inserisci il codice a 6 cifre",
|
||||
"verify": "Verifica",
|
||||
"cancel": "Annulla",
|
||||
"twoFactorNote": "Nota: se non hai accesso al tuo dispositivo di autenticazione, puoi reimpostare il tuo 2FA con un codice di recupero accedendo tramite il sito web.",
|
||||
"masterPassword": "Password principale",
|
||||
"unlockVault": "Sblocca Cassaforte",
|
||||
"unlockTitle": "Sblocca la tua cassaforte",
|
||||
"unlockDescription": "Inserisci la tua password principale per sbloccare la tua cassaforte.",
|
||||
"logout": "Disconnetti",
|
||||
"logoutConfirm": "Sei sicuro di volerti disconnettere?",
|
||||
"sessionExpired": "La sessione è scaduta. Effettua di nuovo il login.",
|
||||
"unlockSuccess": "Cassaforte sbloccata con successo!",
|
||||
"unlockSuccessTitle": "La cassaforte è stata sbloccata con successo",
|
||||
"unlockSuccessDescription": "Ora puoi usare l'auto-riempimento nei moduli di accesso nel tuo browser.",
|
||||
"closePopup": "Chiudi questo popup",
|
||||
"browseVault": "Sfoglia i contenuti della cassaforte",
|
||||
"connectingTo": "Connessione a",
|
||||
"switchAccounts": "Cambia account",
|
||||
"loggedIn": "Accesso effettuato",
|
||||
"errors": {
|
||||
"invalidCode": "Inserisci un codice di autenticazione a 6 cifre valido.",
|
||||
"serverError": "Impossibile connettersi al server di AliasVault. Riprova più tardi o contatta il supporto se il problema persiste.",
|
||||
"noToken": "Accesso fallito — nessun token ricevuto",
|
||||
"migrationError": "Si è verificato un errore nel controllo delle migrazioni pendenti.",
|
||||
"wrongPassword": "Password non corretta. Riprova nuovamente.",
|
||||
"accountLocked": "Account temporaneamente bloccato a causa di troppi tentativi falliti.",
|
||||
"networkError": "Errore di rete: Controlla la tua connessione e riprova.",
|
||||
"loginDataMissing": "Sessione di accesso scaduta. Effettua nuovamente l'accesso."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "Credenziali",
|
||||
"emails": "E-Mail",
|
||||
"settings": "Impostazioni"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Caricamento in corso...",
|
||||
"error": "Errore",
|
||||
"success": "Riuscito",
|
||||
"cancel": "Annulla",
|
||||
"use": "Usa",
|
||||
"delete": "Elimina",
|
||||
"close": "Chiudi",
|
||||
"copied": "Copiato!",
|
||||
"openInNewWindow": "Apri in una nuova finestra",
|
||||
"language": "Lingua",
|
||||
"enabled": "Abilitato",
|
||||
"disabled": "Disabilitato",
|
||||
"showPassword": "Mostra password",
|
||||
"hidePassword": "Nascondi password",
|
||||
"copyToClipboard": "Copia negli appunti",
|
||||
"loadingEmails": "Caricamento e-mail in corso...",
|
||||
"loadingTotpCodes": "Caricamento codici TOTP in corso...",
|
||||
"attachments": "Allegati",
|
||||
"loadingAttachments": "Caricamento allegati in corso...",
|
||||
"settings": "Impostazioni",
|
||||
"recentEmails": "E-mail recenti",
|
||||
"loginCredentials": "Credenziali di accesso",
|
||||
"twoFactorAuthentication": "Autenticazione a due fattori",
|
||||
"alias": "Alias",
|
||||
"notes": "Note",
|
||||
"fullName": "Nome completo",
|
||||
"firstName": "Nome",
|
||||
"lastName": "Cognome",
|
||||
"birthDate": "Data di nascita",
|
||||
"nickname": "Soprannome",
|
||||
"email": "E-mail",
|
||||
"username": "Nome utente",
|
||||
"password": "Password",
|
||||
"syncingVault": "Sincronizzazione cassaforte",
|
||||
"savingChangesToVault": "Salvataggio modifiche cassaforte",
|
||||
"uploadingVaultToServer": "Caricamento cassaforte sul server",
|
||||
"checkingVaultUpdates": "Controllo aggiornamenti cassaforte",
|
||||
"syncingUpdatedVault": "Sincronizzazione cassaforte aggiornata",
|
||||
"executingOperation": "Esecuzione operazione...",
|
||||
"loadMore": "Carica altro",
|
||||
"errors": {
|
||||
"VaultOutdated": "La tua cassaforte è obsoleta. Per favore accedi al sito di AliasVault e segui le istruzioni.",
|
||||
"serverNotAvailable": "Il server di AliasVault non è disponibile. Riprova più tardi o contatta il supporto se il problema persiste.",
|
||||
"clientVersionNotSupported": "Questa versione dell'estensione del browser AliasVault non è più supportata dal server. Aggiorna l'estensione alla versione più recente.",
|
||||
"serverVersionNotSupported": "Il server di AliasVault necessita un aggiornamento a una versione più recente per poter usare questa estensione. Contatta il supporto se hai bisogno di assistenza.",
|
||||
"unknownError": "Si è verificato un errore sconosciuto",
|
||||
"failedToStoreVault": "Salvataggio cassaforte non riuscito",
|
||||
"vaultNotAvailable": "Cassaforte non disponibile",
|
||||
"failedToRetrieveData": "Recupero dati non riuscito",
|
||||
"vaultIsLocked": "La cassaforte è bloccata",
|
||||
"failedToUploadVault": "Caricare della cassaforte non riuscito.",
|
||||
"passwordChanged": "La tua password è cambiata dall'ultima volta che hai effettuato l'accesso. Effettua nuovamente l'accesso per motivi di sicurezza."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "Si è verificato un errore sconosciuto. Riprova.",
|
||||
"ACCOUNT_LOCKED": "Account temporaneamente bloccato a causa di troppi tentativi falliti. Riprova più tardi.",
|
||||
"ACCOUNT_BLOCKED": "Il tuo account è stato disabilitato. Se ritieni che sia un errore, contatta il supporto.",
|
||||
"USER_NOT_FOUND": "Nome utente o password non validi. Riprova.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Codice di autenticazione non valido. Riprova.",
|
||||
"INVALID_RECOVERY_CODE": "Codice di recupero non valido. Riprova.",
|
||||
"REFRESH_TOKEN_REQUIRED": "È necessario aggiornare il token.",
|
||||
"INVALID_REFRESH_TOKEN": "Token di aggiornamento non valido",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Aggiornamento token revocato con successo.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "La registrazione di nuovi account è attualmente disabilitata su questo server. Contatta l'amministratore.",
|
||||
"USERNAME_REQUIRED": "È richiesto il nome utente.",
|
||||
"USERNAME_ALREADY_IN_USE": "Il nome utente è già in uso.",
|
||||
"USERNAME_AVAILABLE": "Il nome utente è disponibile.",
|
||||
"USERNAME_MISMATCH": "Il nome utente non corrisponde all'utente corrente.",
|
||||
"PASSWORD_MISMATCH": "La password fornita non corrisponde alla password attuale.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Account eliminato con successo.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Il nome utente non può essere vuoto o contenere spazi.",
|
||||
"USERNAME_TOO_SHORT": "Nome utente troppo corto: deve contenere almeno 3 caratteri.",
|
||||
"USERNAME_TOO_LONG": "Nome utente troppo lungo: non può superare i 40 caratteri.",
|
||||
"USERNAME_INVALID_EMAIL": "Indirizzo email non valido.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Il nome utente non è valido, può contenere solo lettere o cifre.",
|
||||
"VAULT_NOT_UP_TO_DATE": "La tua cassaforte non è aggiornata. Sincronizzala e riprova.",
|
||||
"INTERNAL_SERVER_ERROR": "Errore interno del server.",
|
||||
"VAULT_ERROR": "La cassaforte locale non è aggiornata. Sincronizzala ricaricando la pagina e riprova."
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"or": "o",
|
||||
"new": "Nuovo",
|
||||
"cancel": "Annulla",
|
||||
"search": "Cerca",
|
||||
"vaultLocked": "AliasVault è bloccato.",
|
||||
"creatingNewAlias": "Creazione nuovo alias...",
|
||||
"noMatchesFound": "Nessun risultato trovato",
|
||||
"searchVault": "Cerca nella cassaforte...",
|
||||
"serviceName": "Nome servizio",
|
||||
"email": "E-mail",
|
||||
"username": "Nome utente",
|
||||
"password": "Password",
|
||||
"enterServiceName": "Inserisci nome servizio",
|
||||
"enterEmailAddress": "Inserisci indirizzo email",
|
||||
"enterUsername": "Inserisci nome utente",
|
||||
"hideFor1Hour": "Nascondi per 1 ora (sito corrente)",
|
||||
"hidePermanently": "Nascondi permanentemente (sito corrente)",
|
||||
"createRandomAlias": "Crea alias casuale",
|
||||
"createUsernamePassword": "Crea nome utente/password",
|
||||
"randomAlias": "Alias casuale",
|
||||
"usernamePassword": "Nome utente/password",
|
||||
"createAndSaveAlias": "Crea e salva alias",
|
||||
"createAndSaveCredential": "Crea e salva credenziali",
|
||||
"randomIdentityDescription": "Genera un'identità casuale con un indirizzo email casuale accessibile in AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Identità casuale con email casuale",
|
||||
"manualCredentialDescription": "Specifica il tuo indirizzo email e nome utente.",
|
||||
"manualCredentialDescriptionDropdown": "Nome utente e password manuali",
|
||||
"failedToCreateIdentity": "Impossibile creare identità. Riprova.",
|
||||
"enterEmailAndOrUsername": "Inserisci email e/o nome utente",
|
||||
"autofillWithAliasVault": "Compilazione automatica con AliasVault",
|
||||
"generateRandomPassword": "Genera password casuale (copia negli appunti)",
|
||||
"generateNewPassword": "Genera nuova password",
|
||||
"togglePasswordVisibility": "Mostra/Nascondi password",
|
||||
"passwordCopiedToClipboard": "Password copiata negli appunti",
|
||||
"enterEmailAndOrUsernameError": "Inserisci email e/o nome utente",
|
||||
"openAliasVaultToUpgrade": "Apri AliasVault per aggiornare",
|
||||
"vaultUpgradeRequired": "Aggiornamento della cassaforte richiesto.",
|
||||
"dismissPopup": "Chiudi finestra"
|
||||
},
|
||||
"credentials": {
|
||||
"title": "Credenziali",
|
||||
"addCredential": "Aggiungi credenziali",
|
||||
"editCredential": "Modifica credenziali",
|
||||
"deleteCredential": "Elimina credenziali",
|
||||
"credentialDetails": "Dettagli credenziali",
|
||||
"serviceName": "Nome servizio",
|
||||
"serviceNamePlaceholder": "es. Gmail, Facebook, Banca",
|
||||
"website": "Sito web",
|
||||
"websitePlaceholder": "https://esempio.com",
|
||||
"username": "Nome utente",
|
||||
"usernamePlaceholder": "Inserisci nome utente",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Inserisci password",
|
||||
"generatePassword": "Genera password",
|
||||
"copyPassword": "Copia password",
|
||||
"showPassword": "Mostra password",
|
||||
"hidePassword": "Nascondi password",
|
||||
"notes": "Note",
|
||||
"notesPlaceholder": "Note aggiuntive...",
|
||||
"totp": "Autenticazione a due fattori",
|
||||
"totpCode": "Codice TOTP",
|
||||
"copyTotp": "Copia TOTP",
|
||||
"totpSecret": "Segreto TOTP",
|
||||
"totpSecretPlaceholder": "Inserisci chiave segreta TOTP",
|
||||
"noCredentials": "Credenziali non trovate",
|
||||
"noCredentialsDescription": "Aggiungi le tue prime credenziali per iniziare",
|
||||
"searchPlaceholder": "Cerca credenziali...",
|
||||
"welcomeTitle": "Benvenuto in AliasVault!",
|
||||
"welcomeDescription": "Per usare l'estensione browser AliasVault: naviga su un sito e usa la finestra di compilazione automatica per creare una nuova credenziale.",
|
||||
"createdAt": "Creato",
|
||||
"updatedAt": "Ultimo aggiornamento",
|
||||
"autofill": "Compilazione automatica",
|
||||
"fillForm": "Compila modulo",
|
||||
"deleteConfirm": "Sei sicuro di voler eliminare questa credenziale?",
|
||||
"saveSuccess": "Credenziali salvate con successo",
|
||||
"tags": "Tag",
|
||||
"addTag": "Aggiungi tag",
|
||||
"removeTag": "Rimuovi tag",
|
||||
"folder": "Cartella",
|
||||
"selectFolder": "Seleziona cartella",
|
||||
"createFolder": "Crea cartella",
|
||||
"saveCredential": "Salva credenziale",
|
||||
"deleteCredentialTitle": "Elimina credenziale",
|
||||
"deleteCredentialConfirm": "Sei sicuro di voler eliminare queste credenziali? Questa azione non può essere annullata.",
|
||||
"randomAlias": "Alias casuale",
|
||||
"manual": "Manuale",
|
||||
"service": "Servizio",
|
||||
"serviceUrl": "URL del servizio",
|
||||
"loginCredentials": "Credenziali di accesso",
|
||||
"generateRandomUsername": "Genera nome utente casuale",
|
||||
"generateRandomPassword": "Genera password casuale",
|
||||
"changePasswordComplexity": "Modifica complessità password",
|
||||
"passwordLength": "Lunghezza password",
|
||||
"includeLowercase": "Includi lettere minuscole",
|
||||
"includeUppercase": "Includi lettere maiuscole",
|
||||
"includeNumbers": "Includi numeri",
|
||||
"includeSpecialChars": "Includi caratteri speciali",
|
||||
"avoidAmbiguousChars": "Evita caratteri ambigui (o, 0, ecc.)",
|
||||
"generateNewPreview": "Genera nuova anteprima",
|
||||
"generateRandomAlias": "Genera alias casuale",
|
||||
"clearAliasFields": "Cancella Campi Alias",
|
||||
"alias": "Alias",
|
||||
"firstName": "Nome",
|
||||
"lastName": "Cognome",
|
||||
"nickName": "Soprannome",
|
||||
"gender": "Genere",
|
||||
"birthDate": "Data di nascita",
|
||||
"birthDatePlaceholder": "AAAA-MM-GG",
|
||||
"metadata": "Metadati",
|
||||
"validation": {
|
||||
"required": "Questo campo è obbligatorio",
|
||||
"serviceNameRequired": "Il nome del servizio è obbligatorio",
|
||||
"invalidEmail": "Formato email non valido",
|
||||
"invalidDateFormat": "La data deve essere nel formato AAAA-MM-GG"
|
||||
},
|
||||
"privateEmailTitle": "Email privata",
|
||||
"privateEmailAliasVaultServer": "Server AliasVault",
|
||||
"privateEmailDescription": "E2E crittografato, completamente privato.",
|
||||
"publicEmailTitle": "Fornitori Pubblici di Email Temporanee",
|
||||
"publicEmailDescription": "Anonimi ma con privacy ridotta. Accessibile a chiunque conosca l'indirizzo.",
|
||||
"useDomainChooser": "Usa selettore di dominio",
|
||||
"enterCustomDomain": "Inserisci un dominio personalizzato",
|
||||
"enterFullEmail": "Inserisci l'indirizzo email completo",
|
||||
"enterEmailPrefix": "Inserisci prefisso email"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Email",
|
||||
"deleteEmailTitle": "Elimina Email",
|
||||
"deleteEmailConfirm": "Sei sicuro di voler eliminare definitivamente questa email?",
|
||||
"from": "Da",
|
||||
"to": "A",
|
||||
"date": "Data",
|
||||
"emailContent": "Contenuto email",
|
||||
"attachments": "Allegati",
|
||||
"emailNotFound": "Email non trovata",
|
||||
"noEmails": "Nessuna email trovata",
|
||||
"noEmailsDescription": "Non hai ancora ricevuto email ai tuoi indirizzi email privati. Quando ne riceverai una nuova, apparirà qui.",
|
||||
"dateFormat": {
|
||||
"justNow": "proprio ora",
|
||||
"minutesAgo_single": "{{count}} min fa",
|
||||
"minutesAgo_plural": "{{count}} min fa",
|
||||
"hoursAgo_single": "{{count}} ora fa",
|
||||
"hoursAgo_plural": "{{count}} ore fa",
|
||||
"yesterday": "ieri"
|
||||
},
|
||||
"errors": {
|
||||
"emailLoadError": "Si è verificato un errore durante il caricamento delle email. Riprova più tardi.",
|
||||
"emailUnexpectedError": "Si è verificato un errore imprevisto durante il caricamento delle email. Riprova più tardi."
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "L'indirizzo email attualmente scelto è già in uso. Cambia l'indirizzo modificando queste credenziali.",
|
||||
"CLAIM_DOES_NOT_EXIST": "Si è verificato un errore durante il caricamento delle email. Prova a modificare e salvare le credenziali per sincronizzare il database, poi riprova."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Impostazioni",
|
||||
"serverUrl": "URL del server",
|
||||
"language": "Lingua",
|
||||
"autofillEnabled": "Abilita compilazione automatica",
|
||||
"version": "Versione",
|
||||
"openInNewWindow": "Apri in una nuova finestra",
|
||||
"openWebApp": "Apri app web",
|
||||
"loggedIn": "Accesso effettuato",
|
||||
"logout": "Disconnetti",
|
||||
"globalSettings": "Impostazioni globali",
|
||||
"autofillPopup": "Popup compilazione automatica",
|
||||
"activeOnAllSites": "Attivo su tutti i siti (a meno che non sia disabilitato sotto)",
|
||||
"disabledOnAllSites": "Disabilitato su tutti i siti",
|
||||
"enabled": "Abilitato",
|
||||
"disabled": "Disabilitato",
|
||||
"rightClickContextMenu": "Menu contestuale clic destro",
|
||||
"autofillMatching": "Riconoscimento campi automatica.",
|
||||
"autofillMatchingMode": "Modalità riconoscimento capi automatica",
|
||||
"autofillMatchingModeDescription": "Determina quali credenziali vengono considerate corrispondenti e mostrate come suggerimenti nel popup di compilazione automatica per un determinato sito web.",
|
||||
"autofillMatchingDefault": "URL + sottodominio + nome wildcard",
|
||||
"autofillMatchingUrlSubdomain": "URL + sottodominio",
|
||||
"autofillMatchingUrlExact": "Solo dominio URL esatto",
|
||||
"siteSpecificSettings": "Impostazioni specifiche per sito",
|
||||
"autofillPopupOn": "Finestra compilazione automatica su: ",
|
||||
"enabledForThisSite": "Abilitato per questo sito",
|
||||
"disabledForThisSite": "Disabilitato per questo sito",
|
||||
"temporarilyDisabledUntil": "Disabilitato temporaneamente fino a ",
|
||||
"resetAllSiteSettings": "Reimposta tutte le impostazioni specifiche per sito",
|
||||
"appearance": "Aspetto",
|
||||
"theme": "Tema",
|
||||
"useDefault": "Usa predefinito",
|
||||
"light": "Chiaro",
|
||||
"dark": "Scuro",
|
||||
"keyboardShortcuts": "Scorciatoie da tastiera",
|
||||
"configureKeyboardShortcuts": "Configura scorciatoie da tastiera",
|
||||
"configure": "Configura",
|
||||
"security": "Sicurezza",
|
||||
"clipboardClearTimeout": "Cancella appunti dopo la copia",
|
||||
"clipboardClearTimeoutDescription": "Cancella automaticamente gli appunti dopo aver copiato i dati sensibili",
|
||||
"clipboardClearDisabled": "Non pulire mai",
|
||||
"clipboardClear5Seconds": "Cancella dopo 5 secondi",
|
||||
"clipboardClear10Seconds": "Cancella dopo 10 secondi",
|
||||
"clipboardClear15Seconds": "Cancella dopo 15 secondi",
|
||||
"autoLockTimeout": "Timeout Blocco Automatico",
|
||||
"autoLockTimeoutDescription": "Blocca automaticamente la cassaforte dopo un periodo di inattività",
|
||||
"autoLockTimeoutHelp": "La cassaforte si bloccherà solo dopo il periodo specificato di inattività (nessun utilizzo di riempimento automatico o estensione popup aperto). La cassaforte si bloccherà sempre quando il browser è chiuso, indipendentemente da questa impostazione.",
|
||||
"autoLockNever": "Mai",
|
||||
"autoLock15Seconds": "15 secondi",
|
||||
"autoLock1Minute": "1 minuto",
|
||||
"autoLock5Minutes": "5 minuti",
|
||||
"autoLock15Minutes": "15 minuti",
|
||||
"autoLock30Minutes": "30 minuti",
|
||||
"autoLock1Hour": "1 ora",
|
||||
"autoLock4Hours": "4 ore",
|
||||
"autoLock8Hours": "8 ore",
|
||||
"autoLock24Hours": "24 ore",
|
||||
"versionPrefix": "Versione ",
|
||||
"preferences": "Preferenze",
|
||||
"autofillSettings": "Impostazioni di riempimento automatico",
|
||||
"clipboardSettings": "Impostazioni appunti",
|
||||
"contextMenuSettings": "Preferenze menu contestuale",
|
||||
"contextMenu": "Menu contestuale",
|
||||
"contextMenuEnabled": "Il menu contestuale è attivato",
|
||||
"contextMenuDisabled": "Il menu contestuale è disabilitato",
|
||||
"contextMenuDescription": "Click destro sui campi di input per accedere alle opzioni di AliasVault",
|
||||
"selectLanguage": "Seleziona la lingua",
|
||||
"validation": {
|
||||
"apiUrlRequired": "L'URL API è obbligatorio",
|
||||
"apiUrlInvalid": "Inserisci un URL API valido",
|
||||
"clientUrlRequired": "L'URL del client è obbligatorio",
|
||||
"clientUrlInvalid": "Inserisci un URL del client valido"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "Aggiorna Cassaforte",
|
||||
"subtitle": "AliasVault è stato aggiornato e la tua cassaforte deve essere aggiornata. Dovrebbe richiedere solo pochi secondi.",
|
||||
"versionInformation": "Informazioni sulla versione",
|
||||
"yourVault": "La tua cassaforte:",
|
||||
"newVersion": "Nuova versione:",
|
||||
"upgrade": "Aggiorna cassaforte",
|
||||
"upgrading": "Aggiornamento in corso...",
|
||||
"logout": "Disconnetti",
|
||||
"whatsNew": "Novità",
|
||||
"whatsNewDescription": "È richiesto un aggiornamento per supportare le seguenti modifiche:",
|
||||
"noDescriptionAvailable": "Nessuna descrizione disponibile per questa versione.",
|
||||
"okay": "Ok",
|
||||
"status": {
|
||||
"preparingUpgrade": "Preparazione aggiornamento...",
|
||||
"vaultAlreadyUpToDate": "La cassaforte è già aggiornata",
|
||||
"startingDatabaseTransaction": "Avvio transazione database...",
|
||||
"applyingDatabaseMigrations": "Applicazione migrazioni database...",
|
||||
"applyingMigration": "Applicazione migrazione {{current}} di {{total}}...",
|
||||
"committingChanges": "Modifica in corso..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Errore",
|
||||
"unableToGetVersionInfo": "Impossibile ottenere informazioni sulla versione. Riprova.",
|
||||
"selfHostedServer": "Server Autospitato",
|
||||
"selfHostedWarning": "Se usi un server autospitato, assicurati di aggiornare anche la tua istanza, altrimenti l'accesso al client web smetterà di funzionare.",
|
||||
"cancel": "Annulla",
|
||||
"continueUpgrade": "Continua aggiornamento",
|
||||
"upgradeFailed": "Aggiornamento non riuscito",
|
||||
"failedToApplyMigration": "Impossibile eseguire la migrazione ({{current}} di {{total}})",
|
||||
"unknownErrorDuringUpgrade": "Si è verificato un errore sconosciuto durante l'aggiornamento. Riprova."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,26 +89,17 @@
|
||||
"executingOperation": "Actie uitvoeren...",
|
||||
"loadMore": "Laad meer",
|
||||
"errors": {
|
||||
"VaultMergeRequired": "Je vault moet worden bijgewerkt. Log in op de AliasVault website en volg de stappen.",
|
||||
"VaultOutdated": "Je vault is verouderd. Log in op de AliasVault website en volg de stappen.",
|
||||
"NoVaultFound": "Je account heeft nog geen vault. Voltooi eerst de tutorial in de AliasVault webclient voordat je de browserextensie gebruikt.",
|
||||
"serverNotAvailable": "De AliasVault server is niet beschikbaar. Probeer het later opnieuw of neem contact op met de ondersteuning als het probleem aanhoudt.",
|
||||
"clientVersionNotSupported": "Deze versie van de AliasVault browserextensie wordt niet meer ondersteund door de server. Update je browserextensie naar de nieuwste versie.",
|
||||
"serverVersionNotSupported": "De AliasVault server moet worden bijgewerkt naar een nieuwere versie om deze browserextensie te kunnen gebruiken. Neem contact op met support als je hulp nodig hebt.",
|
||||
"unknownError": "Er is een onbekende fout opgetreden",
|
||||
"failedToStoreVault": "Vault opslaan mislukt",
|
||||
"vaultNotAvailable": "Vault niet beschikbaar",
|
||||
"failedToGetVault": "Vault ophalen mislukt",
|
||||
"failedToRetrieveData": "Gegevens ophalen mislukt",
|
||||
"vaultIsLocked": "Vault is vergrendeld",
|
||||
"failedToGetCredentials": "Credentials ophalen mislukt",
|
||||
"failedToCreateIdentity": "Identiteit aanmaken mislukt",
|
||||
"failedToGetDefaultEmailDomain": "Standaard e-maildomein ophalen mislukt",
|
||||
"failedToGetDefaultIdentitySettings": "Standaard identiteit instellingen ophalen mislukt",
|
||||
"failedToGetPasswordSettings": "Wachtwoordinstellingen ophalen mislukt",
|
||||
"failedToUploadVault": "Vault uploaden mislukt",
|
||||
"noDerivedKeyAvailable": "Geen afgeleide sleutel beschikbaar voor versleuteling",
|
||||
"failedToUploadVaultToServer": "Nieuwe vault uploaden naar server mislukt",
|
||||
"noVaultOrDerivedKeyFound": "Geen vault of afgeleide sleutel gevonden"
|
||||
"passwordChanged": "Je wachtwoord is veranderd sinds de laatste keer dat je bent ingelogd. Log opnieuw in."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "Er is een onbekende fout opgetreden. Probeer het opnieuw.",
|
||||
@@ -118,8 +109,6 @@
|
||||
"INVALID_AUTHENTICATOR_CODE": "Ongeldige authenticator code. Probeer het opnieuw.",
|
||||
"INVALID_RECOVERY_CODE": "Ongeldige herstelcode. Probeer het opnieuw.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Refresh token is vereist.",
|
||||
"USER_NOT_FOUND_IN_TOKEN": "Gebruiker niet gevonden in token.",
|
||||
"USER_NOT_FOUND_IN_DATABASE": "Gebruiker niet gevonden in database.",
|
||||
"INVALID_REFRESH_TOKEN": "Ongeldig refresh token.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token succesvol ingetrokken.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "Registratie van nieuwe accounts is momenteel uitgeschakeld op deze server. Neem contact op met de beheerder.",
|
||||
@@ -206,23 +195,15 @@
|
||||
"totpSecretPlaceholder": "Voer TOTP secret in",
|
||||
"noCredentials": "Geen credentials gevonden",
|
||||
"noCredentialsDescription": "Voeg je eerste credentials toe om te beginnen",
|
||||
"searchCredentials": "Zoek credentials...",
|
||||
"searchPlaceholder": "Credentials zoeken...",
|
||||
"welcomeTitle": "Welkom bij AliasVault!",
|
||||
"welcomeDescription": "Om de AliasVault browser extensie te gebruiken: navigeer naar een website en gebruik de AliasVault autofill popup om nieuwe credentials aan te maken.",
|
||||
"lastUsed": "Laatst gebruikt",
|
||||
"createdAt": "Aangemaakt",
|
||||
"updatedAt": "Laatst bijgewerkt",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Formulier invullen",
|
||||
"copyUsername": "Gebruikersnaam kopiëren",
|
||||
"openWebsite": "Website openen",
|
||||
"favorite": "Favoriet",
|
||||
"unfavorite": "Uit favorieten verwijderen",
|
||||
"deleteConfirm": "Weet je zeker dat je deze credential wilt verwijderen?",
|
||||
"deleteSuccess": "Credential succesvol verwijderd",
|
||||
"saveSuccess": "Credential succesvol opgeslagen",
|
||||
"copySuccess": "Gekopieerd naar klembord",
|
||||
"tags": "Labels",
|
||||
"addTag": "Label toevoegen",
|
||||
"removeTag": "Label verwijderen",
|
||||
@@ -248,6 +229,7 @@
|
||||
"avoidAmbiguousChars": "Onduidelijke tekens vermijden (o, 0, etc.)",
|
||||
"generateNewPreview": "Genereer nieuw voorbeeld",
|
||||
"generateRandomAlias": "Alias genereren",
|
||||
"clearAliasFields": "Leeg alias velden",
|
||||
"alias": "Alias",
|
||||
"firstName": "Voornaam",
|
||||
"lastName": "Achternaam",
|
||||
@@ -256,20 +238,21 @@
|
||||
"birthDate": "Geboortedatum",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"errors": {
|
||||
"invalidUrl": "Voer een geldige URL in",
|
||||
"saveError": "Credential opslaan mislukt",
|
||||
"loadError": "Credential laden mislukt",
|
||||
"deleteError": "Credential verwijderen mislukt",
|
||||
"copyError": "Kopiëren naar klembord mislukt"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Dit veld is verplicht",
|
||||
"serviceNameRequired": "Servicenaam is verplicht",
|
||||
"invalidUrl": "Ongeldig URL-formaat",
|
||||
"invalidEmail": "Ongeldig e-mailformaat",
|
||||
"invalidDateFormat": "Datum moet in YYYY-MM-DD formaat zijn"
|
||||
}
|
||||
},
|
||||
"privateEmailTitle": "Privé e-mail",
|
||||
"privateEmailAliasVaultServer": "AliasVault server",
|
||||
"privateEmailDescription": "E2E versleuteld, volledig privé.",
|
||||
"publicEmailTitle": "Publieke tijdelijke e-mailproviders",
|
||||
"publicEmailDescription": "Anoniem maar beperkte privacy. E-mail inhoud is leesbaar door iedereen die het adres kent.",
|
||||
"useDomainChooser": "Domein kiezen",
|
||||
"enterCustomDomain": "Voer aangepast domein in",
|
||||
"enterFullEmail": "Voer volledig e-mailadres in",
|
||||
"enterEmailPrefix": "E-mailprefix invoeren"
|
||||
},
|
||||
"emails": {
|
||||
"title": "E-mails",
|
||||
@@ -317,6 +300,12 @@
|
||||
"enabled": "Ingeschakeld",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"rightClickContextMenu": "Rechtermuisknop menu",
|
||||
"autofillMatching": "Autofill matching",
|
||||
"autofillMatchingMode": "Autofill matching modus",
|
||||
"autofillMatchingModeDescription": "Bepaalt op welke manier credentials worden beschouwd als matches en worden getoond als suggestie in de autofill popup voor een bepaalde website.",
|
||||
"autofillMatchingDefault": "URL + subdomein + naam wildcard",
|
||||
"autofillMatchingUrlSubdomain": "URL + subdomein",
|
||||
"autofillMatchingUrlExact": "Exacte URL-domein",
|
||||
"siteSpecificSettings": "Site-specifieke Instellingen",
|
||||
"autofillPopupOn": "Autofill popup op: ",
|
||||
"enabledForThisSite": "Ingeschakeld voor deze site",
|
||||
@@ -331,7 +320,36 @@
|
||||
"keyboardShortcuts": "Snelkoppelingen",
|
||||
"configureKeyboardShortcuts": "Snelkoppelingen configureren",
|
||||
"configure": "Configureren",
|
||||
"security": "Beveiliging",
|
||||
"clipboardClearTimeout": "Automatisch klembord wissen na kopiëren",
|
||||
"clipboardClearTimeoutDescription": "Automatisch het klembord wissen na kopiëren van gevoelige gegevens",
|
||||
"clipboardClearDisabled": "Nooit wissen",
|
||||
"clipboardClear5Seconds": "Wis na 5 seconden",
|
||||
"clipboardClear10Seconds": "Wis na 10 seconden",
|
||||
"clipboardClear15Seconds": "Wis na 15 seconden",
|
||||
"autoLockTimeout": "Automatisch vergrendelen",
|
||||
"autoLockTimeoutDescription": "Vergrendel de vault automatisch na inactiviteit",
|
||||
"autoLockTimeoutHelp": "De vault zal alleen vergrendelen na de opgegeven periode van inactiviteit (geen automatisch invullen of extensie geopend). De vault wordt altijd vergrendeld wanneer de browser wordt afgesloten, ongeacht deze instelling.",
|
||||
"autoLockNever": "Nooit",
|
||||
"autoLock15Seconds": "15 seconden",
|
||||
"autoLock1Minute": "1 minuut",
|
||||
"autoLock5Minutes": "5 minuten",
|
||||
"autoLock15Minutes": "15 minuten",
|
||||
"autoLock30Minutes": "30 minuten",
|
||||
"autoLock1Hour": "1 uur",
|
||||
"autoLock4Hours": "4 uur",
|
||||
"autoLock8Hours": "8 uur",
|
||||
"autoLock24Hours": "24 uur",
|
||||
"versionPrefix": "Versie ",
|
||||
"preferences": "Voorkeuren",
|
||||
"autofillSettings": "Autofill instellingen",
|
||||
"clipboardSettings": "Klembord instellingen",
|
||||
"contextMenuSettings": "Context menu instellingen",
|
||||
"contextMenu": "Context menu",
|
||||
"contextMenuEnabled": "Context menu is ingeschakeld",
|
||||
"contextMenuDisabled": "Context menu is uitgeschakeld",
|
||||
"contextMenuDescription": "Klik met de rechtermuisknop op invoervelden om AliasVault opties te zien",
|
||||
"selectLanguage": "Selecteer taal",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL is vereist",
|
||||
"apiUrlInvalid": "Voer een geldige API URL in",
|
||||
|
||||
393
apps/browser-extension/src/i18n/locales/pt.json
Normal file
393
apps/browser-extension/src/i18n/locales/pt.json
Normal file
@@ -0,0 +1,393 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "Log in to AliasVault",
|
||||
"username": "Username or email",
|
||||
"usernamePlaceholder": "name / name@company.com",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"rememberMe": "Remember me",
|
||||
"loginButton": "Login",
|
||||
"noAccount": "No account yet?",
|
||||
"createVault": "Create new vault",
|
||||
"twoFactorTitle": "Please enter the authentication code from your authenticator app.",
|
||||
"authCode": "Authentication Code",
|
||||
"authCodePlaceholder": "Enter 6-digit code",
|
||||
"verify": "Verify",
|
||||
"cancel": "Cancel",
|
||||
"twoFactorNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.",
|
||||
"masterPassword": "Master Password",
|
||||
"unlockVault": "Unlock Vault",
|
||||
"unlockTitle": "Unlock Your Vault",
|
||||
"unlockDescription": "Enter your master password to unlock your vault.",
|
||||
"logout": "Logout",
|
||||
"logoutConfirm": "Are you sure you want to logout?",
|
||||
"sessionExpired": "Your session has expired. Please log in again.",
|
||||
"unlockSuccess": "Vault unlocked successfully!",
|
||||
"unlockSuccessTitle": "Your vault is successfully unlocked",
|
||||
"unlockSuccessDescription": "You can now use autofill in login forms in your browser.",
|
||||
"closePopup": "Close this popup",
|
||||
"browseVault": "Browse vault contents",
|
||||
"connectingTo": "Connecting to",
|
||||
"switchAccounts": "Switch accounts?",
|
||||
"loggedIn": "Logged in",
|
||||
"errors": {
|
||||
"invalidCode": "Please enter a valid 6-digit authentication code.",
|
||||
"serverError": "Could not reach AliasVault server. Please try again later or contact support if the problem persists.",
|
||||
"noToken": "Login failed -- no token returned",
|
||||
"migrationError": "An error occurred while checking for pending migrations.",
|
||||
"wrongPassword": "Incorrect password. Please try again.",
|
||||
"accountLocked": "Account temporarily locked due to too many failed attempts.",
|
||||
"networkError": "Network error. Please check your connection and try again.",
|
||||
"loginDataMissing": "Login session expired. Please try again."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "Credentials",
|
||||
"emails": "Emails",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"cancel": "Cancel",
|
||||
"use": "Use",
|
||||
"delete": "Delete",
|
||||
"close": "Close",
|
||||
"copied": "Copied!",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"language": "Language",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"loadingEmails": "Loading emails...",
|
||||
"loadingTotpCodes": "Loading TOTP codes...",
|
||||
"attachments": "Attachments",
|
||||
"loadingAttachments": "Loading attachments...",
|
||||
"settings": "Settings",
|
||||
"recentEmails": "Recent emails",
|
||||
"loginCredentials": "Login credentials",
|
||||
"twoFactorAuthentication": "Two-factor authentication",
|
||||
"alias": "Alias",
|
||||
"notes": "Notes",
|
||||
"fullName": "Full Name",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"birthDate": "Birth Date",
|
||||
"nickname": "Nickname",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"syncingVault": "Syncing vault",
|
||||
"savingChangesToVault": "Saving changes to vault",
|
||||
"uploadingVaultToServer": "Uploading vault to server",
|
||||
"checkingVaultUpdates": "Checking for vault updates",
|
||||
"syncingUpdatedVault": "Syncing updated vault",
|
||||
"executingOperation": "Executing operation...",
|
||||
"loadMore": "Load more",
|
||||
"errors": {
|
||||
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
|
||||
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
|
||||
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
|
||||
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
|
||||
"unknownError": "An unknown error occurred",
|
||||
"failedToStoreVault": "Failed to store vault",
|
||||
"vaultNotAvailable": "Vault not available",
|
||||
"failedToRetrieveData": "Failed to retrieve data",
|
||||
"vaultIsLocked": "Vault is locked",
|
||||
"failedToUploadVault": "Failed to upload vault",
|
||||
"passwordChanged": "Your password has changed since the last time you logged in. Please login again for security reasons."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",
|
||||
"ACCOUNT_LOCKED": "Account temporarily locked due to too many failed attempts. Please try again later.",
|
||||
"ACCOUNT_BLOCKED": "Your account has been disabled. If you believe this is a mistake, please contact support.",
|
||||
"USER_NOT_FOUND": "Invalid username or password. Please try again.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Invalid authenticator code. Please try again.",
|
||||
"INVALID_RECOVERY_CODE": "Invalid recovery code. Please try again.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Refresh token is required.",
|
||||
"INVALID_REFRESH_TOKEN": "Invalid refresh token.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token revoked successfully.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "New account registration is currently disabled on this server. Please contact the administrator.",
|
||||
"USERNAME_REQUIRED": "Username is required.",
|
||||
"USERNAME_ALREADY_IN_USE": "Username is already in use.",
|
||||
"USERNAME_AVAILABLE": "Username is available.",
|
||||
"USERNAME_MISMATCH": "Username does not match the current user.",
|
||||
"PASSWORD_MISMATCH": "The provided password does not match your current password.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Account successfully deleted.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Username cannot be empty or whitespace.",
|
||||
"USERNAME_TOO_SHORT": "Username too short: must be at least 3 characters long.",
|
||||
"USERNAME_TOO_LONG": "Username too long: cannot be longer than 40 characters.",
|
||||
"USERNAME_INVALID_EMAIL": "Invalid email address.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Username is invalid, can only contain letters or digits.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Your vault is not up-to-date. Please synchronize your vault and try again.",
|
||||
"INTERNAL_SERVER_ERROR": "Internal server error.",
|
||||
"VAULT_ERROR": "The local vault is not up-to-date. Please synchronize your vault by refreshing the page and try again."
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"or": "or",
|
||||
"new": "New",
|
||||
"cancel": "Cancel",
|
||||
"search": "Search",
|
||||
"vaultLocked": "AliasVault is locked.",
|
||||
"creatingNewAlias": "Creating new alias...",
|
||||
"noMatchesFound": "No matches found",
|
||||
"searchVault": "Search vault...",
|
||||
"serviceName": "Service name",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"enterServiceName": "Enter service name",
|
||||
"enterEmailAddress": "Enter email address",
|
||||
"enterUsername": "Enter username",
|
||||
"hideFor1Hour": "Hide for 1 hour (current site)",
|
||||
"hidePermanently": "Hide permanently (current site)",
|
||||
"createRandomAlias": "Create random alias",
|
||||
"createUsernamePassword": "Create username/password",
|
||||
"randomAlias": "Random alias",
|
||||
"usernamePassword": "Username/password",
|
||||
"createAndSaveAlias": "Create and save alias",
|
||||
"createAndSaveCredential": "Create and save credential",
|
||||
"randomIdentityDescription": "Generate a random identity with a random email address accessible in AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Random identity with random email",
|
||||
"manualCredentialDescription": "Specify your own email address and username.",
|
||||
"manualCredentialDescriptionDropdown": "Manual username and password",
|
||||
"failedToCreateIdentity": "Failed to create identity. Please try again.",
|
||||
"enterEmailAndOrUsername": "Enter email and/or username",
|
||||
"autofillWithAliasVault": "Autofill with AliasVault",
|
||||
"generateRandomPassword": "Generate random password (copy to clipboard)",
|
||||
"generateNewPassword": "Generate new password",
|
||||
"togglePasswordVisibility": "Toggle password visibility",
|
||||
"passwordCopiedToClipboard": "Password copied to clipboard",
|
||||
"enterEmailAndOrUsernameError": "Enter email and/or username",
|
||||
"openAliasVaultToUpgrade": "Open AliasVault to upgrade",
|
||||
"vaultUpgradeRequired": "Vault upgrade required.",
|
||||
"dismissPopup": "Dismiss popup"
|
||||
},
|
||||
"credentials": {
|
||||
"title": "Credentials",
|
||||
"addCredential": "Add Credential",
|
||||
"editCredential": "Edit Credential",
|
||||
"deleteCredential": "Delete Credential",
|
||||
"credentialDetails": "Credential Details",
|
||||
"serviceName": "Service Name",
|
||||
"serviceNamePlaceholder": "e.g., Gmail, Facebook, Bank",
|
||||
"website": "Website",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "Enter username",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"generatePassword": "Generate Password",
|
||||
"copyPassword": "Copy Password",
|
||||
"showPassword": "Show Password",
|
||||
"hidePassword": "Hide Password",
|
||||
"notes": "Notes",
|
||||
"notesPlaceholder": "Additional notes...",
|
||||
"totp": "Two-Factor Authentication",
|
||||
"totpCode": "TOTP Code",
|
||||
"copyTotp": "Copy TOTP",
|
||||
"totpSecret": "TOTP Secret",
|
||||
"totpSecretPlaceholder": "Enter TOTP secret key",
|
||||
"noCredentials": "No credentials found",
|
||||
"noCredentialsDescription": "Add your first credential to get started",
|
||||
"searchPlaceholder": "Search credentials...",
|
||||
"welcomeTitle": "Welcome to AliasVault!",
|
||||
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Fill Form",
|
||||
"deleteConfirm": "Are you sure you want to delete this credential?",
|
||||
"saveSuccess": "Credential saved successfully",
|
||||
"tags": "Tags",
|
||||
"addTag": "Add Tag",
|
||||
"removeTag": "Remove Tag",
|
||||
"folder": "Folder",
|
||||
"selectFolder": "Select Folder",
|
||||
"createFolder": "Create Folder",
|
||||
"saveCredential": "Save credential",
|
||||
"deleteCredentialTitle": "Delete Credential",
|
||||
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
|
||||
"randomAlias": "Random Alias",
|
||||
"manual": "Manual",
|
||||
"service": "Service",
|
||||
"serviceUrl": "Service URL",
|
||||
"loginCredentials": "Login Credentials",
|
||||
"generateRandomUsername": "Generate random username",
|
||||
"generateRandomPassword": "Generate random password",
|
||||
"changePasswordComplexity": "Change password complexity",
|
||||
"passwordLength": "Password length",
|
||||
"includeLowercase": "Include lowercase letters",
|
||||
"includeUppercase": "Include uppercase letters",
|
||||
"includeNumbers": "Include numbers",
|
||||
"includeSpecialChars": "Include special characters",
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"nickName": "Nick Name",
|
||||
"gender": "Gender",
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidEmail": "Invalid email format",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
|
||||
},
|
||||
"privateEmailTitle": "Private Email",
|
||||
"privateEmailAliasVaultServer": "AliasVault server",
|
||||
"privateEmailDescription": "E2E encrypted, fully private.",
|
||||
"publicEmailTitle": "Public Temp Email Providers",
|
||||
"publicEmailDescription": "Anonymous but limited privacy. Email content is readable by anyone that knows the address.",
|
||||
"useDomainChooser": "Use domain chooser",
|
||||
"enterCustomDomain": "Enter custom domain",
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
"deleteEmailTitle": "Delete Email",
|
||||
"deleteEmailConfirm": "Are you sure you want to permanently delete this email?",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"date": "Date",
|
||||
"emailContent": "Email content",
|
||||
"attachments": "Attachments",
|
||||
"emailNotFound": "Email not found",
|
||||
"noEmails": "No emails found",
|
||||
"noEmailsDescription": "You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.",
|
||||
"dateFormat": {
|
||||
"justNow": "just now",
|
||||
"minutesAgo_single": "{{count}} min ago",
|
||||
"minutesAgo_plural": "{{count}} mins ago",
|
||||
"hoursAgo_single": "{{count}} hr ago",
|
||||
"hoursAgo_plural": "{{count}} hrs ago",
|
||||
"yesterday": "yesterday"
|
||||
},
|
||||
"errors": {
|
||||
"emailLoadError": "An error occurred while loading emails. Please try again later.",
|
||||
"emailUnexpectedError": "An unexpected error occurred while loading emails. Please try again later."
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "The current chosen email address is already in use. Please change the email address by editing this credential.",
|
||||
"CLAIM_DOES_NOT_EXIST": "An error occurred while trying to load the emails. Please try to edit and save the credential entry to synchronize the database, then try again."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"serverUrl": "Server URL",
|
||||
"language": "Language",
|
||||
"autofillEnabled": "Enable Autofill",
|
||||
"version": "Version",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"openWebApp": "Open web app",
|
||||
"loggedIn": "Logged in",
|
||||
"logout": "Logout",
|
||||
"globalSettings": "Global Settings",
|
||||
"autofillPopup": "Autofill popup",
|
||||
"activeOnAllSites": "Active on all sites (unless disabled below)",
|
||||
"disabledOnAllSites": "Disabled on all sites",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"rightClickContextMenu": "Right-click context menu",
|
||||
"autofillMatching": "Autofill Matching",
|
||||
"autofillMatchingMode": "Autofill matching mode",
|
||||
"autofillMatchingModeDescription": "Determines which credentials are considered a match and shown as suggestions in the autofill popup for a given website.",
|
||||
"autofillMatchingDefault": "URL + subdomain + name wildcard",
|
||||
"autofillMatchingUrlSubdomain": "URL + subdomain",
|
||||
"autofillMatchingUrlExact": "Exact URL domain only",
|
||||
"siteSpecificSettings": "Site-Specific Settings",
|
||||
"autofillPopupOn": "Autofill popup on: ",
|
||||
"enabledForThisSite": "Enabled for this site",
|
||||
"disabledForThisSite": "Disabled for this site",
|
||||
"temporarilyDisabledUntil": "Temporarily disabled until ",
|
||||
"resetAllSiteSettings": "Reset all site-specific settings",
|
||||
"appearance": "Appearance",
|
||||
"theme": "Theme",
|
||||
"useDefault": "Use default",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||
"configureKeyboardShortcuts": "Configure keyboard shortcuts",
|
||||
"configure": "Configure",
|
||||
"security": "Security",
|
||||
"clipboardClearTimeout": "Clear clipboard after copying",
|
||||
"clipboardClearTimeoutDescription": "Automatically clear the clipboard after copying sensitive data",
|
||||
"clipboardClearDisabled": "Never clear",
|
||||
"clipboardClear5Seconds": "Clear after 5 seconds",
|
||||
"clipboardClear10Seconds": "Clear after 10 seconds",
|
||||
"clipboardClear15Seconds": "Clear after 15 seconds",
|
||||
"autoLockTimeout": "Auto-lock Timeout",
|
||||
"autoLockTimeoutDescription": "Automatically lock the vault after a period of inactivity",
|
||||
"autoLockTimeoutHelp": "The vault will only lock after the specified period of inactivity (no autofill usage or extension popup opened). The vault will always lock when the browser is closed, regardless of this setting.",
|
||||
"autoLockNever": "Never",
|
||||
"autoLock15Seconds": "15 seconds",
|
||||
"autoLock1Minute": "1 minute",
|
||||
"autoLock5Minutes": "5 minutes",
|
||||
"autoLock15Minutes": "15 minutes",
|
||||
"autoLock30Minutes": "30 minutes",
|
||||
"autoLock1Hour": "1 hour",
|
||||
"autoLock4Hours": "4 hours",
|
||||
"autoLock8Hours": "8 hours",
|
||||
"autoLock24Hours": "24 hours",
|
||||
"versionPrefix": "Version ",
|
||||
"preferences": "Preferences",
|
||||
"autofillSettings": "Autofill Settings",
|
||||
"clipboardSettings": "Clipboard Settings",
|
||||
"contextMenuSettings": "Context Menu Settings",
|
||||
"contextMenu": "Context Menu",
|
||||
"contextMenuEnabled": "Context menu is enabled",
|
||||
"contextMenuDisabled": "Context menu is disabled",
|
||||
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
|
||||
"selectLanguage": "Select Language",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL is required",
|
||||
"apiUrlInvalid": "Please enter a valid API URL",
|
||||
"clientUrlRequired": "Client URL is required",
|
||||
"clientUrlInvalid": "Please enter a valid client URL"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "Upgrade Vault",
|
||||
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
|
||||
"versionInformation": "Version Information",
|
||||
"yourVault": "Your vault:",
|
||||
"newVersion": "New version:",
|
||||
"upgrade": "Upgrade Vault",
|
||||
"upgrading": "Upgrading...",
|
||||
"logout": "Logout",
|
||||
"whatsNew": "What's New",
|
||||
"whatsNewDescription": "An upgrade is required to support the following changes:",
|
||||
"noDescriptionAvailable": "No description available for this version.",
|
||||
"okay": "Ok",
|
||||
"status": {
|
||||
"preparingUpgrade": "Preparing upgrade...",
|
||||
"vaultAlreadyUpToDate": "Vault is already up to date",
|
||||
"startingDatabaseTransaction": "Starting database transaction...",
|
||||
"applyingDatabaseMigrations": "Applying database migrations...",
|
||||
"applyingMigration": "Applying migration {{current}} of {{total}}...",
|
||||
"committingChanges": "Committing changes..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Error",
|
||||
"unableToGetVersionInfo": "Unable to get version information. Please try again.",
|
||||
"selfHostedServer": "Self-Hosted Server",
|
||||
"selfHostedWarning": "If you're using a self-hosted server, make sure to also update your self-hosted instance as otherwise logging in to the web client will stop working.",
|
||||
"cancel": "Cancel",
|
||||
"continueUpgrade": "Continue Upgrade",
|
||||
"upgradeFailed": "Upgrade Failed",
|
||||
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
|
||||
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
393
apps/browser-extension/src/i18n/locales/ru.json
Normal file
393
apps/browser-extension/src/i18n/locales/ru.json
Normal file
@@ -0,0 +1,393 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "Войдите в AliasVault",
|
||||
"username": "Имя пользователя или почта",
|
||||
"usernamePlaceholder": "имя / имя@company.com",
|
||||
"password": "Пароль",
|
||||
"passwordPlaceholder": "Введите ваш пароль",
|
||||
"rememberMe": "Запомнить меня",
|
||||
"loginButton": "Логин",
|
||||
"noAccount": "Нет аккаунта?",
|
||||
"createVault": "Создать новое хранилище",
|
||||
"twoFactorTitle": "Пожалуйста, введите код аутентификации из вашего приложения-аутентификатора.",
|
||||
"authCode": "Код аутентификации",
|
||||
"authCodePlaceholder": "Введите 6-значный код",
|
||||
"verify": "Проверить",
|
||||
"cancel": "Отменить",
|
||||
"twoFactorNote": "Примечание: если у вас нет доступа к устройству аутентификации, вы можете сбросить ваш 2FA с помощью кода восстановления, войдя в систему через сайт.",
|
||||
"masterPassword": "Мастер пароль",
|
||||
"unlockVault": "Разблокировать хранилище",
|
||||
"unlockTitle": "Разблокировать ваше хранилище",
|
||||
"unlockDescription": "Введите ваш мастер пароль для разблокировки вашего хранилища.",
|
||||
"logout": "Выйти",
|
||||
"logoutConfirm": "Вы уверены, что хотите выйти?",
|
||||
"sessionExpired": "Время сеанса истекло. Пожалуйста, войдите снова.",
|
||||
"unlockSuccess": "Хранилище успешно разблокировано!",
|
||||
"unlockSuccessTitle": "Ваше хранилище успешно разблокировано",
|
||||
"unlockSuccessDescription": "Теперь вы можете использовать автозаполнение форм входа в Вашем браузере.",
|
||||
"closePopup": "Закрыть окно",
|
||||
"browseVault": "Обзор содержимого хранилища",
|
||||
"connectingTo": "Подключение к",
|
||||
"switchAccounts": "Переключить аккаунт?",
|
||||
"loggedIn": "Вход выполнен",
|
||||
"errors": {
|
||||
"invalidCode": "Пожалуйста, введите правильный 6-значный код аутентификации.",
|
||||
"serverError": "Не удалось подключиться к серверу AliasVault. Пожалуйста, повторите попытку позже или обратитесь в службу поддержки, если проблема не устранится.",
|
||||
"noToken": "Вход не удался -- токен не возвращён",
|
||||
"migrationError": "Возникла ошибка при проверке ожидающих перемещений.",
|
||||
"wrongPassword": "Неверный пароль. Пожалуйста, повторите попытку.",
|
||||
"accountLocked": "Аккаунт временно заблокирован из-за слишком большого числа неудачных попыток.",
|
||||
"networkError": "Ошибка сети. Пожалуйста, проверьте соединение и повторите еще раз.",
|
||||
"loginDataMissing": "Время входа истекло. Пожалуйста, повторите попытку."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "Учетные данные",
|
||||
"emails": "Почта",
|
||||
"settings": "Настройки"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Загрузка...",
|
||||
"error": "Ошибка",
|
||||
"success": "Успешно",
|
||||
"cancel": "Отмена",
|
||||
"use": "Использовать",
|
||||
"delete": "Удалить",
|
||||
"close": "Закрыть",
|
||||
"copied": "Скопировано!",
|
||||
"openInNewWindow": "Открыть в новом окне",
|
||||
"language": "Язык",
|
||||
"enabled": "Включено",
|
||||
"disabled": "Отключено",
|
||||
"showPassword": "Показать пароль",
|
||||
"hidePassword": "Скрыть пароль",
|
||||
"copyToClipboard": "Скопировать в буфер обмена",
|
||||
"loadingEmails": "Загрузка писем...",
|
||||
"loadingTotpCodes": "Загрузка TOTP кодов...",
|
||||
"attachments": "Вложения",
|
||||
"loadingAttachments": "Загрузка вложений...",
|
||||
"settings": "Настройки",
|
||||
"recentEmails": "Последние письма",
|
||||
"loginCredentials": "Данные для авторизации",
|
||||
"twoFactorAuthentication": "Двухфакторная аутентификация",
|
||||
"alias": "Псевдоним",
|
||||
"notes": "Заметки",
|
||||
"fullName": "Полное имя",
|
||||
"firstName": "Имя",
|
||||
"lastName": "Фамилия",
|
||||
"birthDate": "Дата рождения",
|
||||
"nickname": "Никнейм",
|
||||
"email": "Электронная почта",
|
||||
"username": "Имя пользователя",
|
||||
"password": "Пароль",
|
||||
"syncingVault": "Синхронизация хранилища",
|
||||
"savingChangesToVault": "Сохранение изменений в хранилище",
|
||||
"uploadingVaultToServer": "Загрузка хранилища на сервер",
|
||||
"checkingVaultUpdates": "Проверка наличия обновлений в хранилище",
|
||||
"syncingUpdatedVault": "Синхронизация обновленного хранилища",
|
||||
"executingOperation": "Выполнение операций...",
|
||||
"loadMore": "Загрузить ещё",
|
||||
"errors": {
|
||||
"VaultOutdated": "Ваше хранилище устарело. Пожалуйста, войдите на сайт AliasVault и следуйте инструкциям.",
|
||||
"serverNotAvailable": "Сервер AliasVault недоступен. Пожалуйста, повторите попытку позже или обратитесь в службу поддержки, если проблема не устранится.",
|
||||
"clientVersionNotSupported": "Эта версия браузерного расширения AliasVault больше не поддерживается сервером. Пожалуйста, обновите расширение вашего браузера до последней версии.",
|
||||
"serverVersionNotSupported": "Чтобы использовать это расширение для браузера, сервер AliasVault необходимо обновить до более новой версии. Пожалуйста, обратитесь в службу поддержки, если вам нужна помощь.",
|
||||
"unknownError": "Произошла неизвестная ошибка",
|
||||
"failedToStoreVault": "Не удалось сохранить хранилище",
|
||||
"vaultNotAvailable": "Хранилище недоступно",
|
||||
"failedToRetrieveData": "Не удалось получить данные",
|
||||
"vaultIsLocked": "Хранилище заблокировано",
|
||||
"failedToUploadVault": "Не удалось загрузить хранилище",
|
||||
"passwordChanged": "С момента вашего последнего входа ваш пароль изменился. Пожалуйста, войдите еще раз в целях безопасности."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "Произошла неизвестная ошибка. Пожалуйста, попробуйте снова.",
|
||||
"ACCOUNT_LOCKED": "Учетная запись временно заблокирована из-за слишком большого количества неудачных попыток. Пожалуйста, повторите попытку позже.",
|
||||
"ACCOUNT_BLOCKED": "Ваша учетная запись была заблокирована. Если вы считаете, что это ошибка, пожалуйста, свяжитесь со службой поддержки.",
|
||||
"USER_NOT_FOUND": "Неверное имя пользователя или пароль. Пожалуйста, попробуйте снова.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Неверный код аутентификатора. Пожалуйста, попробуйте снова.",
|
||||
"INVALID_RECOVERY_CODE": "Неверный код восстановления. Пожалуйста, попробуйте снова.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Требуется токен обновления.",
|
||||
"INVALID_REFRESH_TOKEN": "Недопустимый токен обновления.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Токен обновления успешно отозван.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "В настоящее время регистрация новой учетной записи на этом сервере отключена. Пожалуйста, свяжитесь с администратором.",
|
||||
"USERNAME_REQUIRED": "Требуется ввести имя пользователя.",
|
||||
"USERNAME_ALREADY_IN_USE": "Имя пользователя уже используется.",
|
||||
"USERNAME_AVAILABLE": "Имя пользователя доступно.",
|
||||
"USERNAME_MISMATCH": "Имя пользователя не соответствует текущему пользователю.",
|
||||
"PASSWORD_MISMATCH": "Указанный пароль не совпадает с вашим текущим паролем.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Аккаунт успешно удалена.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Имя пользователя не может быть пустым или через пробел.",
|
||||
"USERNAME_TOO_SHORT": "Слишком короткое имя пользователя: должно быть не менее 3-х символов.",
|
||||
"USERNAME_TOO_LONG": "Слишком длинное имя пользователя: оно не может быть длиннее 40 символов.",
|
||||
"USERNAME_INVALID_EMAIL": "Неверный адрес электронной почты.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Имя пользователя неверно, может содержать только буквы или цифры.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Ваше хранилище не обновлено. Пожалуйста, синхронизируйте ваше хранилище и повторите попытку.",
|
||||
"INTERNAL_SERVER_ERROR": "Внутренняя ошибка сервера.",
|
||||
"VAULT_ERROR": "Локальное хранилище не обновлено. Пожалуйста, синхронизируйте ваше хранилище, обновив страницу, и повторите попытку."
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"or": "или",
|
||||
"new": "Новый",
|
||||
"cancel": "отмена",
|
||||
"search": "Поиск",
|
||||
"vaultLocked": "AliasVault заблокирован.",
|
||||
"creatingNewAlias": "Создание нового псевдонима...",
|
||||
"noMatchesFound": "Совпадений не найдено",
|
||||
"searchVault": "Поиск хранилища...",
|
||||
"serviceName": "Имя сервиса",
|
||||
"email": "Электронная почта",
|
||||
"username": "Имя пользователя",
|
||||
"password": "Пароль",
|
||||
"enterServiceName": "Введите имя сервиса",
|
||||
"enterEmailAddress": "Введите адрес электронной почты",
|
||||
"enterUsername": "Введите имя пользователя",
|
||||
"hideFor1Hour": "Скрыть на 1 час (текущий сайт)",
|
||||
"hidePermanently": "Скрыть навсегда (текущий сайт)",
|
||||
"createRandomAlias": "Создать случайный псевдоним",
|
||||
"createUsernamePassword": "Создайте имя пользователя/пароль",
|
||||
"randomAlias": "Случайный псевдоним",
|
||||
"usernamePassword": "Имя пользователя/пароль",
|
||||
"createAndSaveAlias": "Создать и сохранить псевдоним",
|
||||
"createAndSaveCredential": "Создать и сохранить учетные данные",
|
||||
"randomIdentityDescription": "Сгенерировать случайную личность со случайным адресом электронной почты, доступным в AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Случайная личность со случайным адресом электронной почты",
|
||||
"manualCredentialDescription": "Укажите свой собственный адрес электронной почты и имя пользователя.",
|
||||
"manualCredentialDescriptionDropdown": "Ручной ввод имени пользователя и пароля",
|
||||
"failedToCreateIdentity": "Не удалось создать личность. Пожалуйста, попробуйте снова.",
|
||||
"enterEmailAndOrUsername": "Введите адрес электронной почты и/или имя пользователя",
|
||||
"autofillWithAliasVault": "Автозаполнение с помощью AliasVault",
|
||||
"generateRandomPassword": "Сгенерировать случайный пароль (скопировать в буфер обмена)",
|
||||
"generateNewPassword": "Сгенерировать новый пароль",
|
||||
"togglePasswordVisibility": "Переключение видимости пароля",
|
||||
"passwordCopiedToClipboard": "Пароль скопирован в буфер обмена",
|
||||
"enterEmailAndOrUsernameError": "Введите адрес электронной почты и/или имя пользователя",
|
||||
"openAliasVaultToUpgrade": "Откройте AliasVault для обновления",
|
||||
"vaultUpgradeRequired": "Требуется обновление хранилища.",
|
||||
"dismissPopup": "Закрыть окно"
|
||||
},
|
||||
"credentials": {
|
||||
"title": "Учетные данные",
|
||||
"addCredential": "Добавить учетные данные",
|
||||
"editCredential": "Редактировать учетные данные",
|
||||
"deleteCredential": "Удалить учетные данные",
|
||||
"credentialDetails": "Подробности учетных данных",
|
||||
"serviceName": "Название сервиса",
|
||||
"serviceNamePlaceholder": "например, Gmail, Facebook, Банк",
|
||||
"website": "Сайт",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "Имя пользователя",
|
||||
"usernamePlaceholder": "Введите имя пользователя",
|
||||
"password": "Пароль",
|
||||
"passwordPlaceholder": "Введите пароль",
|
||||
"generatePassword": "Сгенерировать пароль",
|
||||
"copyPassword": "Скопировать пароль",
|
||||
"showPassword": "Показать пароль",
|
||||
"hidePassword": "Скрыть пароль",
|
||||
"notes": "Заметки",
|
||||
"notesPlaceholder": "Дополнительные заметки...",
|
||||
"totp": "Двухфакторная аутентификация",
|
||||
"totpCode": "TOTP код",
|
||||
"copyTotp": "Скопировать TOTP",
|
||||
"totpSecret": "TOTP секрет",
|
||||
"totpSecretPlaceholder": "Введите секретный ключ TOTP",
|
||||
"noCredentials": "Учетные данные не найдены",
|
||||
"noCredentialsDescription": "Добавьте свои первые учетные данные, чтобы начать работу",
|
||||
"searchPlaceholder": "Поиск учетных данных...",
|
||||
"welcomeTitle": "Добро пожаловать в AliasVault!",
|
||||
"welcomeDescription": "Чтобы использовать браузерное расширение AliasVault: перейдите на сайт и используйте всплывающее окно автозаполнения AliasVault для создания новых учетных данных.",
|
||||
"createdAt": "Создан",
|
||||
"updatedAt": "Последнее обновление",
|
||||
"autofill": "Автозаполнение",
|
||||
"fillForm": "Заполнить форму",
|
||||
"deleteConfirm": "Вы уверены, что хотите удалить эти учетные данные?",
|
||||
"saveSuccess": "Учетные данные успешно сохранены",
|
||||
"tags": "Теги",
|
||||
"addTag": "Добавить тег",
|
||||
"removeTag": "Удалить тег",
|
||||
"folder": "Папка",
|
||||
"selectFolder": "Выбрать папку",
|
||||
"createFolder": "Создать папку",
|
||||
"saveCredential": "Сохранить учетные данные",
|
||||
"deleteCredentialTitle": "Удалить учетные данные",
|
||||
"deleteCredentialConfirm": "Вы уверены, что хотите удалить эти учетные данные? Это действие невозможно отменить.",
|
||||
"randomAlias": "Случайный псевдоним",
|
||||
"manual": "Инструкция",
|
||||
"service": "Сервис",
|
||||
"serviceUrl": "URL сервиса",
|
||||
"loginCredentials": "Учетные данные для авторизации",
|
||||
"generateRandomUsername": "Сгенерировать случайное имя пользователя",
|
||||
"generateRandomPassword": "Сгенерировать случайный пароль",
|
||||
"changePasswordComplexity": "Изменить сложность пароля",
|
||||
"passwordLength": "Длина пароля",
|
||||
"includeLowercase": "Включить строчные буквы",
|
||||
"includeUppercase": "Включить заглавные буквы",
|
||||
"includeNumbers": "Включить числа",
|
||||
"includeSpecialChars": "Включить специальные символы",
|
||||
"avoidAmbiguousChars": "Избегать двусмысленных символов (o, 0 и т.д.).",
|
||||
"generateNewPreview": "Создать новый предварительный просмотр",
|
||||
"generateRandomAlias": "Сгенерировать случайный псевдоним",
|
||||
"clearAliasFields": "Очистить поля псевдонимов",
|
||||
"alias": "Псевдоним",
|
||||
"firstName": "Имя",
|
||||
"lastName": "Фамилия",
|
||||
"nickName": "Никнейм",
|
||||
"gender": "Пол",
|
||||
"birthDate": "Дата рождения",
|
||||
"birthDatePlaceholder": "ГГГГ-ММ-ДД",
|
||||
"metadata": "Метаданные",
|
||||
"validation": {
|
||||
"required": "Это поле является обязательным",
|
||||
"serviceNameRequired": "Требуется указать название сервиса",
|
||||
"invalidEmail": "Неверный формат электронной почты",
|
||||
"invalidDateFormat": "Дата должна быть указана в формате ГГГГ-ММ-ДД"
|
||||
},
|
||||
"privateEmailTitle": "Личная электронная почта",
|
||||
"privateEmailAliasVaultServer": "Сервер AliasVault",
|
||||
"privateEmailDescription": "Шифрование E2E, полностью приватный.",
|
||||
"publicEmailTitle": "Общедоступные временные поставщики электронной почты",
|
||||
"publicEmailDescription": "Анонимность, но ограниченная конфиденциальность. Содержимое письма может прочитать любой, кому известен адрес.",
|
||||
"useDomainChooser": "Использовать выбор домена",
|
||||
"enterCustomDomain": "Ввести пользовательский домен",
|
||||
"enterFullEmail": "Введите полный адрес электронной почты",
|
||||
"enterEmailPrefix": "Введите префикс электронной почты"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Письма",
|
||||
"deleteEmailTitle": "Удалить письмо",
|
||||
"deleteEmailConfirm": "Вы уверены, что хотите навсегда удалить это письмо?",
|
||||
"from": "От",
|
||||
"to": "К",
|
||||
"date": "Дата",
|
||||
"emailContent": "Содержимое письма",
|
||||
"attachments": "Вложения",
|
||||
"emailNotFound": "Адрес электронной почты не найден",
|
||||
"noEmails": "Электронные письма не найдены",
|
||||
"noEmailsDescription": "Вы еще не получали никаких электронных писем на свои личные адреса электронной почты. Когда вы получите новое электронное письмо, оно появится здесь.",
|
||||
"dateFormat": {
|
||||
"justNow": "прямо сейчас",
|
||||
"minutesAgo_single": "{{count}} мин назад",
|
||||
"minutesAgo_plural": "{{count}} минут назад",
|
||||
"hoursAgo_single": "{{count}} часов назад",
|
||||
"hoursAgo_plural": "{{count}} часов назад",
|
||||
"yesterday": "вчера"
|
||||
},
|
||||
"errors": {
|
||||
"emailLoadError": "Произошла ошибка при загрузке писем. Пожалуйста, повторите попытку позже.",
|
||||
"emailUnexpectedError": "При загрузке писем произошла непредвиденная ошибка. Пожалуйста, повторите попытку позже."
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "Текущий выбранный адрес электронной почты уже используется. Пожалуйста, измените адрес электронной почты, отредактировав эти учетные данные.",
|
||||
"CLAIM_DOES_NOT_EXIST": "При попытке загрузить письма произошла ошибка. Пожалуйста, попробуйте отредактировать и сохранить данные для синхронизации базы данных, затем повторите попытку."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Настройки",
|
||||
"serverUrl": "URL-адрес сервера",
|
||||
"language": "Язык",
|
||||
"autofillEnabled": "Включить автозаполнение",
|
||||
"version": "Версия",
|
||||
"openInNewWindow": "Открыть в новом окне",
|
||||
"openWebApp": "Открыть веб-приложение",
|
||||
"loggedIn": "Вход выполнен",
|
||||
"logout": "Выйти",
|
||||
"globalSettings": "Глобальные настройки",
|
||||
"autofillPopup": "Всплывающее окно автозаполнения",
|
||||
"activeOnAllSites": "Активен на всех сайтах (если не отключен ниже)",
|
||||
"disabledOnAllSites": "Отключено на всех сайтах",
|
||||
"enabled": "Включен",
|
||||
"disabled": "Выключен",
|
||||
"rightClickContextMenu": "Контекстное меню правым щелчком мыши",
|
||||
"autofillMatching": "Соответствие автозаполнения",
|
||||
"autofillMatchingMode": "Режим сопоставления автозаполнения",
|
||||
"autofillMatchingModeDescription": "Определяет, какие учетные данные считаются соответствующими и отображаются в качестве предложений во всплывающем окне автозаполнения для данного веб-сайта.",
|
||||
"autofillMatchingDefault": "URL + поддомен + подстановочный знак в названии",
|
||||
"autofillMatchingUrlSubdomain": "URL + поддомен",
|
||||
"autofillMatchingUrlExact": "Только точный URL-адрес домена",
|
||||
"siteSpecificSettings": "Настройки для конкретного сайта",
|
||||
"autofillPopupOn": "Всплывающее окно автозаполнения: ",
|
||||
"enabledForThisSite": "Включено для этого сайта",
|
||||
"disabledForThisSite": "Отключено для этого сайта",
|
||||
"temporarilyDisabledUntil": "Временно отключен до тех пор, пока",
|
||||
"resetAllSiteSettings": "Сбросить все настройки для сайтов",
|
||||
"appearance": "Внешний вид",
|
||||
"theme": "Тема",
|
||||
"useDefault": "Использовать по умолчанию",
|
||||
"light": "Светлая",
|
||||
"dark": "Темная",
|
||||
"keyboardShortcuts": "Горячие клавиши",
|
||||
"configureKeyboardShortcuts": "Настройка горячих клавиш",
|
||||
"configure": "Настройка",
|
||||
"security": "Безопасность",
|
||||
"clipboardClearTimeout": "Очистить буфер обмена после копирования",
|
||||
"clipboardClearTimeoutDescription": "Автоматическая очистка буфера обмена после копирования конфиденциальных данных",
|
||||
"clipboardClearDisabled": "Никогда не очищать",
|
||||
"clipboardClear5Seconds": "Очистка через 5 секунд",
|
||||
"clipboardClear10Seconds": "Очистка через 10 секунд",
|
||||
"clipboardClear15Seconds": "Очистка через 15 секунд",
|
||||
"autoLockTimeout": "Тайм-аут автоматической блокировки",
|
||||
"autoLockTimeoutDescription": "Автоматическая блокировка хранилища после некоторого периода бездействия",
|
||||
"autoLockTimeoutHelp": "Хранилище будет заблокировано только по истечении указанного периода бездействия (не будет использоваться функция автозаполнения или не откроется всплывающее окно с расширением). Хранилище всегда будет заблокировано при закрытии браузера, независимо от этого параметра.",
|
||||
"autoLockNever": "Никогда",
|
||||
"autoLock15Seconds": "15 секунд",
|
||||
"autoLock1Minute": "1 минута",
|
||||
"autoLock5Minutes": "5 минут",
|
||||
"autoLock15Minutes": "15 минут",
|
||||
"autoLock30Minutes": "30 минут",
|
||||
"autoLock1Hour": "1 час",
|
||||
"autoLock4Hours": "4 часа",
|
||||
"autoLock8Hours": "8 часов",
|
||||
"autoLock24Hours": "24 часов",
|
||||
"versionPrefix": "Версия ",
|
||||
"preferences": "Предпочтения",
|
||||
"autofillSettings": "Настройки автозаполнения",
|
||||
"clipboardSettings": "Настройки буфера обмена",
|
||||
"contextMenuSettings": "Настройки контекстного меню",
|
||||
"contextMenu": "Контекстное меню",
|
||||
"contextMenuEnabled": "Контекстное меню включено",
|
||||
"contextMenuDisabled": "Контекстное меню отключено",
|
||||
"contextMenuDescription": "Щелкните правой кнопкой мыши на полях ввода, чтобы получить доступ к параметрам AliasVault",
|
||||
"selectLanguage": "Выбрать язык",
|
||||
"validation": {
|
||||
"apiUrlRequired": "Требуется URL-адрес API",
|
||||
"apiUrlInvalid": "Пожалуйста, введите корректный URL-адрес API",
|
||||
"clientUrlRequired": "Требуется URL-адрес клиента",
|
||||
"clientUrlInvalid": "Пожалуйста, введите корректный URL-адрес клиента"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "Обновить хранилище",
|
||||
"subtitle": "AliasVault обновился, и ваше хранилище необходимо обновить. Это займет всего несколько секунд.",
|
||||
"versionInformation": "Информация о версии",
|
||||
"yourVault": "Ваше хранилище:",
|
||||
"newVersion": "Новая версия:",
|
||||
"upgrade": "Обновить хранилище",
|
||||
"upgrading": "Обновление...",
|
||||
"logout": "Выйти",
|
||||
"whatsNew": "Что нового",
|
||||
"whatsNewDescription": "Для поддержки следующих изменений требуется обновление:",
|
||||
"noDescriptionAvailable": "Описание для этой версии недоступно.",
|
||||
"okay": "ОК",
|
||||
"status": {
|
||||
"preparingUpgrade": "Подготовка обновления...",
|
||||
"vaultAlreadyUpToDate": "Хранилище уже обновлено",
|
||||
"startingDatabaseTransaction": "Запуск операции с базой данных...",
|
||||
"applyingDatabaseMigrations": "Применение перемещения базы данных...",
|
||||
"applyingMigration": "Применяя перемещение {{current}} из {{total}}...",
|
||||
"committingChanges": "Фиксация изменений..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Ошибка",
|
||||
"unableToGetVersionInfo": "Не удалось получить информацию о версии. Пожалуйста, попробуйте снова.",
|
||||
"selfHostedServer": "Автономный сервер",
|
||||
"selfHostedWarning": "Если вы используете автономный сервер, обязательно обновите свой автономный экземпляр, так как в противном случае вход в веб-клиент перестанет работать.",
|
||||
"cancel": "Отменить",
|
||||
"continueUpgrade": "Продолжить обновление",
|
||||
"upgradeFailed": "Ошибка обновления",
|
||||
"failedToApplyMigration": "Не удалось применить перенос ({{current}} из {{total}})",
|
||||
"unknownErrorDuringUpgrade": "Во время обновления произошла неизвестная ошибка. Пожалуйста, попробуйте снова."
|
||||
}
|
||||
}
|
||||
}
|
||||
393
apps/browser-extension/src/i18n/locales/sv.json
Normal file
393
apps/browser-extension/src/i18n/locales/sv.json
Normal file
@@ -0,0 +1,393 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "Log in to AliasVault",
|
||||
"username": "Username or email",
|
||||
"usernamePlaceholder": "name / name@company.com",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"rememberMe": "Remember me",
|
||||
"loginButton": "Login",
|
||||
"noAccount": "No account yet?",
|
||||
"createVault": "Create new vault",
|
||||
"twoFactorTitle": "Please enter the authentication code from your authenticator app.",
|
||||
"authCode": "Authentication Code",
|
||||
"authCodePlaceholder": "Enter 6-digit code",
|
||||
"verify": "Verify",
|
||||
"cancel": "Cancel",
|
||||
"twoFactorNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.",
|
||||
"masterPassword": "Master Password",
|
||||
"unlockVault": "Unlock Vault",
|
||||
"unlockTitle": "Unlock Your Vault",
|
||||
"unlockDescription": "Enter your master password to unlock your vault.",
|
||||
"logout": "Logout",
|
||||
"logoutConfirm": "Are you sure you want to logout?",
|
||||
"sessionExpired": "Your session has expired. Please log in again.",
|
||||
"unlockSuccess": "Vault unlocked successfully!",
|
||||
"unlockSuccessTitle": "Your vault is successfully unlocked",
|
||||
"unlockSuccessDescription": "You can now use autofill in login forms in your browser.",
|
||||
"closePopup": "Close this popup",
|
||||
"browseVault": "Browse vault contents",
|
||||
"connectingTo": "Connecting to",
|
||||
"switchAccounts": "Switch accounts?",
|
||||
"loggedIn": "Logged in",
|
||||
"errors": {
|
||||
"invalidCode": "Please enter a valid 6-digit authentication code.",
|
||||
"serverError": "Could not reach AliasVault server. Please try again later or contact support if the problem persists.",
|
||||
"noToken": "Login failed -- no token returned",
|
||||
"migrationError": "An error occurred while checking for pending migrations.",
|
||||
"wrongPassword": "Incorrect password. Please try again.",
|
||||
"accountLocked": "Account temporarily locked due to too many failed attempts.",
|
||||
"networkError": "Network error. Please check your connection and try again.",
|
||||
"loginDataMissing": "Login session expired. Please try again."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "Credentials",
|
||||
"emails": "Emails",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"cancel": "Cancel",
|
||||
"use": "Use",
|
||||
"delete": "Delete",
|
||||
"close": "Close",
|
||||
"copied": "Copied!",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"language": "Language",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"loadingEmails": "Loading emails...",
|
||||
"loadingTotpCodes": "Loading TOTP codes...",
|
||||
"attachments": "Attachments",
|
||||
"loadingAttachments": "Loading attachments...",
|
||||
"settings": "Settings",
|
||||
"recentEmails": "Recent emails",
|
||||
"loginCredentials": "Login credentials",
|
||||
"twoFactorAuthentication": "Two-factor authentication",
|
||||
"alias": "Alias",
|
||||
"notes": "Notes",
|
||||
"fullName": "Full Name",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"birthDate": "Birth Date",
|
||||
"nickname": "Nickname",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"syncingVault": "Syncing vault",
|
||||
"savingChangesToVault": "Saving changes to vault",
|
||||
"uploadingVaultToServer": "Uploading vault to server",
|
||||
"checkingVaultUpdates": "Checking for vault updates",
|
||||
"syncingUpdatedVault": "Syncing updated vault",
|
||||
"executingOperation": "Executing operation...",
|
||||
"loadMore": "Load more",
|
||||
"errors": {
|
||||
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
|
||||
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
|
||||
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
|
||||
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
|
||||
"unknownError": "An unknown error occurred",
|
||||
"failedToStoreVault": "Failed to store vault",
|
||||
"vaultNotAvailable": "Vault not available",
|
||||
"failedToRetrieveData": "Failed to retrieve data",
|
||||
"vaultIsLocked": "Vault is locked",
|
||||
"failedToUploadVault": "Failed to upload vault",
|
||||
"passwordChanged": "Your password has changed since the last time you logged in. Please login again for security reasons."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",
|
||||
"ACCOUNT_LOCKED": "Account temporarily locked due to too many failed attempts. Please try again later.",
|
||||
"ACCOUNT_BLOCKED": "Your account has been disabled. If you believe this is a mistake, please contact support.",
|
||||
"USER_NOT_FOUND": "Invalid username or password. Please try again.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Invalid authenticator code. Please try again.",
|
||||
"INVALID_RECOVERY_CODE": "Invalid recovery code. Please try again.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Refresh token is required.",
|
||||
"INVALID_REFRESH_TOKEN": "Invalid refresh token.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token revoked successfully.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "New account registration is currently disabled on this server. Please contact the administrator.",
|
||||
"USERNAME_REQUIRED": "Username is required.",
|
||||
"USERNAME_ALREADY_IN_USE": "Username is already in use.",
|
||||
"USERNAME_AVAILABLE": "Username is available.",
|
||||
"USERNAME_MISMATCH": "Username does not match the current user.",
|
||||
"PASSWORD_MISMATCH": "The provided password does not match your current password.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Account successfully deleted.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Username cannot be empty or whitespace.",
|
||||
"USERNAME_TOO_SHORT": "Username too short: must be at least 3 characters long.",
|
||||
"USERNAME_TOO_LONG": "Username too long: cannot be longer than 40 characters.",
|
||||
"USERNAME_INVALID_EMAIL": "Invalid email address.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Username is invalid, can only contain letters or digits.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Your vault is not up-to-date. Please synchronize your vault and try again.",
|
||||
"INTERNAL_SERVER_ERROR": "Internal server error.",
|
||||
"VAULT_ERROR": "The local vault is not up-to-date. Please synchronize your vault by refreshing the page and try again."
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"or": "or",
|
||||
"new": "New",
|
||||
"cancel": "Cancel",
|
||||
"search": "Search",
|
||||
"vaultLocked": "AliasVault is locked.",
|
||||
"creatingNewAlias": "Creating new alias...",
|
||||
"noMatchesFound": "No matches found",
|
||||
"searchVault": "Search vault...",
|
||||
"serviceName": "Service name",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"enterServiceName": "Enter service name",
|
||||
"enterEmailAddress": "Enter email address",
|
||||
"enterUsername": "Enter username",
|
||||
"hideFor1Hour": "Hide for 1 hour (current site)",
|
||||
"hidePermanently": "Hide permanently (current site)",
|
||||
"createRandomAlias": "Create random alias",
|
||||
"createUsernamePassword": "Create username/password",
|
||||
"randomAlias": "Random alias",
|
||||
"usernamePassword": "Username/password",
|
||||
"createAndSaveAlias": "Create and save alias",
|
||||
"createAndSaveCredential": "Create and save credential",
|
||||
"randomIdentityDescription": "Generate a random identity with a random email address accessible in AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Random identity with random email",
|
||||
"manualCredentialDescription": "Specify your own email address and username.",
|
||||
"manualCredentialDescriptionDropdown": "Manual username and password",
|
||||
"failedToCreateIdentity": "Failed to create identity. Please try again.",
|
||||
"enterEmailAndOrUsername": "Enter email and/or username",
|
||||
"autofillWithAliasVault": "Autofill with AliasVault",
|
||||
"generateRandomPassword": "Generate random password (copy to clipboard)",
|
||||
"generateNewPassword": "Generate new password",
|
||||
"togglePasswordVisibility": "Toggle password visibility",
|
||||
"passwordCopiedToClipboard": "Password copied to clipboard",
|
||||
"enterEmailAndOrUsernameError": "Enter email and/or username",
|
||||
"openAliasVaultToUpgrade": "Open AliasVault to upgrade",
|
||||
"vaultUpgradeRequired": "Vault upgrade required.",
|
||||
"dismissPopup": "Dismiss popup"
|
||||
},
|
||||
"credentials": {
|
||||
"title": "Credentials",
|
||||
"addCredential": "Add Credential",
|
||||
"editCredential": "Edit Credential",
|
||||
"deleteCredential": "Delete Credential",
|
||||
"credentialDetails": "Credential Details",
|
||||
"serviceName": "Service Name",
|
||||
"serviceNamePlaceholder": "e.g., Gmail, Facebook, Bank",
|
||||
"website": "Website",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "Enter username",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"generatePassword": "Generate Password",
|
||||
"copyPassword": "Copy Password",
|
||||
"showPassword": "Show Password",
|
||||
"hidePassword": "Hide Password",
|
||||
"notes": "Notes",
|
||||
"notesPlaceholder": "Additional notes...",
|
||||
"totp": "Two-Factor Authentication",
|
||||
"totpCode": "TOTP Code",
|
||||
"copyTotp": "Copy TOTP",
|
||||
"totpSecret": "TOTP Secret",
|
||||
"totpSecretPlaceholder": "Enter TOTP secret key",
|
||||
"noCredentials": "No credentials found",
|
||||
"noCredentialsDescription": "Add your first credential to get started",
|
||||
"searchPlaceholder": "Search credentials...",
|
||||
"welcomeTitle": "Welcome to AliasVault!",
|
||||
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Fill Form",
|
||||
"deleteConfirm": "Are you sure you want to delete this credential?",
|
||||
"saveSuccess": "Credential saved successfully",
|
||||
"tags": "Tags",
|
||||
"addTag": "Add Tag",
|
||||
"removeTag": "Remove Tag",
|
||||
"folder": "Folder",
|
||||
"selectFolder": "Select Folder",
|
||||
"createFolder": "Create Folder",
|
||||
"saveCredential": "Save credential",
|
||||
"deleteCredentialTitle": "Delete Credential",
|
||||
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
|
||||
"randomAlias": "Random Alias",
|
||||
"manual": "Manual",
|
||||
"service": "Service",
|
||||
"serviceUrl": "Service URL",
|
||||
"loginCredentials": "Login Credentials",
|
||||
"generateRandomUsername": "Generate random username",
|
||||
"generateRandomPassword": "Generate random password",
|
||||
"changePasswordComplexity": "Change password complexity",
|
||||
"passwordLength": "Password length",
|
||||
"includeLowercase": "Include lowercase letters",
|
||||
"includeUppercase": "Include uppercase letters",
|
||||
"includeNumbers": "Include numbers",
|
||||
"includeSpecialChars": "Include special characters",
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"nickName": "Nick Name",
|
||||
"gender": "Gender",
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidEmail": "Invalid email format",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
|
||||
},
|
||||
"privateEmailTitle": "Private Email",
|
||||
"privateEmailAliasVaultServer": "AliasVault server",
|
||||
"privateEmailDescription": "E2E encrypted, fully private.",
|
||||
"publicEmailTitle": "Public Temp Email Providers",
|
||||
"publicEmailDescription": "Anonymous but limited privacy. Email content is readable by anyone that knows the address.",
|
||||
"useDomainChooser": "Use domain chooser",
|
||||
"enterCustomDomain": "Enter custom domain",
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
"deleteEmailTitle": "Delete Email",
|
||||
"deleteEmailConfirm": "Are you sure you want to permanently delete this email?",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"date": "Date",
|
||||
"emailContent": "Email content",
|
||||
"attachments": "Attachments",
|
||||
"emailNotFound": "Email not found",
|
||||
"noEmails": "No emails found",
|
||||
"noEmailsDescription": "You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.",
|
||||
"dateFormat": {
|
||||
"justNow": "just now",
|
||||
"minutesAgo_single": "{{count}} min ago",
|
||||
"minutesAgo_plural": "{{count}} mins ago",
|
||||
"hoursAgo_single": "{{count}} hr ago",
|
||||
"hoursAgo_plural": "{{count}} hrs ago",
|
||||
"yesterday": "yesterday"
|
||||
},
|
||||
"errors": {
|
||||
"emailLoadError": "An error occurred while loading emails. Please try again later.",
|
||||
"emailUnexpectedError": "An unexpected error occurred while loading emails. Please try again later."
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "The current chosen email address is already in use. Please change the email address by editing this credential.",
|
||||
"CLAIM_DOES_NOT_EXIST": "An error occurred while trying to load the emails. Please try to edit and save the credential entry to synchronize the database, then try again."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"serverUrl": "Server URL",
|
||||
"language": "Language",
|
||||
"autofillEnabled": "Enable Autofill",
|
||||
"version": "Version",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"openWebApp": "Open web app",
|
||||
"loggedIn": "Logged in",
|
||||
"logout": "Logout",
|
||||
"globalSettings": "Global Settings",
|
||||
"autofillPopup": "Autofill popup",
|
||||
"activeOnAllSites": "Active on all sites (unless disabled below)",
|
||||
"disabledOnAllSites": "Disabled on all sites",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"rightClickContextMenu": "Right-click context menu",
|
||||
"autofillMatching": "Autofill Matching",
|
||||
"autofillMatchingMode": "Autofill matching mode",
|
||||
"autofillMatchingModeDescription": "Determines which credentials are considered a match and shown as suggestions in the autofill popup for a given website.",
|
||||
"autofillMatchingDefault": "URL + subdomain + name wildcard",
|
||||
"autofillMatchingUrlSubdomain": "URL + subdomain",
|
||||
"autofillMatchingUrlExact": "Exact URL domain only",
|
||||
"siteSpecificSettings": "Site-Specific Settings",
|
||||
"autofillPopupOn": "Autofill popup on: ",
|
||||
"enabledForThisSite": "Enabled for this site",
|
||||
"disabledForThisSite": "Disabled for this site",
|
||||
"temporarilyDisabledUntil": "Temporarily disabled until ",
|
||||
"resetAllSiteSettings": "Reset all site-specific settings",
|
||||
"appearance": "Appearance",
|
||||
"theme": "Theme",
|
||||
"useDefault": "Use default",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||
"configureKeyboardShortcuts": "Configure keyboard shortcuts",
|
||||
"configure": "Configure",
|
||||
"security": "Security",
|
||||
"clipboardClearTimeout": "Clear clipboard after copying",
|
||||
"clipboardClearTimeoutDescription": "Automatically clear the clipboard after copying sensitive data",
|
||||
"clipboardClearDisabled": "Never clear",
|
||||
"clipboardClear5Seconds": "Clear after 5 seconds",
|
||||
"clipboardClear10Seconds": "Clear after 10 seconds",
|
||||
"clipboardClear15Seconds": "Clear after 15 seconds",
|
||||
"autoLockTimeout": "Auto-lock Timeout",
|
||||
"autoLockTimeoutDescription": "Automatically lock the vault after a period of inactivity",
|
||||
"autoLockTimeoutHelp": "The vault will only lock after the specified period of inactivity (no autofill usage or extension popup opened). The vault will always lock when the browser is closed, regardless of this setting.",
|
||||
"autoLockNever": "Never",
|
||||
"autoLock15Seconds": "15 seconds",
|
||||
"autoLock1Minute": "1 minute",
|
||||
"autoLock5Minutes": "5 minutes",
|
||||
"autoLock15Minutes": "15 minutes",
|
||||
"autoLock30Minutes": "30 minutes",
|
||||
"autoLock1Hour": "1 hour",
|
||||
"autoLock4Hours": "4 hours",
|
||||
"autoLock8Hours": "8 hours",
|
||||
"autoLock24Hours": "24 hours",
|
||||
"versionPrefix": "Version ",
|
||||
"preferences": "Preferences",
|
||||
"autofillSettings": "Autofill Settings",
|
||||
"clipboardSettings": "Clipboard Settings",
|
||||
"contextMenuSettings": "Context Menu Settings",
|
||||
"contextMenu": "Context Menu",
|
||||
"contextMenuEnabled": "Context menu is enabled",
|
||||
"contextMenuDisabled": "Context menu is disabled",
|
||||
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
|
||||
"selectLanguage": "Select Language",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL is required",
|
||||
"apiUrlInvalid": "Please enter a valid API URL",
|
||||
"clientUrlRequired": "Client URL is required",
|
||||
"clientUrlInvalid": "Please enter a valid client URL"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "Upgrade Vault",
|
||||
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
|
||||
"versionInformation": "Version Information",
|
||||
"yourVault": "Your vault:",
|
||||
"newVersion": "New version:",
|
||||
"upgrade": "Upgrade Vault",
|
||||
"upgrading": "Upgrading...",
|
||||
"logout": "Logout",
|
||||
"whatsNew": "What's New",
|
||||
"whatsNewDescription": "An upgrade is required to support the following changes:",
|
||||
"noDescriptionAvailable": "No description available for this version.",
|
||||
"okay": "Ok",
|
||||
"status": {
|
||||
"preparingUpgrade": "Preparing upgrade...",
|
||||
"vaultAlreadyUpToDate": "Vault is already up to date",
|
||||
"startingDatabaseTransaction": "Starting database transaction...",
|
||||
"applyingDatabaseMigrations": "Applying database migrations...",
|
||||
"applyingMigration": "Applying migration {{current}} of {{total}}...",
|
||||
"committingChanges": "Committing changes..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Error",
|
||||
"unableToGetVersionInfo": "Unable to get version information. Please try again.",
|
||||
"selfHostedServer": "Self-Hosted Server",
|
||||
"selfHostedWarning": "If you're using a self-hosted server, make sure to also update your self-hosted instance as otherwise logging in to the web client will stop working.",
|
||||
"cancel": "Cancel",
|
||||
"continueUpgrade": "Continue Upgrade",
|
||||
"upgradeFailed": "Upgrade Failed",
|
||||
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
|
||||
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
393
apps/browser-extension/src/i18n/locales/tr.json
Normal file
393
apps/browser-extension/src/i18n/locales/tr.json
Normal file
@@ -0,0 +1,393 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "Log in to AliasVault",
|
||||
"username": "Username or email",
|
||||
"usernamePlaceholder": "name / name@company.com",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"rememberMe": "Remember me",
|
||||
"loginButton": "Login",
|
||||
"noAccount": "No account yet?",
|
||||
"createVault": "Create new vault",
|
||||
"twoFactorTitle": "Please enter the authentication code from your authenticator app.",
|
||||
"authCode": "Authentication Code",
|
||||
"authCodePlaceholder": "Enter 6-digit code",
|
||||
"verify": "Verify",
|
||||
"cancel": "Cancel",
|
||||
"twoFactorNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.",
|
||||
"masterPassword": "Master Password",
|
||||
"unlockVault": "Unlock Vault",
|
||||
"unlockTitle": "Unlock Your Vault",
|
||||
"unlockDescription": "Enter your master password to unlock your vault.",
|
||||
"logout": "Logout",
|
||||
"logoutConfirm": "Are you sure you want to logout?",
|
||||
"sessionExpired": "Your session has expired. Please log in again.",
|
||||
"unlockSuccess": "Vault unlocked successfully!",
|
||||
"unlockSuccessTitle": "Your vault is successfully unlocked",
|
||||
"unlockSuccessDescription": "You can now use autofill in login forms in your browser.",
|
||||
"closePopup": "Close this popup",
|
||||
"browseVault": "Browse vault contents",
|
||||
"connectingTo": "Connecting to",
|
||||
"switchAccounts": "Switch accounts?",
|
||||
"loggedIn": "Logged in",
|
||||
"errors": {
|
||||
"invalidCode": "Please enter a valid 6-digit authentication code.",
|
||||
"serverError": "Could not reach AliasVault server. Please try again later or contact support if the problem persists.",
|
||||
"noToken": "Login failed -- no token returned",
|
||||
"migrationError": "An error occurred while checking for pending migrations.",
|
||||
"wrongPassword": "Incorrect password. Please try again.",
|
||||
"accountLocked": "Account temporarily locked due to too many failed attempts.",
|
||||
"networkError": "Network error. Please check your connection and try again.",
|
||||
"loginDataMissing": "Login session expired. Please try again."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "Credentials",
|
||||
"emails": "Emails",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"cancel": "Cancel",
|
||||
"use": "Use",
|
||||
"delete": "Delete",
|
||||
"close": "Close",
|
||||
"copied": "Copied!",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"language": "Language",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"loadingEmails": "Loading emails...",
|
||||
"loadingTotpCodes": "Loading TOTP codes...",
|
||||
"attachments": "Attachments",
|
||||
"loadingAttachments": "Loading attachments...",
|
||||
"settings": "Settings",
|
||||
"recentEmails": "Recent emails",
|
||||
"loginCredentials": "Login credentials",
|
||||
"twoFactorAuthentication": "Two-factor authentication",
|
||||
"alias": "Alias",
|
||||
"notes": "Notes",
|
||||
"fullName": "Full Name",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"birthDate": "Birth Date",
|
||||
"nickname": "Nickname",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"syncingVault": "Syncing vault",
|
||||
"savingChangesToVault": "Saving changes to vault",
|
||||
"uploadingVaultToServer": "Uploading vault to server",
|
||||
"checkingVaultUpdates": "Checking for vault updates",
|
||||
"syncingUpdatedVault": "Syncing updated vault",
|
||||
"executingOperation": "Executing operation...",
|
||||
"loadMore": "Load more",
|
||||
"errors": {
|
||||
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
|
||||
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
|
||||
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
|
||||
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
|
||||
"unknownError": "An unknown error occurred",
|
||||
"failedToStoreVault": "Failed to store vault",
|
||||
"vaultNotAvailable": "Vault not available",
|
||||
"failedToRetrieveData": "Failed to retrieve data",
|
||||
"vaultIsLocked": "Vault is locked",
|
||||
"failedToUploadVault": "Failed to upload vault",
|
||||
"passwordChanged": "Your password has changed since the last time you logged in. Please login again for security reasons."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",
|
||||
"ACCOUNT_LOCKED": "Account temporarily locked due to too many failed attempts. Please try again later.",
|
||||
"ACCOUNT_BLOCKED": "Your account has been disabled. If you believe this is a mistake, please contact support.",
|
||||
"USER_NOT_FOUND": "Invalid username or password. Please try again.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Invalid authenticator code. Please try again.",
|
||||
"INVALID_RECOVERY_CODE": "Invalid recovery code. Please try again.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Refresh token is required.",
|
||||
"INVALID_REFRESH_TOKEN": "Invalid refresh token.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token revoked successfully.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "New account registration is currently disabled on this server. Please contact the administrator.",
|
||||
"USERNAME_REQUIRED": "Username is required.",
|
||||
"USERNAME_ALREADY_IN_USE": "Username is already in use.",
|
||||
"USERNAME_AVAILABLE": "Username is available.",
|
||||
"USERNAME_MISMATCH": "Username does not match the current user.",
|
||||
"PASSWORD_MISMATCH": "The provided password does not match your current password.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Account successfully deleted.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Username cannot be empty or whitespace.",
|
||||
"USERNAME_TOO_SHORT": "Username too short: must be at least 3 characters long.",
|
||||
"USERNAME_TOO_LONG": "Username too long: cannot be longer than 40 characters.",
|
||||
"USERNAME_INVALID_EMAIL": "Invalid email address.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Username is invalid, can only contain letters or digits.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Your vault is not up-to-date. Please synchronize your vault and try again.",
|
||||
"INTERNAL_SERVER_ERROR": "Internal server error.",
|
||||
"VAULT_ERROR": "The local vault is not up-to-date. Please synchronize your vault by refreshing the page and try again."
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"or": "or",
|
||||
"new": "New",
|
||||
"cancel": "Cancel",
|
||||
"search": "Search",
|
||||
"vaultLocked": "AliasVault is locked.",
|
||||
"creatingNewAlias": "Creating new alias...",
|
||||
"noMatchesFound": "No matches found",
|
||||
"searchVault": "Search vault...",
|
||||
"serviceName": "Service name",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"enterServiceName": "Enter service name",
|
||||
"enterEmailAddress": "Enter email address",
|
||||
"enterUsername": "Enter username",
|
||||
"hideFor1Hour": "Hide for 1 hour (current site)",
|
||||
"hidePermanently": "Hide permanently (current site)",
|
||||
"createRandomAlias": "Create random alias",
|
||||
"createUsernamePassword": "Create username/password",
|
||||
"randomAlias": "Random alias",
|
||||
"usernamePassword": "Username/password",
|
||||
"createAndSaveAlias": "Create and save alias",
|
||||
"createAndSaveCredential": "Create and save credential",
|
||||
"randomIdentityDescription": "Generate a random identity with a random email address accessible in AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Random identity with random email",
|
||||
"manualCredentialDescription": "Specify your own email address and username.",
|
||||
"manualCredentialDescriptionDropdown": "Manual username and password",
|
||||
"failedToCreateIdentity": "Failed to create identity. Please try again.",
|
||||
"enterEmailAndOrUsername": "Enter email and/or username",
|
||||
"autofillWithAliasVault": "Autofill with AliasVault",
|
||||
"generateRandomPassword": "Generate random password (copy to clipboard)",
|
||||
"generateNewPassword": "Generate new password",
|
||||
"togglePasswordVisibility": "Toggle password visibility",
|
||||
"passwordCopiedToClipboard": "Password copied to clipboard",
|
||||
"enterEmailAndOrUsernameError": "Enter email and/or username",
|
||||
"openAliasVaultToUpgrade": "Open AliasVault to upgrade",
|
||||
"vaultUpgradeRequired": "Vault upgrade required.",
|
||||
"dismissPopup": "Dismiss popup"
|
||||
},
|
||||
"credentials": {
|
||||
"title": "Credentials",
|
||||
"addCredential": "Add Credential",
|
||||
"editCredential": "Edit Credential",
|
||||
"deleteCredential": "Delete Credential",
|
||||
"credentialDetails": "Credential Details",
|
||||
"serviceName": "Service Name",
|
||||
"serviceNamePlaceholder": "e.g., Gmail, Facebook, Bank",
|
||||
"website": "Website",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "Enter username",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"generatePassword": "Generate Password",
|
||||
"copyPassword": "Copy Password",
|
||||
"showPassword": "Show Password",
|
||||
"hidePassword": "Hide Password",
|
||||
"notes": "Notes",
|
||||
"notesPlaceholder": "Additional notes...",
|
||||
"totp": "Two-Factor Authentication",
|
||||
"totpCode": "TOTP Code",
|
||||
"copyTotp": "Copy TOTP",
|
||||
"totpSecret": "TOTP Secret",
|
||||
"totpSecretPlaceholder": "Enter TOTP secret key",
|
||||
"noCredentials": "No credentials found",
|
||||
"noCredentialsDescription": "Add your first credential to get started",
|
||||
"searchPlaceholder": "Search credentials...",
|
||||
"welcomeTitle": "Welcome to AliasVault!",
|
||||
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Fill Form",
|
||||
"deleteConfirm": "Are you sure you want to delete this credential?",
|
||||
"saveSuccess": "Credential saved successfully",
|
||||
"tags": "Tags",
|
||||
"addTag": "Add Tag",
|
||||
"removeTag": "Remove Tag",
|
||||
"folder": "Folder",
|
||||
"selectFolder": "Select Folder",
|
||||
"createFolder": "Create Folder",
|
||||
"saveCredential": "Save credential",
|
||||
"deleteCredentialTitle": "Delete Credential",
|
||||
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
|
||||
"randomAlias": "Random Alias",
|
||||
"manual": "Manual",
|
||||
"service": "Service",
|
||||
"serviceUrl": "Service URL",
|
||||
"loginCredentials": "Login Credentials",
|
||||
"generateRandomUsername": "Generate random username",
|
||||
"generateRandomPassword": "Generate random password",
|
||||
"changePasswordComplexity": "Change password complexity",
|
||||
"passwordLength": "Password length",
|
||||
"includeLowercase": "Include lowercase letters",
|
||||
"includeUppercase": "Include uppercase letters",
|
||||
"includeNumbers": "Include numbers",
|
||||
"includeSpecialChars": "Include special characters",
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"nickName": "Nick Name",
|
||||
"gender": "Gender",
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidEmail": "Invalid email format",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
|
||||
},
|
||||
"privateEmailTitle": "Private Email",
|
||||
"privateEmailAliasVaultServer": "AliasVault server",
|
||||
"privateEmailDescription": "E2E encrypted, fully private.",
|
||||
"publicEmailTitle": "Public Temp Email Providers",
|
||||
"publicEmailDescription": "Anonymous but limited privacy. Email content is readable by anyone that knows the address.",
|
||||
"useDomainChooser": "Use domain chooser",
|
||||
"enterCustomDomain": "Enter custom domain",
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
"deleteEmailTitle": "Delete Email",
|
||||
"deleteEmailConfirm": "Are you sure you want to permanently delete this email?",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"date": "Date",
|
||||
"emailContent": "Email content",
|
||||
"attachments": "Attachments",
|
||||
"emailNotFound": "Email not found",
|
||||
"noEmails": "No emails found",
|
||||
"noEmailsDescription": "You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.",
|
||||
"dateFormat": {
|
||||
"justNow": "just now",
|
||||
"minutesAgo_single": "{{count}} min ago",
|
||||
"minutesAgo_plural": "{{count}} mins ago",
|
||||
"hoursAgo_single": "{{count}} hr ago",
|
||||
"hoursAgo_plural": "{{count}} hrs ago",
|
||||
"yesterday": "yesterday"
|
||||
},
|
||||
"errors": {
|
||||
"emailLoadError": "An error occurred while loading emails. Please try again later.",
|
||||
"emailUnexpectedError": "An unexpected error occurred while loading emails. Please try again later."
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "The current chosen email address is already in use. Please change the email address by editing this credential.",
|
||||
"CLAIM_DOES_NOT_EXIST": "An error occurred while trying to load the emails. Please try to edit and save the credential entry to synchronize the database, then try again."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"serverUrl": "Server URL",
|
||||
"language": "Language",
|
||||
"autofillEnabled": "Enable Autofill",
|
||||
"version": "Version",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"openWebApp": "Open web app",
|
||||
"loggedIn": "Logged in",
|
||||
"logout": "Logout",
|
||||
"globalSettings": "Global Settings",
|
||||
"autofillPopup": "Autofill popup",
|
||||
"activeOnAllSites": "Active on all sites (unless disabled below)",
|
||||
"disabledOnAllSites": "Disabled on all sites",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"rightClickContextMenu": "Right-click context menu",
|
||||
"autofillMatching": "Autofill Matching",
|
||||
"autofillMatchingMode": "Autofill matching mode",
|
||||
"autofillMatchingModeDescription": "Determines which credentials are considered a match and shown as suggestions in the autofill popup for a given website.",
|
||||
"autofillMatchingDefault": "URL + subdomain + name wildcard",
|
||||
"autofillMatchingUrlSubdomain": "URL + subdomain",
|
||||
"autofillMatchingUrlExact": "Exact URL domain only",
|
||||
"siteSpecificSettings": "Site-Specific Settings",
|
||||
"autofillPopupOn": "Autofill popup on: ",
|
||||
"enabledForThisSite": "Enabled for this site",
|
||||
"disabledForThisSite": "Disabled for this site",
|
||||
"temporarilyDisabledUntil": "Temporarily disabled until ",
|
||||
"resetAllSiteSettings": "Reset all site-specific settings",
|
||||
"appearance": "Appearance",
|
||||
"theme": "Theme",
|
||||
"useDefault": "Use default",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||
"configureKeyboardShortcuts": "Configure keyboard shortcuts",
|
||||
"configure": "Configure",
|
||||
"security": "Security",
|
||||
"clipboardClearTimeout": "Clear clipboard after copying",
|
||||
"clipboardClearTimeoutDescription": "Automatically clear the clipboard after copying sensitive data",
|
||||
"clipboardClearDisabled": "Never clear",
|
||||
"clipboardClear5Seconds": "Clear after 5 seconds",
|
||||
"clipboardClear10Seconds": "Clear after 10 seconds",
|
||||
"clipboardClear15Seconds": "Clear after 15 seconds",
|
||||
"autoLockTimeout": "Auto-lock Timeout",
|
||||
"autoLockTimeoutDescription": "Automatically lock the vault after a period of inactivity",
|
||||
"autoLockTimeoutHelp": "Kasa, yalnızca belirtilen süre boyunca herhangi bir işlem yapılmadığında (otomatik doldurma kullanılmadığında veya uzantı açılmadığında) kilitlenecektir. Ancak, bu ayardan bağımsız olarak tarayıcı kapatıldığında her zaman kilitlenir.",
|
||||
"autoLockNever": "Never",
|
||||
"autoLock15Seconds": "15 seconds",
|
||||
"autoLock1Minute": "1 minute",
|
||||
"autoLock5Minutes": "5 minutes",
|
||||
"autoLock15Minutes": "15 minutes",
|
||||
"autoLock30Minutes": "30 minutes",
|
||||
"autoLock1Hour": "1 hour",
|
||||
"autoLock4Hours": "4 hours",
|
||||
"autoLock8Hours": "8 hours",
|
||||
"autoLock24Hours": "24 hours",
|
||||
"versionPrefix": "Version ",
|
||||
"preferences": "Preferences",
|
||||
"autofillSettings": "Autofill Settings",
|
||||
"clipboardSettings": "Clipboard Settings",
|
||||
"contextMenuSettings": "Context Menu Settings",
|
||||
"contextMenu": "Context Menu",
|
||||
"contextMenuEnabled": "Context menu is enabled",
|
||||
"contextMenuDisabled": "Context menu is disabled",
|
||||
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
|
||||
"selectLanguage": "Select Language",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL is required",
|
||||
"apiUrlInvalid": "Please enter a valid API URL",
|
||||
"clientUrlRequired": "Client URL is required",
|
||||
"clientUrlInvalid": "Please enter a valid client URL"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "Upgrade Vault",
|
||||
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
|
||||
"versionInformation": "Version Information",
|
||||
"yourVault": "Your vault:",
|
||||
"newVersion": "New version:",
|
||||
"upgrade": "Upgrade Vault",
|
||||
"upgrading": "Upgrading...",
|
||||
"logout": "Logout",
|
||||
"whatsNew": "What's New",
|
||||
"whatsNewDescription": "An upgrade is required to support the following changes:",
|
||||
"noDescriptionAvailable": "No description available for this version.",
|
||||
"okay": "Ok",
|
||||
"status": {
|
||||
"preparingUpgrade": "Preparing upgrade...",
|
||||
"vaultAlreadyUpToDate": "Vault is already up to date",
|
||||
"startingDatabaseTransaction": "Starting database transaction...",
|
||||
"applyingDatabaseMigrations": "Applying database migrations...",
|
||||
"applyingMigration": "Applying migration {{current}} of {{total}}...",
|
||||
"committingChanges": "Committing changes..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Error",
|
||||
"unableToGetVersionInfo": "Unable to get version information. Please try again.",
|
||||
"selfHostedServer": "Self-Hosted Server",
|
||||
"selfHostedWarning": "If you're using a self-hosted server, make sure to also update your self-hosted instance as otherwise logging in to the web client will stop working.",
|
||||
"cancel": "Cancel",
|
||||
"continueUpgrade": "Continue Upgrade",
|
||||
"upgradeFailed": "Upgrade Failed",
|
||||
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
|
||||
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,375 +1,393 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "Log in to AliasVault",
|
||||
"username": "Username or email",
|
||||
"usernamePlaceholder": "name / name@company.com",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"rememberMe": "Remember me",
|
||||
"loginButton": "Login",
|
||||
"noAccount": "No account yet?",
|
||||
"createVault": "Create new vault",
|
||||
"twoFactorTitle": "Please enter the authentication code from your authenticator app.",
|
||||
"authCode": "Authentication Code",
|
||||
"authCodePlaceholder": "Enter 6-digit code",
|
||||
"verify": "Verify",
|
||||
"cancel": "Cancel",
|
||||
"twoFactorNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.",
|
||||
"masterPassword": "Master Password",
|
||||
"unlockVault": "Unlock Vault",
|
||||
"unlockTitle": "Unlock Your Vault",
|
||||
"unlockDescription": "Enter your master password to unlock your vault.",
|
||||
"logout": "Logout",
|
||||
"logoutConfirm": "Are you sure you want to logout?",
|
||||
"sessionExpired": "Your session has expired. Please log in again.",
|
||||
"unlockSuccess": "Vault unlocked successfully!",
|
||||
"unlockSuccessTitle": "Your vault is successfully unlocked",
|
||||
"unlockSuccessDescription": "You can now use autofill in login forms in your browser.",
|
||||
"closePopup": "Close this popup",
|
||||
"browseVault": "Browse vault contents",
|
||||
"connectingTo": "Connecting to",
|
||||
"switchAccounts": "Switch accounts?",
|
||||
"loggedIn": "Logged in",
|
||||
"loginTitle": "Увійти до AliasVault",
|
||||
"username": "Ім'я користувача або електронна пошта",
|
||||
"usernamePlaceholder": "ім'я / name@company.com",
|
||||
"password": "Пароль",
|
||||
"passwordPlaceholder": "Введіть ваш пароль",
|
||||
"rememberMe": "Запам'ятати мене",
|
||||
"loginButton": "Увійти",
|
||||
"noAccount": "Ще не маєте облікового запису?",
|
||||
"createVault": "Створити нове сховище",
|
||||
"twoFactorTitle": "Будь ласка, введіть код автентифікації з вашого застосунку для автентифікації.",
|
||||
"authCode": "Код автентифікації",
|
||||
"authCodePlaceholder": "Введіть 6-значний код",
|
||||
"verify": "Перевірка",
|
||||
"cancel": "Скасувати",
|
||||
"twoFactorNote": "Примітка: якщо у вас немає доступу до вашого пристрою автентифікатора, ви можете скинути налаштування 2FA за допомогою коду відновлення, увійшовши через вебсайт.",
|
||||
"masterPassword": "Головний пароль",
|
||||
"unlockVault": "Розблокувати Vault",
|
||||
"unlockTitle": "Розблокувати своє сховище",
|
||||
"unlockDescription": "Введіть свій головний пароль, щоб розблокувати сховище.",
|
||||
"logout": "Вийти",
|
||||
"logoutConfirm": "Ви впевнені, що хочете вийти?",
|
||||
"sessionExpired": "Ваш сеанс закінчився. Будь ласка, увійдіть знову.",
|
||||
"unlockSuccess": "Сховище успішно розблоковано!",
|
||||
"unlockSuccessTitle": "Ваше сховище успішно розблоковано",
|
||||
"unlockSuccessDescription": "Тепер ви можете використовувати автозаповнення форм входу у вашому браузері.",
|
||||
"closePopup": "Закрити цю підказку",
|
||||
"browseVault": "Переглянути вміст сховища",
|
||||
"connectingTo": "Підключення до",
|
||||
"switchAccounts": "Змінити обліковий запис?",
|
||||
"loggedIn": "Вхід виконано",
|
||||
"errors": {
|
||||
"invalidCode": "Please enter a valid 6-digit authentication code.",
|
||||
"serverError": "Could not reach AliasVault server. Please try again later or contact support if the problem persists.",
|
||||
"noToken": "Login failed -- no token returned",
|
||||
"migrationError": "An error occurred while checking for pending migrations.",
|
||||
"wrongPassword": "Incorrect password. Please try again.",
|
||||
"accountLocked": "Account temporarily locked due to too many failed attempts.",
|
||||
"networkError": "Network error. Please check your connection and try again.",
|
||||
"loginDataMissing": "Login session expired. Please try again."
|
||||
"invalidCode": "Будь ласка, введіть дійсний 6-значний код автентифікації.",
|
||||
"serverError": "Не вдалося зв’язатися зі сервером AliasVault. Будь ласка, спробуйте пізніше або зверніться до служби підтримки, якщо проблема не зникне.",
|
||||
"noToken": "Не вдалося ввійти -- токен не знайдено",
|
||||
"migrationError": "Під час перевірки незавершених перенесень сталася помилка.",
|
||||
"wrongPassword": "Невірний пароль. Будь ласка, спробуйте ще раз.",
|
||||
"accountLocked": "Обліковий запис тимчасово заблоковано через занадто багато невдалих спроб.",
|
||||
"networkError": "Помилка мережі. Будь ласка, перевірте з’єднання та спробуйте ще раз.",
|
||||
"loginDataMissing": "Термін дії сеансу закінчився. Будь ласка, спробуйте ще раз."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "Credentials",
|
||||
"emails": "Emails",
|
||||
"settings": "Settings"
|
||||
"credentials": "Облікові дані",
|
||||
"emails": "Електронні адреси",
|
||||
"settings": "Налаштування"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"cancel": "Cancel",
|
||||
"use": "Use",
|
||||
"delete": "Delete",
|
||||
"close": "Close",
|
||||
"copied": "Copied!",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"language": "Language",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"loadingEmails": "Loading emails...",
|
||||
"loadingTotpCodes": "Loading TOTP codes...",
|
||||
"attachments": "Attachments",
|
||||
"loadingAttachments": "Loading attachments...",
|
||||
"settings": "Settings",
|
||||
"recentEmails": "Recent emails",
|
||||
"loginCredentials": "Login credentials",
|
||||
"twoFactorAuthentication": "Two-factor authentication",
|
||||
"alias": "Alias",
|
||||
"notes": "Notes",
|
||||
"fullName": "Full Name",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"birthDate": "Birth Date",
|
||||
"nickname": "Nickname",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"syncingVault": "Syncing vault",
|
||||
"savingChangesToVault": "Saving changes to vault",
|
||||
"uploadingVaultToServer": "Uploading vault to server",
|
||||
"checkingVaultUpdates": "Checking for vault updates",
|
||||
"syncingUpdatedVault": "Syncing updated vault",
|
||||
"executingOperation": "Executing operation...",
|
||||
"loadMore": "Load more",
|
||||
"loading": "Завантаження даних...",
|
||||
"error": "Помилка",
|
||||
"success": "Успішно",
|
||||
"cancel": "Скасувати",
|
||||
"use": "Використовувати",
|
||||
"delete": "Видалити",
|
||||
"close": "Закрити",
|
||||
"copied": "Скопійовано!",
|
||||
"openInNewWindow": "Відкрити у новому вікні",
|
||||
"language": "Мова",
|
||||
"enabled": "Увімкнено",
|
||||
"disabled": "Вимкнено",
|
||||
"showPassword": "Показати пароль",
|
||||
"hidePassword": "Приховати пароль",
|
||||
"copyToClipboard": "Копіювати до буфера обміну",
|
||||
"loadingEmails": "Завантаження електронних адрес...",
|
||||
"loadingTotpCodes": "Завантаження кодів TOTP...",
|
||||
"attachments": "Вкладення",
|
||||
"loadingAttachments": "Завантаження вкладень...",
|
||||
"settings": "Налаштування",
|
||||
"recentEmails": "Останні електронні листи",
|
||||
"loginCredentials": "Облікові дані для входу",
|
||||
"twoFactorAuthentication": "Двофакторна автентифікація",
|
||||
"alias": "Псевдонім",
|
||||
"notes": "Нотатки",
|
||||
"fullName": "Повне ім'я",
|
||||
"firstName": "Ім’я",
|
||||
"lastName": "Прізвище",
|
||||
"birthDate": "Дата народження",
|
||||
"nickname": "Нікнейм",
|
||||
"email": "Електронна пошта",
|
||||
"username": "Ім'я користувача",
|
||||
"password": "Пароль",
|
||||
"syncingVault": "Синхронізація сховища",
|
||||
"savingChangesToVault": "Збереження змін у сховищі",
|
||||
"uploadingVaultToServer": "Завантаження сховища на сервер",
|
||||
"checkingVaultUpdates": "Перевірка оновлень сховища",
|
||||
"syncingUpdatedVault": "Синхронізація оновленого сховища",
|
||||
"executingOperation": "Виконання операції...",
|
||||
"loadMore": "Завантажити ще",
|
||||
"errors": {
|
||||
"VaultMergeRequired": "Your vault needs to be updated. Please login on the AliasVault website and follow the steps.",
|
||||
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
|
||||
"NoVaultFound": "Your account does not have a vault yet. Please complete the tutorial in the AliasVault web client before using the browser extension.",
|
||||
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
|
||||
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
|
||||
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
|
||||
"unknownError": "An unknown error occurred",
|
||||
"failedToStoreVault": "Failed to store vault",
|
||||
"vaultNotAvailable": "Vault not available",
|
||||
"failedToGetVault": "Failed to get vault",
|
||||
"vaultIsLocked": "Vault is locked",
|
||||
"failedToGetCredentials": "Failed to get credentials",
|
||||
"failedToCreateIdentity": "Failed to create identity",
|
||||
"failedToGetDefaultEmailDomain": "Failed to get default email domain",
|
||||
"failedToGetDefaultIdentitySettings": "Failed to get default identity settings",
|
||||
"failedToGetPasswordSettings": "Failed to get password settings",
|
||||
"failedToUploadVault": "Failed to upload vault",
|
||||
"noDerivedKeyAvailable": "No derived key available for encryption",
|
||||
"failedToUploadVaultToServer": "Failed to upload new vault to server",
|
||||
"noVaultOrDerivedKeyFound": "No vault or derived key found"
|
||||
"VaultOutdated": "Ваше сховище застаріло. Будь ласка, увійдіть на вебсайт AliasVault та виконайте наведені нижче дії.",
|
||||
"serverNotAvailable": "Не вдалося зв’язатися зі сервером AliasVault. Будь ласка, спробуйте пізніше або зверніться до служби підтримки, якщо проблема не зникне.",
|
||||
"clientVersionNotSupported": "Ця версія розширення браузера AliasVault більше не підтримується сервером. Будь ласка, оновіть розширення браузера до останньої версії.",
|
||||
"serverVersionNotSupported": "Щоб використовувати це розширення браузера, потрібно оновити сервер AliasVault до новішої версії. Зверніться до служби підтримки, якщо вам потрібна допомога.",
|
||||
"unknownError": "Сталася невідома помилка",
|
||||
"failedToStoreVault": "Не вдалося зберегти сховище",
|
||||
"vaultNotAvailable": "Сховище недоступне",
|
||||
"failedToRetrieveData": "Не вдалося отримати дані",
|
||||
"vaultIsLocked": "Сховище заблоковано",
|
||||
"failedToUploadVault": "Не вдалося завантажити сховище",
|
||||
"passwordChanged": "Ваш пароль змінився з моменту останнього входу. З міркувань безпеки, будь ласка, увійдіть ще раз."
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",
|
||||
"ACCOUNT_LOCKED": "Account temporarily locked due to too many failed attempts. Please try again later.",
|
||||
"ACCOUNT_BLOCKED": "Your account has been disabled. If you believe this is a mistake, please contact support.",
|
||||
"USER_NOT_FOUND": "Invalid username or password. Please try again.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Invalid authenticator code. Please try again.",
|
||||
"INVALID_RECOVERY_CODE": "Invalid recovery code. Please try again.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Refresh token is required.",
|
||||
"USER_NOT_FOUND_IN_TOKEN": "User not found in token.",
|
||||
"USER_NOT_FOUND_IN_DATABASE": "User not found in database.",
|
||||
"INVALID_REFRESH_TOKEN": "Invalid refresh token.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token revoked successfully.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "New account registration is currently disabled on this server. Please contact the administrator.",
|
||||
"USERNAME_REQUIRED": "Username is required.",
|
||||
"USERNAME_ALREADY_IN_USE": "Username is already in use.",
|
||||
"USERNAME_AVAILABLE": "Username is available.",
|
||||
"USERNAME_MISMATCH": "Username does not match the current user.",
|
||||
"PASSWORD_MISMATCH": "The provided password does not match your current password.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Account successfully deleted.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Username cannot be empty or whitespace.",
|
||||
"USERNAME_TOO_SHORT": "Username too short: must be at least 3 characters long.",
|
||||
"USERNAME_TOO_LONG": "Username too long: cannot be longer than 40 characters.",
|
||||
"USERNAME_INVALID_EMAIL": "Invalid email address.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Username is invalid, can only contain letters or digits.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Your vault is not up-to-date. Please synchronize your vault and try again.",
|
||||
"INTERNAL_SERVER_ERROR": "Internal server error.",
|
||||
"VAULT_ERROR": "The local vault is not up-to-date. Please synchronize your vault by refreshing the page and try again."
|
||||
"UNKNOWN_ERROR": "Сталася невідома помилка. Будь ласка, спробуйте ще раз.",
|
||||
"ACCOUNT_LOCKED": "Обліковий запис тимчасово заблоковано через занадто багато невдалих спроб. Будь ласка, спробуйте пізніше.",
|
||||
"ACCOUNT_BLOCKED": "Ваш обліковий запис вимкнено. Якщо ви вважаєте, що це помилка, зверніться до служби підтримки.",
|
||||
"USER_NOT_FOUND": "Недійсне ім'я користувача або пароль. Спробуйте ще раз.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Недійсний код автентифікатора. Спробуйте ще раз.",
|
||||
"INVALID_RECOVERY_CODE": "Недійсний код відновлення. Будь ласка, спробуйте ще раз.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Необхідне оновлення токена.",
|
||||
"INVALID_REFRESH_TOKEN": "Оновлення токена невдале.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Оновлення токена відкликано успішно.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "Реєстрація нових облікових записів на цьому сервері наразі вимкнена. Зверніться до адміністратора.",
|
||||
"USERNAME_REQUIRED": "Ім'я користувача обов'язкове.",
|
||||
"USERNAME_ALREADY_IN_USE": "Ім'я користувача вже використовується.",
|
||||
"USERNAME_AVAILABLE": "Ім'я користувача доступне.",
|
||||
"USERNAME_MISMATCH": "Ім'я користувача не відповідає поточному користувачеві.",
|
||||
"PASSWORD_MISMATCH": "Введений пароль не відповідає вашому поточному паролю.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Обліковий запис успішно видалено.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "Ім’я користувача не може бути порожнім або містити пробіли.",
|
||||
"USERNAME_TOO_SHORT": "Ім’я користувача закоротке: має містити щонайменше 3 символи.",
|
||||
"USERNAME_TOO_LONG": "Ім'я користувача занадто довге: не може бути довшим за 40 символів.",
|
||||
"USERNAME_INVALID_EMAIL": "Недійсна адреса електронної пошти.",
|
||||
"USERNAME_INVALID_CHARACTERS": "Ім'я користувача недійсне, може містити лише літери або цифри.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Ваше сховище застаріло. Синхронізуйте його та спробуйте ще раз.",
|
||||
"INTERNAL_SERVER_ERROR": "Внутрішня помилка сервера.",
|
||||
"VAULT_ERROR": "Локальне сховище не оновлене. Синхронізуйте своє сховище, оновивши сторінку, та повторіть спробу."
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"or": "or",
|
||||
"new": "New",
|
||||
"cancel": "Cancel",
|
||||
"search": "Search",
|
||||
"vaultLocked": "AliasVault is locked.",
|
||||
"creatingNewAlias": "Creating new alias...",
|
||||
"noMatchesFound": "No matches found",
|
||||
"searchVault": "Search vault...",
|
||||
"serviceName": "Service name",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"enterServiceName": "Enter service name",
|
||||
"enterEmailAddress": "Enter email address",
|
||||
"enterUsername": "Enter username",
|
||||
"hideFor1Hour": "Hide for 1 hour (current site)",
|
||||
"hidePermanently": "Hide permanently (current site)",
|
||||
"createRandomAlias": "Create random alias",
|
||||
"createUsernamePassword": "Create username/password",
|
||||
"randomAlias": "Random alias",
|
||||
"usernamePassword": "Username/password",
|
||||
"createAndSaveAlias": "Create and save alias",
|
||||
"createAndSaveCredential": "Create and save credential",
|
||||
"randomIdentityDescription": "Generate a random identity with a random email address accessible in AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Random identity with random email",
|
||||
"manualCredentialDescription": "Specify your own email address and username.",
|
||||
"manualCredentialDescriptionDropdown": "Manual username and password",
|
||||
"failedToCreateIdentity": "Failed to create identity. Please try again.",
|
||||
"enterEmailAndOrUsername": "Enter email and/or username",
|
||||
"autofillWithAliasVault": "Autofill with AliasVault",
|
||||
"generateRandomPassword": "Generate random password (copy to clipboard)",
|
||||
"generateNewPassword": "Generate new password",
|
||||
"togglePasswordVisibility": "Toggle password visibility",
|
||||
"passwordCopiedToClipboard": "Password copied to clipboard",
|
||||
"enterEmailAndOrUsernameError": "Enter email and/or username",
|
||||
"openAliasVaultToUpgrade": "Open AliasVault to upgrade",
|
||||
"vaultUpgradeRequired": "Vault upgrade required.",
|
||||
"dismissPopup": "Dismiss popup"
|
||||
"or": "або",
|
||||
"new": "Новий",
|
||||
"cancel": "Скасувати",
|
||||
"search": "Пошук",
|
||||
"vaultLocked": "AliasVault заблоковано.",
|
||||
"creatingNewAlias": "Створення нового псевдоніму...",
|
||||
"noMatchesFound": "Збігів не знайдено",
|
||||
"searchVault": "Пошук сховища...",
|
||||
"serviceName": "Назва служби",
|
||||
"email": "Електронна пошта",
|
||||
"username": "Ім'я користувача",
|
||||
"password": "Пароль",
|
||||
"enterServiceName": "Введіть назву служби",
|
||||
"enterEmailAddress": "Введіть електронну адресу",
|
||||
"enterUsername": "Введіть ім'я користувача",
|
||||
"hideFor1Hour": "Сховати протягом 1 години (поточний сайт)",
|
||||
"hidePermanently": "Приховати назавжди (поточний сайт)",
|
||||
"createRandomAlias": "Створити випадковий псевдонім",
|
||||
"createUsernamePassword": "Створити ім'я користувача/пароль",
|
||||
"randomAlias": "Випадковий псевдонім",
|
||||
"usernamePassword": "Ім'я користувача/Пароль",
|
||||
"createAndSaveAlias": "Створити та зберегти псевдонім",
|
||||
"createAndSaveCredential": "Створити та зберегти облікові дані",
|
||||
"randomIdentityDescription": "Згенеруйте випадкову особу з випадковою адресою електронної пошти, доступною в AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Випадкова ідентифікація з випадковою електронною поштою",
|
||||
"manualCredentialDescription": "Вкажіть власну адресу електронної пошти та ім'я користувача.",
|
||||
"manualCredentialDescriptionDropdown": "Ім'я користувача та пароль вручну",
|
||||
"failedToCreateIdentity": "Не вдалося створити ідентифікатор. Спробуйте ще раз.",
|
||||
"enterEmailAndOrUsername": "Введіть електронну пошту та/або ім'я користувача",
|
||||
"autofillWithAliasVault": "Автозаповнення за допомогою AliasVault",
|
||||
"generateRandomPassword": "Згенерувати випадковий пароль (скопіювати в буфер обміну)",
|
||||
"generateNewPassword": "Згенерувати новий пароль",
|
||||
"togglePasswordVisibility": "Перемикання видимості пароля",
|
||||
"passwordCopiedToClipboard": "Пароль скопійовано в буфер обміну",
|
||||
"enterEmailAndOrUsernameError": "Введіть електронну пошту та/або ім'я користувача",
|
||||
"openAliasVaultToUpgrade": "Відкрити AliasVault для покращення",
|
||||
"vaultUpgradeRequired": "Потрібне оновлення сховища.",
|
||||
"dismissPopup": "Закрити спливаюче вікно"
|
||||
},
|
||||
"credentials": {
|
||||
"title": "Credentials",
|
||||
"addCredential": "Add Credential",
|
||||
"editCredential": "Edit Credential",
|
||||
"deleteCredential": "Delete Credential",
|
||||
"credentialDetails": "Credential Details",
|
||||
"serviceName": "Service Name",
|
||||
"serviceNamePlaceholder": "e.g., Gmail, Facebook, Bank",
|
||||
"website": "Website",
|
||||
"title": "Облікові дані",
|
||||
"addCredential": "Додати облікові дані",
|
||||
"editCredential": "Редагувати облікові дані",
|
||||
"deleteCredential": "Видалити облікові дані",
|
||||
"credentialDetails": "Відомості про облікові дані",
|
||||
"serviceName": "Назва сервісу",
|
||||
"serviceNamePlaceholder": "наприклад, Gmail, Facebook, Bank",
|
||||
"website": "Вебсайт",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "Enter username",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"generatePassword": "Generate Password",
|
||||
"copyPassword": "Copy Password",
|
||||
"showPassword": "Show Password",
|
||||
"hidePassword": "Hide Password",
|
||||
"notes": "Notes",
|
||||
"notesPlaceholder": "Additional notes...",
|
||||
"totp": "Two-Factor Authentication",
|
||||
"totpCode": "TOTP Code",
|
||||
"copyTotp": "Copy TOTP",
|
||||
"totpSecret": "TOTP Secret",
|
||||
"totpSecretPlaceholder": "Enter TOTP secret key",
|
||||
"noCredentials": "No credentials found",
|
||||
"noCredentialsDescription": "Add your first credential to get started",
|
||||
"searchCredentials": "Search credentials...",
|
||||
"searchPlaceholder": "Search credentials...",
|
||||
"welcomeTitle": "Welcome to AliasVault!",
|
||||
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
|
||||
"lastUsed": "Last used",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Fill Form",
|
||||
"copyUsername": "Copy Username",
|
||||
"openWebsite": "Open Website",
|
||||
"favorite": "Favorite",
|
||||
"unfavorite": "Remove from Favorites",
|
||||
"deleteConfirm": "Are you sure you want to delete this credential?",
|
||||
"deleteSuccess": "Credential deleted successfully",
|
||||
"saveSuccess": "Credential saved successfully",
|
||||
"copySuccess": "Copied to clipboard",
|
||||
"tags": "Tags",
|
||||
"addTag": "Add Tag",
|
||||
"removeTag": "Remove Tag",
|
||||
"folder": "Folder",
|
||||
"selectFolder": "Select Folder",
|
||||
"createFolder": "Create Folder",
|
||||
"saveCredential": "Save credential",
|
||||
"deleteCredentialTitle": "Delete Credential",
|
||||
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
|
||||
"randomAlias": "Random Alias",
|
||||
"manual": "Manual",
|
||||
"service": "Service",
|
||||
"serviceUrl": "Service URL",
|
||||
"loginCredentials": "Login Credentials",
|
||||
"generateRandomUsername": "Generate random username",
|
||||
"generateRandomPassword": "Generate random password",
|
||||
"changePasswordComplexity": "Change password complexity",
|
||||
"passwordLength": "Password length",
|
||||
"includeLowercase": "Include lowercase letters",
|
||||
"includeUppercase": "Include uppercase letters",
|
||||
"includeNumbers": "Include numbers",
|
||||
"includeSpecialChars": "Include special characters",
|
||||
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
|
||||
"generateNewPreview": "Generate new preview",
|
||||
"generateRandomAlias": "Generate Random Alias",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"nickName": "Nick Name",
|
||||
"gender": "Gender",
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"errors": {
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"saveError": "Failed to save credential",
|
||||
"loadError": "Failed to load credentials",
|
||||
"deleteError": "Failed to delete credential",
|
||||
"copyError": "Failed to copy to clipboard"
|
||||
},
|
||||
"username": "Ім'я користувача",
|
||||
"usernamePlaceholder": "Введіть ім'я користувача",
|
||||
"password": "Пароль",
|
||||
"passwordPlaceholder": "Введіть пароль",
|
||||
"generatePassword": "Згенерувати пароль",
|
||||
"copyPassword": "Копіювати пароль",
|
||||
"showPassword": "Показати пароль",
|
||||
"hidePassword": "Приховати пароль",
|
||||
"notes": "Нотатки",
|
||||
"notesPlaceholder": "Додаткові нотатки...",
|
||||
"totp": "Двофакторна аутентифікація",
|
||||
"totpCode": "Код TOTP",
|
||||
"copyTotp": "Копіювати TOTP",
|
||||
"totpSecret": "Секрет TOTP",
|
||||
"totpSecretPlaceholder": "Введіть секретний ключ TOTP",
|
||||
"noCredentials": "Облікових даних не знайдено",
|
||||
"noCredentialsDescription": "Додайте свої перші облікові дані, щоб розпочати",
|
||||
"searchPlaceholder": "Пошук облікових даних...",
|
||||
"welcomeTitle": "Ласкаво просимо до AliasVult!",
|
||||
"welcomeDescription": "Щоб скористатися розширенням браузера AliasVault: перейдіть на вебсайт і скористайтеся спливаючим вікном автозаповнення AliasVault, щоб створити нові облікові дані.",
|
||||
"createdAt": "Створено",
|
||||
"updatedAt": "Останнє оновлення",
|
||||
"autofill": "Автозаповнення",
|
||||
"fillForm": "Заповнити форму",
|
||||
"deleteConfirm": "Ви впевнені, що хочете видалити ці облікові дані?",
|
||||
"saveSuccess": "Облікові дані успішно збережено",
|
||||
"tags": "Теги",
|
||||
"addTag": "Додати тег",
|
||||
"removeTag": "Видалити тег",
|
||||
"folder": "Тека",
|
||||
"selectFolder": "Вибрати теку",
|
||||
"createFolder": "Створити теку",
|
||||
"saveCredential": "Зберегти облікові дані",
|
||||
"deleteCredentialTitle": "Видалити облікові дані",
|
||||
"deleteCredentialConfirm": "Ви впевнені, що хочете видалити ці облікові дані? Цю дію неможливо скасувати.",
|
||||
"randomAlias": "Випадковий псевдонім",
|
||||
"manual": "Посібник",
|
||||
"service": "Служба",
|
||||
"serviceUrl": "URL-адреса сервісу",
|
||||
"loginCredentials": "Дані для входу",
|
||||
"generateRandomUsername": "Згенерувати випадкове ім'я користувача",
|
||||
"generateRandomPassword": "Згенерувати випадковий пароль",
|
||||
"changePasswordComplexity": "Зміна складності пароля",
|
||||
"passwordLength": "Довжина пароля",
|
||||
"includeLowercase": "Включити малі літери",
|
||||
"includeUppercase": "Включити великі літери",
|
||||
"includeNumbers": "Включити числа",
|
||||
"includeSpecialChars": "Включити спеціальні символи",
|
||||
"avoidAmbiguousChars": "Уникайте неоднозначних символів (o, 0 тощо)",
|
||||
"generateNewPreview": "Згенерувати новий попередній перегляд",
|
||||
"generateRandomAlias": "Генерувати випадковий псевдонім",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"alias": "Псевдонім",
|
||||
"firstName": "Ім’я",
|
||||
"lastName": "Прізвище",
|
||||
"nickName": "Нікнейм",
|
||||
"gender": "Стать",
|
||||
"birthDate": "Дата народження",
|
||||
"birthDatePlaceholder": "РРРР-ММ-ДД",
|
||||
"metadata": "Метадані",
|
||||
"validation": {
|
||||
"required": "This field is required",
|
||||
"serviceNameRequired": "Service name is required",
|
||||
"invalidUrl": "Invalid URL format",
|
||||
"invalidEmail": "Invalid email format",
|
||||
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
|
||||
}
|
||||
"required": "Це поле обов'язкове",
|
||||
"serviceNameRequired": "Назва служби обов'язкова",
|
||||
"invalidEmail": "Недійсний формат електронної пошти",
|
||||
"invalidDateFormat": "Дата має бути у форматі РРРР-ММ-ДД"
|
||||
},
|
||||
"privateEmailTitle": "Приватна електронна адреса",
|
||||
"privateEmailAliasVaultServer": "Сервер AliasVault",
|
||||
"privateEmailDescription": "Наскрізне шифрування, повністю конфіденційно.",
|
||||
"publicEmailTitle": "Публічні тимчасові постачальники електронної пошти",
|
||||
"publicEmailDescription": "Анонімність, але обмежена конфіденційність. Зміст електронного листа може прочитати будь-хто, хто знає адресу.",
|
||||
"useDomainChooser": "Використовувати засіб вибору домену",
|
||||
"enterCustomDomain": "Введіть користувацький домен",
|
||||
"enterFullEmail": "Введіть повну електронну адресу",
|
||||
"enterEmailPrefix": "Введіть префікс електронної адреси"
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
"deleteEmailTitle": "Delete Email",
|
||||
"deleteEmailConfirm": "Are you sure you want to permanently delete this email?",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"date": "Date",
|
||||
"emailContent": "Email content",
|
||||
"attachments": "Attachments",
|
||||
"emailNotFound": "Email not found",
|
||||
"noEmails": "No emails found",
|
||||
"noEmailsDescription": "You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.",
|
||||
"title": "Електронні листи",
|
||||
"deleteEmailTitle": "Видалити електронного листа",
|
||||
"deleteEmailConfirm": "Ви впевнені, що хочете остаточно видалити цей електронний лист?",
|
||||
"from": "Від",
|
||||
"to": "До",
|
||||
"date": "Дата",
|
||||
"emailContent": "Вміст електронного листа",
|
||||
"attachments": "Вкладення",
|
||||
"emailNotFound": "Електронний лист не знайдено",
|
||||
"noEmails": "Електронних листів не знайдено",
|
||||
"noEmailsDescription": "Ви ще не отримували жодних листів на свої приватні адреси електронної пошти. Коли ви отримаєте новий лист, він з’явиться тут.",
|
||||
"dateFormat": {
|
||||
"justNow": "just now",
|
||||
"minutesAgo_single": "{{count}} min ago",
|
||||
"minutesAgo_plural": "{{count}} mins ago",
|
||||
"hoursAgo_single": "{{count}} hr ago",
|
||||
"hoursAgo_plural": "{{count}} hrs ago",
|
||||
"yesterday": "yesterday"
|
||||
"justNow": "щойно",
|
||||
"minutesAgo_single": "{{count}} хвилина тому",
|
||||
"minutesAgo_plural": "{{count}} хвилин тому",
|
||||
"hoursAgo_single": "{{count}} година тому",
|
||||
"hoursAgo_plural": "{{count}} годин тому",
|
||||
"yesterday": "учора"
|
||||
},
|
||||
"errors": {
|
||||
"emailLoadError": "An error occurred while loading emails. Please try again later.",
|
||||
"emailUnexpectedError": "An unexpected error occurred while loading emails. Please try again later."
|
||||
"emailLoadError": "Під час завантаження електронних листів сталася помилка. Спробуйте ще раз пізніше.",
|
||||
"emailUnexpectedError": "Під час завантаження електронних листів сталася неочікувана помилка. Спробуйте ще раз пізніше."
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "The current chosen email address is already in use. Please change the email address by editing this credential.",
|
||||
"CLAIM_DOES_NOT_EXIST": "An error occurred while trying to load the emails. Please try to edit and save the credential entry to synchronize the database, then try again."
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "Поточна вибрана електронна адреса вже використовується. Змініть електронну адресу, відредагувавши ці облікові дані.",
|
||||
"CLAIM_DOES_NOT_EXIST": "Під час спроби завантажити електронні листи сталася помилка. Спробуйте відредагувати та зберегти запис облікових даних, щоб синхронізувати базу даних, а потім повторіть спробу."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"serverUrl": "Server URL",
|
||||
"language": "Language",
|
||||
"autofillEnabled": "Enable Autofill",
|
||||
"version": "Version",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"openWebApp": "Open web app",
|
||||
"loggedIn": "Logged in",
|
||||
"logout": "Logout",
|
||||
"globalSettings": "Global Settings",
|
||||
"autofillPopup": "Autofill popup",
|
||||
"activeOnAllSites": "Active on all sites (unless disabled below)",
|
||||
"disabledOnAllSites": "Disabled on all sites",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"rightClickContextMenu": "Right-click context menu",
|
||||
"siteSpecificSettings": "Site-Specific Settings",
|
||||
"autofillPopupOn": "Autofill popup on: ",
|
||||
"enabledForThisSite": "Enabled for this site",
|
||||
"disabledForThisSite": "Disabled for this site",
|
||||
"temporarilyDisabledUntil": "Temporarily disabled until ",
|
||||
"resetAllSiteSettings": "Reset all site-specific settings",
|
||||
"appearance": "Appearance",
|
||||
"theme": "Theme",
|
||||
"useDefault": "Use default",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||
"configureKeyboardShortcuts": "Configure keyboard shortcuts",
|
||||
"configure": "Configure",
|
||||
"versionPrefix": "Version ",
|
||||
"title": "Налаштування",
|
||||
"serverUrl": "URL-адреса сервера",
|
||||
"language": "Мова",
|
||||
"autofillEnabled": "Увімкнути автозаповнення",
|
||||
"version": "Версія",
|
||||
"openInNewWindow": "Відкрити у новому вікні",
|
||||
"openWebApp": "Відкрити веб додаток",
|
||||
"loggedIn": "Вхід виконано",
|
||||
"logout": "Вийти",
|
||||
"globalSettings": "Глобальні налаштування",
|
||||
"autofillPopup": "Спливаюче вікно автозаповнення",
|
||||
"activeOnAllSites": "Активно на всіх сайтах (якщо не вимкнено нижче)",
|
||||
"disabledOnAllSites": "Вимкнено на всіх сайтах",
|
||||
"enabled": "Увімкнено",
|
||||
"disabled": "Вимкнено",
|
||||
"rightClickContextMenu": "Контекстне меню правою кнопкою миші",
|
||||
"autofillMatching": "Автозаповнення відповідності",
|
||||
"autofillMatchingMode": "Режим автозаповнення відповідностей",
|
||||
"autofillMatchingModeDescription": "Визначає, які облікові дані вважаються відповідними та будуть показуватися як пропозиції у спливному вікні автозаповнення для певного вебсайту.",
|
||||
"autofillMatchingDefault": "URL-адреса + піддомен + універсальне ім'я",
|
||||
"autofillMatchingUrlSubdomain": "URL-адреса + піддомен",
|
||||
"autofillMatchingUrlExact": "Лише точний домен URL-адреси",
|
||||
"siteSpecificSettings": "Налаштування, специфічні для сайту",
|
||||
"autofillPopupOn": "Спливаюче вікно автозаповнення на: ",
|
||||
"enabledForThisSite": "Увімкнено для цього сайту",
|
||||
"disabledForThisSite": "Вимкнено для цього сайту",
|
||||
"temporarilyDisabledUntil": "Тимчасово вимкнено до ",
|
||||
"resetAllSiteSettings": "Скинути всі налаштування, характерні для сайту",
|
||||
"appearance": "Зовнішній вигляд",
|
||||
"theme": "Тема",
|
||||
"useDefault": "Використовувати за замовчуванням",
|
||||
"light": "Світла",
|
||||
"dark": "Темна",
|
||||
"keyboardShortcuts": "Комбінації клавіш",
|
||||
"configureKeyboardShortcuts": "Налаштування комбінацій клавіш",
|
||||
"configure": "Налаштування",
|
||||
"security": "Безпека",
|
||||
"clipboardClearTimeout": "Очистити буфер обміну після копіювання",
|
||||
"clipboardClearTimeoutDescription": "Автоматично очищати буфер обміну після копіювання конфіденційних даних",
|
||||
"clipboardClearDisabled": "Ніколи не очищати",
|
||||
"clipboardClear5Seconds": "Очистити після 5 секунд",
|
||||
"clipboardClear10Seconds": "Очистити після 10 секунд",
|
||||
"clipboardClear15Seconds": "Очистити після 15 секунд",
|
||||
"autoLockTimeout": "Тайм-аут автоматичного блокування",
|
||||
"autoLockTimeoutDescription": "Автоматично блокувати сховище після періоду бездіяльності",
|
||||
"autoLockTimeoutHelp": "Сховище буде заблоковано лише після зазначеного періоду бездіяльності (не використовується автозаповнення або не відкривається спливне вікно розширення). Сховище завжди блокуватиметься, коли браузер закривається, незалежно від цього налаштування.",
|
||||
"autoLockNever": "Ніколи",
|
||||
"autoLock15Seconds": "15 секунд",
|
||||
"autoLock1Minute": "1 хвилина",
|
||||
"autoLock5Minutes": "5 хвилин",
|
||||
"autoLock15Minutes": "15 хвилин",
|
||||
"autoLock30Minutes": "30 хвилин",
|
||||
"autoLock1Hour": "1 година",
|
||||
"autoLock4Hours": "4 години",
|
||||
"autoLock8Hours": "8 годин",
|
||||
"autoLock24Hours": "24 години",
|
||||
"versionPrefix": "Версія ",
|
||||
"preferences": "Налаштування",
|
||||
"autofillSettings": "Налаштування автозаповнення",
|
||||
"clipboardSettings": "Параметри буфера обміну",
|
||||
"contextMenuSettings": "Налаштування контекстного меню",
|
||||
"contextMenu": "Контекстне меню",
|
||||
"contextMenuEnabled": "Контекстне меню увімкнено",
|
||||
"contextMenuDisabled": "Контекстне меню вимкнено",
|
||||
"contextMenuDescription": "Натисніть правою кнопкою миші на поля введення, щоб отримати доступ до параметрів AliasVault",
|
||||
"selectLanguage": "Виберіть мову",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL is required",
|
||||
"apiUrlInvalid": "Please enter a valid API URL",
|
||||
"clientUrlRequired": "Client URL is required",
|
||||
"clientUrlInvalid": "Please enter a valid client URL"
|
||||
"apiUrlRequired": "URL-адреса API обов'язкова",
|
||||
"apiUrlInvalid": "Будь ласка, введіть дійсну URL-адресу API",
|
||||
"clientUrlRequired": "URL-адреса клієнта обов'язкова",
|
||||
"clientUrlInvalid": "Будь ласка, введіть дійсну URL-адресу клієнта"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "Upgrade Vault",
|
||||
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
|
||||
"versionInformation": "Version Information",
|
||||
"yourVault": "Your vault:",
|
||||
"newVersion": "New version:",
|
||||
"upgrade": "Upgrade Vault",
|
||||
"upgrading": "Upgrading...",
|
||||
"logout": "Logout",
|
||||
"whatsNew": "What's New",
|
||||
"whatsNewDescription": "An upgrade is required to support the following changes:",
|
||||
"noDescriptionAvailable": "No description available for this version.",
|
||||
"okay": "Ok",
|
||||
"title": "Оновлення сховища",
|
||||
"subtitle": "AliasVault оновлено, і ваше сховище потрібно оновити. Це займе лише кілька секунд.",
|
||||
"versionInformation": "Інформація про версію",
|
||||
"yourVault": "Ваше сховище:",
|
||||
"newVersion": "Нова версія:",
|
||||
"upgrade": "Оновлення сховища",
|
||||
"upgrading": "Оновлення...",
|
||||
"logout": "Вихід",
|
||||
"whatsNew": "Що нового",
|
||||
"whatsNewDescription": "Для підтримки таких змін потрібне оновлення:",
|
||||
"noDescriptionAvailable": "Для цієї версії немає опису.",
|
||||
"okay": "Ок",
|
||||
"status": {
|
||||
"preparingUpgrade": "Preparing upgrade...",
|
||||
"vaultAlreadyUpToDate": "Vault is already up to date",
|
||||
"startingDatabaseTransaction": "Starting database transaction...",
|
||||
"applyingDatabaseMigrations": "Applying database migrations...",
|
||||
"applyingMigration": "Applying migration {{current}} of {{total}}...",
|
||||
"committingChanges": "Committing changes..."
|
||||
"preparingUpgrade": "Підготовка оновлення...",
|
||||
"vaultAlreadyUpToDate": "Сховище вже оновлено",
|
||||
"startingDatabaseTransaction": "Початок транзакції бази даних...",
|
||||
"applyingDatabaseMigrations": "Застосування міграцій бази даних...",
|
||||
"applyingMigration": "Застосування міграції {{current}} з {{total}}...",
|
||||
"committingChanges": "Внесення змін..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Error",
|
||||
"unableToGetVersionInfo": "Unable to get version information. Please try again.",
|
||||
"selfHostedServer": "Self-Hosted Server",
|
||||
"selfHostedWarning": "If you're using a self-hosted server, make sure to also update your self-hosted instance as otherwise logging in to the web client will stop working.",
|
||||
"cancel": "Cancel",
|
||||
"continueUpgrade": "Continue Upgrade",
|
||||
"upgradeFailed": "Upgrade Failed",
|
||||
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
|
||||
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
|
||||
"error": "Помилка",
|
||||
"unableToGetVersionInfo": "Не вдалося отримати інформацію про версію. Спробуйте ще раз.",
|
||||
"selfHostedServer": "Сервер із самостійним розміщенням",
|
||||
"selfHostedWarning": "Якщо ви використовуєте власний сервер, обов’язково оновіть і свій власний екземпляр, інакше вхід до вебклієнта перестане працювати.",
|
||||
"cancel": "Скасувати",
|
||||
"continueUpgrade": "Продовжити оновлення",
|
||||
"upgradeFailed": "Помилка оновлення",
|
||||
"failedToApplyMigration": "Не вдалося застосувати міграцію ({{current}} з {{total}})",
|
||||
"unknownErrorDuringUpgrade": "Під час оновлення сталася невідома помилка. Спробуйте ще раз."
|
||||
}
|
||||
}
|
||||
}
|
||||
393
apps/browser-extension/src/i18n/locales/zh.json
Normal file
393
apps/browser-extension/src/i18n/locales/zh.json
Normal file
@@ -0,0 +1,393 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "登录AliasVault",
|
||||
"username": "用户名或电子邮箱",
|
||||
"usernamePlaceholder": "name / name@company.com",
|
||||
"password": "密码",
|
||||
"passwordPlaceholder": "请输入密码",
|
||||
"rememberMe": "记住我",
|
||||
"loginButton": "登录",
|
||||
"noAccount": "还没有账户?",
|
||||
"createVault": "创建新保险库",
|
||||
"twoFactorTitle": "请输入认证器的动态验证码。",
|
||||
"authCode": "动态验证码",
|
||||
"authCodePlaceholder": "输入6位动态验证码",
|
||||
"verify": "验证",
|
||||
"cancel": "取消",
|
||||
"twoFactorNote": "注意:如果无法访问你的认证设备,你可以通过网站登录,使用恢复码重置双因素认证(2FA)。",
|
||||
"masterPassword": "主密码",
|
||||
"unlockVault": "解锁保险库",
|
||||
"unlockTitle": "解锁你的保险库",
|
||||
"unlockDescription": "输入你的主密码以解锁保险库。",
|
||||
"logout": "退出登录",
|
||||
"logoutConfirm": "确定要退出登录吗?",
|
||||
"sessionExpired": "你的会话已过期。请重新登录。",
|
||||
"unlockSuccess": "保险库解锁成功!",
|
||||
"unlockSuccessTitle": "你的保险库已成功解锁",
|
||||
"unlockSuccessDescription": "现在你可以在浏览器的登录表单中使用自动填充功能了。",
|
||||
"closePopup": "关闭此弹窗",
|
||||
"browseVault": "浏览保险库内容",
|
||||
"connectingTo": "正在连接到",
|
||||
"switchAccounts": "切换账户?",
|
||||
"loggedIn": "已登录",
|
||||
"errors": {
|
||||
"invalidCode": "请输入有效的6位动态验证码。",
|
||||
"serverError": "无法连接到AliasVault服务器。请稍后重试,若问题依旧,请联系支持人员。",
|
||||
"noToken": "登录失败——未返回令牌",
|
||||
"migrationError": "检查待处理迁移时发生错误。",
|
||||
"wrongPassword": "密码不正确。请重试。",
|
||||
"accountLocked": "由于多次尝试失败,账户已暂时锁定。",
|
||||
"networkError": "网络错误。请检查你的连接后重试。",
|
||||
"loginDataMissing": "登录会话已过期。请重试。"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "凭证",
|
||||
"emails": "邮件",
|
||||
"settings": "设置"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "加载中……",
|
||||
"error": "错误",
|
||||
"success": "成功",
|
||||
"cancel": "取消",
|
||||
"use": "使用",
|
||||
"delete": "删除",
|
||||
"close": "关闭",
|
||||
"copied": "已复制!",
|
||||
"openInNewWindow": "在新窗口中打开",
|
||||
"language": "语言",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"showPassword": "显示密码",
|
||||
"hidePassword": "隐藏密码",
|
||||
"copyToClipboard": "复制到剪贴板",
|
||||
"loadingEmails": "加载邮件中……",
|
||||
"loadingTotpCodes": "加载TOTP验证码中……",
|
||||
"attachments": "附件",
|
||||
"loadingAttachments": "加载附件中……",
|
||||
"settings": "设置",
|
||||
"recentEmails": "最近邮件",
|
||||
"loginCredentials": "登录凭证",
|
||||
"twoFactorAuthentication": "双因素认证(2FA)",
|
||||
"alias": "别名",
|
||||
"notes": "备注",
|
||||
"fullName": "全名",
|
||||
"firstName": "名",
|
||||
"lastName": "姓",
|
||||
"birthDate": "出生日期",
|
||||
"nickname": "昵称",
|
||||
"email": "电子邮箱",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"syncingVault": "同步保险库中",
|
||||
"savingChangesToVault": "正在保存对保险库的更改",
|
||||
"uploadingVaultToServer": "正在将保险库上传到服务器",
|
||||
"checkingVaultUpdates": "检查保险库更新中",
|
||||
"syncingUpdatedVault": "同步更新后的保险库",
|
||||
"executingOperation": "执行操作中……",
|
||||
"loadMore": "加载更多",
|
||||
"errors": {
|
||||
"VaultOutdated": "你的保险库版本多低。请登录AliasVault网站并按照步骤操作。",
|
||||
"serverNotAvailable": "AliasVault服务器不可用。请稍后重试,若问题持续请联系支持人员。",
|
||||
"clientVersionNotSupported": "此版本的AliasVault浏览器扩展已不被服务器支持。请将浏览器扩展更新到最新版本。",
|
||||
"serverVersionNotSupported": "AliasVault服务器需要更新到新版本才能使用此浏览器扩展。如需帮助,请联系支持人员。",
|
||||
"unknownError": "发生未知错误",
|
||||
"failedToStoreVault": "存储保险库失败",
|
||||
"vaultNotAvailable": "保险库不可用",
|
||||
"failedToRetrieveData": "无法检索数据",
|
||||
"vaultIsLocked": "保险库已锁定",
|
||||
"failedToUploadVault": "上传保险库失败",
|
||||
"passwordChanged": "登录密码已更新,请重新登录以确保账户安全。"
|
||||
},
|
||||
"apiErrors": {
|
||||
"UNKNOWN_ERROR": "发生未知错误。请重试。",
|
||||
"ACCOUNT_LOCKED": "由于多次尝试失败,账户已暂时锁定。请稍后重试。",
|
||||
"ACCOUNT_BLOCKED": "你的账户已被禁用。如果你认为这是误操作,请联系支持人员。",
|
||||
"USER_NOT_FOUND": "用户名或密码无效。请重试。",
|
||||
"INVALID_AUTHENTICATOR_CODE": "认证器验证码无效。请重试。",
|
||||
"INVALID_RECOVERY_CODE": "恢复码无效。请重试。",
|
||||
"REFRESH_TOKEN_REQUIRED": "需要刷新令牌。",
|
||||
"INVALID_REFRESH_TOKEN": "无效的刷新令牌。",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "刷新令牌已成功注销。",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "当前服务器已禁用新账户注册。请联系管理员。",
|
||||
"USERNAME_REQUIRED": "用户名是必填项。",
|
||||
"USERNAME_ALREADY_IN_USE": "用户名已被使用。",
|
||||
"USERNAME_AVAILABLE": "用户名可用。",
|
||||
"USERNAME_MISMATCH": "用户名与当前用户不匹配。",
|
||||
"PASSWORD_MISMATCH": "提供的密码与你的当前密码不匹配。",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "账户已成功删除。",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "用户名不能为空或仅含空格。",
|
||||
"USERNAME_TOO_SHORT": "用户名过短:至少需要3个字符。",
|
||||
"USERNAME_TOO_LONG": "用户名过长:不能超过40个字符。",
|
||||
"USERNAME_INVALID_EMAIL": "无效的邮箱地址。",
|
||||
"USERNAME_INVALID_CHARACTERS": "用户名无效,只能包含字母或数字。",
|
||||
"VAULT_NOT_UP_TO_DATE": "你的保险库不是最新的。请同步你的保险库后重试。",
|
||||
"INTERNAL_SERVER_ERROR": "服务器内部错误。",
|
||||
"VAULT_ERROR": "本地保险库不是最新的。请刷新页面同步保险库后重试。"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"or": "或者",
|
||||
"new": "新建",
|
||||
"cancel": "取消",
|
||||
"search": "搜索",
|
||||
"vaultLocked": "AliasVault已锁定。",
|
||||
"creatingNewAlias": "正在创建新别名……",
|
||||
"noMatchesFound": "未找到匹配项",
|
||||
"searchVault": "搜索保险库……",
|
||||
"serviceName": "服务名称",
|
||||
"email": "电子邮箱",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"enterServiceName": "输入服务名称",
|
||||
"enterEmailAddress": "输入邮箱地址",
|
||||
"enterUsername": "输入用户名",
|
||||
"hideFor1Hour": "隐藏1小时(当前网站)",
|
||||
"hidePermanently": "永久隐藏(当前网站)",
|
||||
"createRandomAlias": "创建随机别名",
|
||||
"createUsernamePassword": "创建用户名/密码",
|
||||
"randomAlias": "随机别名",
|
||||
"usernamePassword": "用户名/密码",
|
||||
"createAndSaveAlias": "创建并保存别名",
|
||||
"createAndSaveCredential": "创建并保存凭证",
|
||||
"randomIdentityDescription": "生成一个包含随机邮箱地址的随机身份,可在AliasVault中查看。",
|
||||
"randomIdentityDescriptionDropdown": "带随机邮箱的随机身份",
|
||||
"manualCredentialDescription": "指定你自己的邮箱地址和用户名。",
|
||||
"manualCredentialDescriptionDropdown": "手动输入用户名和密码",
|
||||
"failedToCreateIdentity": "创建身份失败。请重试。",
|
||||
"enterEmailAndOrUsername": "输入邮箱和/或用户名",
|
||||
"autofillWithAliasVault": "使用AliasVault自动填充",
|
||||
"generateRandomPassword": "生成随机密码(复制到剪贴板)",
|
||||
"generateNewPassword": "生成新密码",
|
||||
"togglePasswordVisibility": "切换密码可见性",
|
||||
"passwordCopiedToClipboard": "密码已复制到剪贴板",
|
||||
"enterEmailAndOrUsernameError": "请输入邮箱和/或用户名",
|
||||
"openAliasVaultToUpgrade": "打开AliasVault进行升级",
|
||||
"vaultUpgradeRequired": "需要升级保险库。",
|
||||
"dismissPopup": "关闭弹窗"
|
||||
},
|
||||
"credentials": {
|
||||
"title": "凭证",
|
||||
"addCredential": "添加凭证",
|
||||
"editCredential": "编辑凭证",
|
||||
"deleteCredential": "删除凭证",
|
||||
"credentialDetails": "凭证详情",
|
||||
"serviceName": "服务名称",
|
||||
"serviceNamePlaceholder": "例如:Gmail、Facebook、银行",
|
||||
"website": "网站",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "用户名",
|
||||
"usernamePlaceholder": "输入用户名",
|
||||
"password": "密码",
|
||||
"passwordPlaceholder": "输入密码",
|
||||
"generatePassword": "生成密码",
|
||||
"copyPassword": "复制密码",
|
||||
"showPassword": "显示密码",
|
||||
"hidePassword": "隐藏密码",
|
||||
"notes": "备注",
|
||||
"notesPlaceholder": "添加备注……",
|
||||
"totp": "双因素认证(2FA)",
|
||||
"totpCode": "TOTP验证码",
|
||||
"copyTotp": "复制 TOTP",
|
||||
"totpSecret": "TOTP密钥",
|
||||
"totpSecretPlaceholder": "输入TOTP密钥",
|
||||
"noCredentials": "未找到凭证",
|
||||
"noCredentialsDescription": "添加你的第一个凭证开始使用",
|
||||
"searchPlaceholder": "搜索凭证……",
|
||||
"welcomeTitle": "欢迎使用AliasVault!",
|
||||
"welcomeDescription": "要使用AliasVault浏览器扩展:导航到某个网站,使用AliasVault自动填充弹窗创建新凭证。",
|
||||
"createdAt": "创建时间",
|
||||
"updatedAt": "最后更新",
|
||||
"autofill": "自动填充",
|
||||
"fillForm": "填充表单",
|
||||
"deleteConfirm": "确定要删除此凭证吗?",
|
||||
"saveSuccess": "凭证保存成功",
|
||||
"tags": "标签",
|
||||
"addTag": "添加标签",
|
||||
"removeTag": "移除标签",
|
||||
"folder": "文件夹",
|
||||
"selectFolder": "选择文件夹",
|
||||
"createFolder": "创建文件夹",
|
||||
"saveCredential": "保存凭证",
|
||||
"deleteCredentialTitle": "删除凭证",
|
||||
"deleteCredentialConfirm": "确定要删除此凭证吗?此操作无法撤销。",
|
||||
"randomAlias": "随机别名",
|
||||
"manual": "手动",
|
||||
"service": "服务",
|
||||
"serviceUrl": "服务 URL",
|
||||
"loginCredentials": "登录凭证",
|
||||
"generateRandomUsername": "生成随机用户名",
|
||||
"generateRandomPassword": "生成随机密码",
|
||||
"changePasswordComplexity": "修改密码复杂度",
|
||||
"passwordLength": "密码长度",
|
||||
"includeLowercase": "包含小写字母",
|
||||
"includeUppercase": "包含大写字母",
|
||||
"includeNumbers": "包含数字",
|
||||
"includeSpecialChars": "包含特殊字符",
|
||||
"avoidAmbiguousChars": "避免易混淆字符(o、0 等)",
|
||||
"generateNewPreview": "生成新预览",
|
||||
"generateRandomAlias": "生成随机别名",
|
||||
"clearAliasFields": "清除别名字段",
|
||||
"alias": "别名",
|
||||
"firstName": "名",
|
||||
"lastName": "姓",
|
||||
"nickName": "昵称",
|
||||
"gender": "性别",
|
||||
"birthDate": "出生日期",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "备注",
|
||||
"validation": {
|
||||
"required": "此字段为必填项",
|
||||
"serviceNameRequired": "服务名称为必填项",
|
||||
"invalidEmail": "无效的邮箱格式",
|
||||
"invalidDateFormat": "日期必须为 YYYY-MM-DD 格式"
|
||||
},
|
||||
"privateEmailTitle": "私人邮箱",
|
||||
"privateEmailAliasVaultServer": "AliasVault服务器",
|
||||
"privateEmailDescription": "端对端加密,完全私密。",
|
||||
"publicEmailTitle": "公开临时邮箱供应商",
|
||||
"publicEmailDescription": "匿名但隐私有限。任何知晓该邮箱地址的人均可查看邮件内容。",
|
||||
"useDomainChooser": "使用域名选择器",
|
||||
"enterCustomDomain": "输入自定义域名",
|
||||
"enterFullEmail": "输入验证邮箱地址",
|
||||
"enterEmailPrefix": "输入邮箱前缀"
|
||||
},
|
||||
"emails": {
|
||||
"title": "邮件",
|
||||
"deleteEmailTitle": "删除邮件",
|
||||
"deleteEmailConfirm": "确定要永久删除此邮件吗?",
|
||||
"from": "发件人",
|
||||
"to": "收件人",
|
||||
"date": "日期",
|
||||
"emailContent": "邮件内容",
|
||||
"attachments": "附件",
|
||||
"emailNotFound": "未找到邮件",
|
||||
"noEmails": "未找到邮件",
|
||||
"noEmailsDescription": "你的私人邮箱地址尚未收到任何邮件。当你收到新邮件时,会显示在这里。",
|
||||
"dateFormat": {
|
||||
"justNow": "刚刚",
|
||||
"minutesAgo_single": "{{count}}分钟前",
|
||||
"minutesAgo_plural": "{{count}}分钟前",
|
||||
"hoursAgo_single": "{{count}}小时前",
|
||||
"hoursAgo_plural": "{{count}}小时前",
|
||||
"yesterday": "昨天"
|
||||
},
|
||||
"errors": {
|
||||
"emailLoadError": "加载邮件时发生错误。请稍后重试。",
|
||||
"emailUnexpectedError": "加载邮件时发生意外错误。请稍后重试。"
|
||||
},
|
||||
"apiErrors": {
|
||||
"CLAIM_DOES_NOT_MATCH_USER": "当前选择的邮箱地址已被使用。请通过编辑此凭证修改邮箱地址。",
|
||||
"CLAIM_DOES_NOT_EXIST": "加载邮件时发生错误。请尝试编辑并保存凭证条目以同步数据库,然后重试。"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
"serverUrl": "服务器 URL",
|
||||
"language": "语言",
|
||||
"autofillEnabled": "启用自动填充",
|
||||
"version": "版本",
|
||||
"openInNewWindow": "在新窗口中打开",
|
||||
"openWebApp": "打开网页应用",
|
||||
"loggedIn": "已登录",
|
||||
"logout": "退出登录",
|
||||
"globalSettings": "全局设置",
|
||||
"autofillPopup": "自动填充弹窗",
|
||||
"activeOnAllSites": "在所有网站上激活(除非在下方禁用)",
|
||||
"disabledOnAllSites": "在所有网站上禁用",
|
||||
"enabled": "启用",
|
||||
"disabled": "禁用",
|
||||
"rightClickContextMenu": "右键上下文菜单",
|
||||
"autofillMatching": "自动填充匹配",
|
||||
"autofillMatchingMode": "自动填充匹配模式",
|
||||
"autofillMatchingModeDescription": "用于判定哪些凭证会被视为匹配项,并在指定网站的自动填充弹窗中显示为建议选项。",
|
||||
"autofillMatchingDefault": "URL + 子域名 + 名称通配符",
|
||||
"autofillMatchingUrlSubdomain": "URL + 子域名",
|
||||
"autofillMatchingUrlExact": "精确匹配URL域名",
|
||||
"siteSpecificSettings": "网站特定设置",
|
||||
"autofillPopupOn": "自动填充弹窗在以下位置: ",
|
||||
"enabledForThisSite": "对此网站启用",
|
||||
"disabledForThisSite": "对此网站禁用",
|
||||
"temporarilyDisabledUntil": "暂时禁用至 ",
|
||||
"resetAllSiteSettings": "重置所有网站特定设置",
|
||||
"appearance": "外观",
|
||||
"theme": "主题",
|
||||
"useDefault": "使用默认",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"keyboardShortcuts": "键盘快捷键",
|
||||
"configureKeyboardShortcuts": "配置键盘快捷键",
|
||||
"configure": "配置",
|
||||
"security": "安全",
|
||||
"clipboardClearTimeout": "复制后清楚剪切板",
|
||||
"clipboardClearTimeoutDescription": "复制敏感数据后自动清除剪贴板",
|
||||
"clipboardClearDisabled": "从不清除",
|
||||
"clipboardClear5Seconds": "5秒后清除",
|
||||
"clipboardClear10Seconds": "10 秒后清除",
|
||||
"clipboardClear15Seconds": "15分钟后清除",
|
||||
"autoLockTimeout": "自动锁定超时时间",
|
||||
"autoLockTimeoutDescription": "无操作一段时间后自动锁定保险库",
|
||||
"autoLockTimeoutHelp": "保险库仅在达到指定的无操作时长后才会锁定(未使用自动填充功能或未打开扩展程序弹窗)。无论此设置如何,关闭浏览器时保险库始终会锁定。",
|
||||
"autoLockNever": "从不",
|
||||
"autoLock15Seconds": "15秒",
|
||||
"autoLock1Minute": "1分钟",
|
||||
"autoLock5Minutes": "5分钟",
|
||||
"autoLock15Minutes": "15分钟",
|
||||
"autoLock30Minutes": "30分钟",
|
||||
"autoLock1Hour": "1小时",
|
||||
"autoLock4Hours": "4小时",
|
||||
"autoLock8Hours": "8小时",
|
||||
"autoLock24Hours": "24小时",
|
||||
"versionPrefix": "版本 ",
|
||||
"preferences": "首选项",
|
||||
"autofillSettings": "自动填充设置",
|
||||
"clipboardSettings": "剪切板设置",
|
||||
"contextMenuSettings": "上下文菜单设置",
|
||||
"contextMenu": "上下文菜单",
|
||||
"contextMenuEnabled": "上下文菜单已启用",
|
||||
"contextMenuDisabled": "上下文菜单已停用",
|
||||
"contextMenuDescription": "右键点击输入字段即可访问 AliasVault 选项",
|
||||
"selectLanguage": "选择语言",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL 为必填项",
|
||||
"apiUrlInvalid": "请输入有效的 API URL",
|
||||
"clientUrlRequired": "客户端 URL 为必填项",
|
||||
"clientUrlInvalid": "请输入有效的客户端 URL"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "升级保险库",
|
||||
"subtitle": "AliasVault已更新,你的保险库需要升级,仅需数秒。",
|
||||
"versionInformation": "版本信息",
|
||||
"yourVault": "你的保险库:",
|
||||
"newVersion": "新版本:",
|
||||
"upgrade": "升级保险库",
|
||||
"upgrading": "升级中……",
|
||||
"logout": "退出登录",
|
||||
"whatsNew": "新功能",
|
||||
"whatsNewDescription": "需要升级以支持以下变更:",
|
||||
"noDescriptionAvailable": "此版本无可用说明。",
|
||||
"okay": "确定",
|
||||
"status": {
|
||||
"preparingUpgrade": "准备升级中……",
|
||||
"vaultAlreadyUpToDate": "当前保险库数据已是最新",
|
||||
"startingDatabaseTransaction": "开始数据库事务……",
|
||||
"applyingDatabaseMigrations": "应用数据库迁移……",
|
||||
"applyingMigration": "应用迁移 {{current}} / {{total}}……",
|
||||
"committingChanges": "提交更改中……"
|
||||
},
|
||||
"alerts": {
|
||||
"error": "错误",
|
||||
"unableToGetVersionInfo": "无法获取版本信息。请重试。",
|
||||
"selfHostedServer": "自托管服务器",
|
||||
"selfHostedWarning": "如果你使用的是自托管服务器,请确保同时更新你的自托管实例,否则将无法登录网页客户端。",
|
||||
"cancel": "取消",
|
||||
"continueUpgrade": "继续升级",
|
||||
"upgradeFailed": "升级失败",
|
||||
"failedToApplyMigration": "应用迁移失败({{current}} / {{total}})",
|
||||
"unknownErrorDuringUpgrade": "升级过程中发生未知错误。请重试。"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export class AppInfo {
|
||||
/**
|
||||
* The current extension version. This should be updated with each release of the extension.
|
||||
*/
|
||||
public static readonly VERSION = '0.21.2';
|
||||
public static readonly VERSION = '0.23.2';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault server (API) version. If the server version is below this, the
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// TODO: store generic setting constants somewhere else.
|
||||
export const DISABLED_SITES_KEY = 'local:aliasvault_disabled_sites';
|
||||
export const GLOBAL_AUTOFILL_POPUP_ENABLED_KEY = 'local:aliasvault_global_autofill_popup_enabled';
|
||||
export const GLOBAL_CONTEXT_MENU_ENABLED_KEY = 'local:aliasvault_global_context_menu_enabled';
|
||||
export const VAULT_LOCKED_DISMISS_UNTIL_KEY = 'local:aliasvault_vault_locked_dismiss_until';
|
||||
export const TEMPORARY_DISABLED_SITES_KEY = 'local:aliasvault_temporary_disabled_sites';
|
||||
export const CLIPBOARD_CLEAR_TIMEOUT_KEY = 'local:aliasvault_clipboard_clear_timeout';
|
||||
export const AUTO_LOCK_TIMEOUT_KEY = 'local:aliasvault_auto_lock_timeout';
|
||||
export const AUTOFILL_MATCHING_MODE_KEY = 'local:aliasvault_autofill_matching_mode';
|
||||
|
||||
// TODO: store these settings in the actual vault when updating the datamodel for roadmap v1.0.
|
||||
export const LAST_CUSTOM_EMAIL_KEY = 'local:aliasvault_last_custom_email';
|
||||
export const LAST_CUSTOM_USERNAME_KEY = 'local:aliasvault_last_custom_username';
|
||||
export const CUSTOM_EMAIL_HISTORY_KEY = 'local:aliasvault_custom_email_history';
|
||||
export const CUSTOM_USERNAME_HISTORY_KEY = 'local:aliasvault_custom_username_history';
|
||||
export const SKIP_FORM_RESTORE_KEY = 'local:aliasvault_skip_form_restore';
|
||||
|
||||
@@ -357,6 +357,7 @@ export class SqliteClient {
|
||||
const isValidDomain = (domain: string): boolean => {
|
||||
return Boolean(domain &&
|
||||
domain !== 'DISABLED.TLD' &&
|
||||
domain !== '' &&
|
||||
(privateEmailDomains.includes(domain) || publicEmailDomains.includes(domain)));
|
||||
};
|
||||
|
||||
|
||||
@@ -262,7 +262,8 @@ export class WebApiService {
|
||||
return {
|
||||
clientVersionSupported: true,
|
||||
serverVersion: '0.0.0',
|
||||
vaultRevision: 0
|
||||
vaultRevision: 0,
|
||||
srpSalt: ''
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -272,15 +273,15 @@ export class WebApiService {
|
||||
*/
|
||||
public validateStatusResponse(statusResponse: StatusResponse): string | null {
|
||||
if (statusResponse.serverVersion === '0.0.0') {
|
||||
return 'errors.serverNotAvailable';
|
||||
return 'serverNotAvailable';
|
||||
}
|
||||
|
||||
if (!statusResponse.clientVersionSupported) {
|
||||
return 'errors.clientVersionNotSupported';
|
||||
return 'clientVersionNotSupported';
|
||||
}
|
||||
|
||||
if (!AppInfo.isServerVersionSupported(statusResponse.serverVersion)) {
|
||||
return 'errors.serverVersionNotSupported';
|
||||
return 'serverVersionNotSupported';
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -296,17 +297,13 @@ export class WebApiService {
|
||||
*/
|
||||
if (vaultResponseJson.status === 1) {
|
||||
// Note: vault merge is no longer allowed by the API as of 0.20.0, updates with the same revision number are rejected. So this check can be removed later.
|
||||
return t('errors.VaultMergeRequired');
|
||||
return t('errors.VaultOutdated');
|
||||
}
|
||||
|
||||
if (vaultResponseJson.status === 2) {
|
||||
return t('errors.VaultOutdated');
|
||||
}
|
||||
|
||||
if (!vaultResponseJson.vault?.blob) {
|
||||
return t('errors.NoVaultFound');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ type StatusResponse = {
|
||||
clientVersionSupported: boolean;
|
||||
serverVersion: string;
|
||||
vaultRevision: number;
|
||||
srpSalt: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -169,6 +169,37 @@ export class FormDetector {
|
||||
return this.clickedElement?.closest('form, [role="dialog"]') as HTMLElement | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual input element from a potentially custom element.
|
||||
* This handles any element with shadow DOM containing input elements.
|
||||
* @param element The element to check (could be a custom element or regular input)
|
||||
* @returns The actual input element, or the original element if no nested input is found
|
||||
*/
|
||||
private getActualInputElement(element: HTMLElement): HTMLElement {
|
||||
// If it's already an input, return it
|
||||
if (element.tagName.toLowerCase() === 'input') {
|
||||
return element;
|
||||
}
|
||||
|
||||
// Check for shadow DOM input (generic approach)
|
||||
const elementWithShadow = element as HTMLElement & { shadowRoot?: ShadowRoot };
|
||||
if (elementWithShadow.shadowRoot) {
|
||||
const shadowInput = elementWithShadow.shadowRoot.querySelector('input, textarea') as HTMLElement;
|
||||
if (shadowInput) {
|
||||
return shadowInput;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for regular child input (non-shadow DOM)
|
||||
const childInput = element.querySelector('input, textarea') as HTMLElement;
|
||||
if (childInput) {
|
||||
return childInput;
|
||||
}
|
||||
|
||||
// Return the original element if no nested input found
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an element and all its parents are visible.
|
||||
* This checks for display:none, visibility:hidden, and opacity:0
|
||||
@@ -257,10 +288,32 @@ export class FormDetector {
|
||||
types: string[],
|
||||
excludeElements: HTMLInputElement[] = []
|
||||
): HTMLInputElement[] {
|
||||
// Query for both standard input elements and any element with a type attribute
|
||||
const candidates = form
|
||||
? form.querySelectorAll<HTMLElement>('input, select, [type]')
|
||||
: this.document.querySelectorAll<HTMLElement>('input, select, [type]');
|
||||
// Query for standard input elements, select elements, and elements with type attributes
|
||||
const standardCandidates = form
|
||||
? Array.from(form.querySelectorAll<HTMLElement>('input, select, [type]'))
|
||||
: Array.from(this.document.querySelectorAll<HTMLElement>('input, select, [type]'));
|
||||
|
||||
/**
|
||||
* Also find any custom elements that might contain shadow DOM inputs
|
||||
* Look for elements with shadow roots that contain input elements
|
||||
*/
|
||||
const allElements = form
|
||||
? Array.from(form.querySelectorAll<HTMLElement>('*'))
|
||||
: Array.from(this.document.querySelectorAll<HTMLElement>('*'));
|
||||
|
||||
const shadowDOMCandidates = allElements.filter(el => {
|
||||
// Check if element has shadow DOM with input elements
|
||||
const elementWithShadow = el as HTMLElement & { shadowRoot?: ShadowRoot };
|
||||
if (elementWithShadow.shadowRoot) {
|
||||
const shadowInput = elementWithShadow.shadowRoot.querySelector('input, textarea');
|
||||
return shadowInput !== null;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Combine and deduplicate candidates
|
||||
const allCandidates = [...standardCandidates, ...shadowDOMCandidates];
|
||||
const candidates = allCandidates.filter((el, index, arr) => arr.indexOf(el) === index);
|
||||
|
||||
const matches: { input: HTMLInputElement; score: number }[] = [];
|
||||
|
||||
@@ -274,12 +327,32 @@ export class FormDetector {
|
||||
}
|
||||
|
||||
// Get type from either the element's type property or its type attribute
|
||||
const type = input.tagName.toLowerCase() === 'select'
|
||||
const tagName = input.tagName.toLowerCase();
|
||||
let type = tagName === 'select'
|
||||
? 'select'
|
||||
: (input as HTMLInputElement).type?.toLowerCase() || input.getAttribute('type')?.toLowerCase() || '';
|
||||
|
||||
// Check if element has shadow DOM with input elements (generic detection)
|
||||
const elementWithShadow = input as HTMLElement & { shadowRoot?: ShadowRoot };
|
||||
const hasShadowDOMInput = elementWithShadow.shadowRoot &&
|
||||
elementWithShadow.shadowRoot.querySelector('input, textarea');
|
||||
|
||||
// For elements with shadow DOM, get the type from the actual input inside
|
||||
if (hasShadowDOMInput && !type) {
|
||||
const shadowInput = elementWithShadow.shadowRoot!.querySelector('input, textarea') as HTMLInputElement;
|
||||
if (shadowInput) {
|
||||
type = shadowInput.type?.toLowerCase() || 'text';
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this element should be considered based on type matching
|
||||
if (!types.includes(type)) {
|
||||
continue;
|
||||
// For shadow DOM elements, allow if we're looking for text and it contains an input
|
||||
if (hasShadowDOMInput && types.includes('text') && !type) {
|
||||
// This is a shadow DOM element without explicit type, treat as text input
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (types.includes('email') && type === 'email') {
|
||||
@@ -303,6 +376,34 @@ export class FormDetector {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for slot-based labels (e.g., <span slot="label">Email or username</span>)
|
||||
* Look for slot elements within the input's parent hierarchy
|
||||
*/
|
||||
let slotParent: HTMLElement | null = input;
|
||||
for (let depth = 0; depth < 3 && slotParent; depth++) {
|
||||
const slotElements = slotParent.querySelectorAll('[slot="label"], [slot="helper-text"]');
|
||||
for (const slotEl of Array.from(slotElements)) {
|
||||
const slotText = slotEl.textContent?.toLowerCase() ?? '';
|
||||
if (slotText) {
|
||||
attributesToCheck.push(slotText);
|
||||
}
|
||||
}
|
||||
/** Also check if the parent itself is a custom element with slots */
|
||||
if (slotParent.shadowRoot) {
|
||||
const shadowSlots = slotParent.shadowRoot.querySelectorAll('slot[name="label"], slot[name="helper-text"]');
|
||||
for (const slot of Array.from(shadowSlots)) {
|
||||
const assignedNodes = (slot as HTMLSlotElement).assignedNodes();
|
||||
for (const node of assignedNodes) {
|
||||
if (node.textContent) {
|
||||
attributesToCheck.push(node.textContent.toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
slotParent = slotParent.parentElement;
|
||||
}
|
||||
|
||||
// Check for sibling elements with class containing "label"
|
||||
const parent = input.parentElement;
|
||||
if (parent) {
|
||||
@@ -378,14 +479,18 @@ export class FormDetector {
|
||||
excludeElements: HTMLInputElement[] = []
|
||||
): HTMLInputElement | null {
|
||||
const all = this.findAllInputFields(form, patterns, types, excludeElements);
|
||||
|
||||
// Filter out parent-child duplicates
|
||||
const filtered = this.filterOutNestedDuplicates(all);
|
||||
|
||||
// if email type explicitly requested, prefer actual <input type="email">
|
||||
if (types.includes('email')) {
|
||||
const emailMatch = all.find(i => (i.type || '').toLowerCase() === 'email');
|
||||
const emailMatch = filtered.find(i => (i.type || '').toLowerCase() === 'email');
|
||||
if (emailMatch) {
|
||||
return emailMatch;
|
||||
}
|
||||
}
|
||||
return all.length > 0 ? all[0] : null;
|
||||
return filtered.length > 0 ? filtered[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -395,25 +500,32 @@ export class FormDetector {
|
||||
primary: HTMLInputElement | null,
|
||||
confirm: HTMLInputElement | null
|
||||
} {
|
||||
// Find primary email field
|
||||
const primaryEmail = this.findInputField(
|
||||
// Find all email fields first
|
||||
const emailFields = this.findAllInputFields(
|
||||
form,
|
||||
CombinedFieldPatterns.email,
|
||||
['text', 'email']
|
||||
);
|
||||
|
||||
// Filter out parent-child relationships
|
||||
const filteredEmailFields = this.filterOutNestedDuplicates(emailFields);
|
||||
const primaryEmail = filteredEmailFields[0] ?? null;
|
||||
|
||||
/*
|
||||
* Find confirmation email field if primary exists
|
||||
* and ensure it's not the same as the primary email field.
|
||||
*/
|
||||
const confirmEmail = primaryEmail
|
||||
? this.findInputField(
|
||||
const confirmEmailFields = primaryEmail
|
||||
? this.findAllInputFields(
|
||||
form,
|
||||
CombinedFieldPatterns.emailConfirm,
|
||||
['text', 'email'],
|
||||
[primaryEmail]
|
||||
)
|
||||
: null;
|
||||
: [];
|
||||
|
||||
const filteredConfirmFields = this.filterOutNestedDuplicates(confirmEmailFields);
|
||||
const confirmEmail = filteredConfirmFields[0] ?? null;
|
||||
|
||||
return {
|
||||
primary: primaryEmail,
|
||||
@@ -566,6 +678,56 @@ export class FormDetector {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out nested duplicates where a parent element and its child are both detected.
|
||||
* This happens with custom elements that contain actual input elements.
|
||||
* We prefer the innermost actual input element over the parent custom element.
|
||||
*/
|
||||
private filterOutNestedDuplicates(fields: HTMLInputElement[]): HTMLInputElement[] {
|
||||
if (fields.length <= 1) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
const filtered: HTMLInputElement[] = [];
|
||||
|
||||
for (const field of fields) {
|
||||
let shouldInclude = true;
|
||||
|
||||
// Check if this field is a parent of any other field in the list
|
||||
for (const otherField of fields) {
|
||||
if (field !== otherField) {
|
||||
// Check if field contains otherField (field is parent)
|
||||
if (field.contains(otherField)) {
|
||||
shouldInclude = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if field's shadow DOM contains otherField
|
||||
const fieldWithShadow = field as HTMLElement & { shadowRoot?: ShadowRoot };
|
||||
if (fieldWithShadow.shadowRoot && fieldWithShadow.shadowRoot.contains(otherField)) {
|
||||
shouldInclude = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldInclude) {
|
||||
// Also check if this field is not already represented by its actual input
|
||||
const actualInput = this.getActualInputElement(field);
|
||||
if (actualInput !== field) {
|
||||
// If the actual input is also in the list, skip the parent
|
||||
if (fields.includes(actualInput as HTMLInputElement)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
filtered.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the password field in a form.
|
||||
*/
|
||||
@@ -575,9 +737,12 @@ export class FormDetector {
|
||||
} {
|
||||
const passwordFields = this.findAllInputFields(form, CombinedFieldPatterns.password, ['password']);
|
||||
|
||||
// Filter out parent-child relationships to avoid detecting the same field twice
|
||||
const filteredFields = this.filterOutNestedDuplicates(passwordFields);
|
||||
|
||||
return {
|
||||
primary: passwordFields[0] ?? null,
|
||||
confirm: passwordFields[1] ?? null
|
||||
primary: filteredFields[0] ?? null,
|
||||
confirm: filteredFields[1] ?? null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -631,21 +796,34 @@ export class FormDetector {
|
||||
// Check if it's a username, email or password field by reusing the existing detection logic
|
||||
const formWrapper = this.getFormWrapper();
|
||||
|
||||
// Check if the clicked element is a username field.
|
||||
if (!this.clickedElement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the actual input element (handles shadow DOM)
|
||||
const actualElement = this.getActualInputElement(this.clickedElement);
|
||||
|
||||
// Check both the clicked element and the actual input element
|
||||
const elementsToCheck = [this.clickedElement, actualElement].filter((el, index, arr) =>
|
||||
el && arr.indexOf(el) === index // Remove duplicates
|
||||
);
|
||||
|
||||
// Check if any of the elements is a username field
|
||||
const usernameFields = this.findAllInputFields(formWrapper as HTMLFormElement | null, CombinedFieldPatterns.username, ['text']);
|
||||
if (usernameFields.some(input => input === this.clickedElement)) {
|
||||
if (usernameFields.some(input => elementsToCheck.includes(input))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the clicked element is a password field.
|
||||
// Check if any of the elements is a password field
|
||||
const passwordField = this.findPasswordField(formWrapper as HTMLFormElement | null);
|
||||
if (passwordField.primary === this.clickedElement || passwordField.confirm === this.clickedElement) {
|
||||
if ((passwordField.primary && elementsToCheck.includes(passwordField.primary)) ||
|
||||
(passwordField.confirm && elementsToCheck.includes(passwordField.confirm))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the clicked element is an email field.
|
||||
// Check if any of the elements is an email field
|
||||
const emailFields = this.findAllInputFields(formWrapper as HTMLFormElement | null, CombinedFieldPatterns.email, ['text', 'email']);
|
||||
if (emailFields.some(input => input === this.clickedElement)) {
|
||||
if (emailFields.some(input => elementsToCheck.includes(input))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@ import { Gender, IdentityHelperUtils } from "@/utils/dist/shared/identity-genera
|
||||
import type { Credential } from "@/utils/dist/shared/models/vault";
|
||||
import { CombinedDateOptionPatterns, CombinedGenderOptionPatterns } from "@/utils/formDetector/FieldPatterns";
|
||||
import { FormFields } from "@/utils/formDetector/types/FormFields";
|
||||
import { ClickValidator } from "@/utils/security/ClickValidator";
|
||||
/**
|
||||
* Class to fill the fields of a form with the given credential.
|
||||
*/
|
||||
export class FormFiller {
|
||||
private readonly clickValidator = ClickValidator.getInstance();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
@@ -23,33 +26,267 @@ export class FormFiller {
|
||||
* Fill the fields of the form with the given credential.
|
||||
* @param credential The credential to fill the form with.
|
||||
*/
|
||||
public fillFields(credential: Credential): void {
|
||||
this.fillBasicFields(credential);
|
||||
public async fillFields(credential: Credential): Promise<void> {
|
||||
// Perform security validation before filling any fields
|
||||
if (!await this.validateFormSecurity()) {
|
||||
console.warn('[AliasVault Security] Autofill blocked due to security validation failure');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill basic fields and password fields in parallel
|
||||
await Promise.all([
|
||||
this.fillBasicFields(credential),
|
||||
this.fillPasswordFields(credential)
|
||||
]);
|
||||
|
||||
this.fillBirthdateFields(credential);
|
||||
this.fillGenderFields(credential);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate form security to prevent autofill in potential clickjacking scenarios.
|
||||
* This method checks for various attack vectors including:
|
||||
* - Page-wide opacity manipulation
|
||||
* - Form field obstruction via overlays
|
||||
* - Suspicious element positioning
|
||||
* - Multiple forms with identical fields (potential decoy attacks)
|
||||
*/
|
||||
private async validateFormSecurity(): Promise<boolean> {
|
||||
try {
|
||||
// Skip security validation in test environments where browser APIs may not be available
|
||||
if (typeof window === 'undefined' || typeof MouseEvent === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 1. Check page-wide security using ClickValidator (detects body/HTML opacity tricks)
|
||||
const dummyEvent = new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX: window.innerWidth / 2,
|
||||
clientY: window.innerHeight / 2
|
||||
});
|
||||
// Note: isTrusted is read-only and set by the browser
|
||||
|
||||
if (!await this.clickValidator.validateClick(dummyEvent)) {
|
||||
console.warn('[AliasVault Security] Form autofill blocked: Page-wide attack detected');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Check form field obstruction and positioning
|
||||
const formFields = this.getAllFormFields();
|
||||
for (const field of formFields) {
|
||||
if (!this.validateFieldSecurity(field)) {
|
||||
console.warn('[AliasVault Security] Form autofill blocked: Field obstruction detected', field);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check for suspicious form duplication (decoy attack)
|
||||
if (this.detectDecoyForms()) {
|
||||
console.warn('[AliasVault Security] Form autofill blocked: Multiple suspicious forms detected');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[AliasVault Security] Form security validation error:', error);
|
||||
return false; // Fail safely - block autofill if validation fails
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all form fields that will be filled.
|
||||
*/
|
||||
private getAllFormFields(): HTMLElement[] {
|
||||
const fields: HTMLElement[] = [];
|
||||
|
||||
if (this.form.usernameField) {
|
||||
fields.push(this.form.usernameField);
|
||||
}
|
||||
if (this.form.passwordField) {
|
||||
fields.push(this.form.passwordField);
|
||||
}
|
||||
if (this.form.passwordConfirmField) {
|
||||
fields.push(this.form.passwordConfirmField);
|
||||
}
|
||||
if (this.form.emailField) {
|
||||
fields.push(this.form.emailField);
|
||||
}
|
||||
if (this.form.emailConfirmField) {
|
||||
fields.push(this.form.emailConfirmField);
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate individual field security to detect obstruction attacks.
|
||||
*/
|
||||
private validateFieldSecurity(field: HTMLElement): boolean {
|
||||
if (!field) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip field validation in test environments where browser APIs may not be available
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined' || !document.elementsFromPoint) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const rect = field.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
|
||||
// Check if field is within viewport
|
||||
if (rect.width === 0 || rect.height === 0 ||
|
||||
centerX < 0 || centerY < 0 ||
|
||||
centerX > window.innerWidth || centerY > window.innerHeight) {
|
||||
console.warn('[AliasVault Security] Field outside viewport or zero-sized:', rect);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use elementsFromPoint to check what's actually at the field center
|
||||
try {
|
||||
const elementsAtPoint = document.elementsFromPoint(centerX, centerY);
|
||||
|
||||
if (elementsAtPoint.length === 0) {
|
||||
console.warn('[AliasVault Security] No elements found at field center');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if our field is in the element stack (or its parents/children)
|
||||
const fieldFound = elementsAtPoint.some(element =>
|
||||
element === field ||
|
||||
field.contains(element) ||
|
||||
element.contains(field)
|
||||
);
|
||||
|
||||
if (!fieldFound) {
|
||||
console.warn('[AliasVault Security] Field is obstructed by other elements');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for suspicious covering elements
|
||||
const suspiciousCovering = elementsAtPoint.slice(0, 3).some(element => {
|
||||
if (element === field || field.contains(element) || element.contains(field)) {
|
||||
return false; // This is our field or related element
|
||||
}
|
||||
|
||||
const style = getComputedStyle(element);
|
||||
|
||||
// Check for nearly transparent overlays
|
||||
const opacity = parseFloat(style.opacity);
|
||||
if (opacity > 0 && opacity < 0.1) {
|
||||
console.warn('[AliasVault Security] Nearly transparent overlay detected:', element);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for high z-index elements (potential overlays)
|
||||
const zIndex = parseInt(style.zIndex) || 0;
|
||||
if (zIndex > 1000000) {
|
||||
console.warn('[AliasVault Security] Suspicious high z-index element:', element, zIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for elements covering large areas (potential clickjacking overlays)
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
if (elementRect.width >= window.innerWidth * 0.8 &&
|
||||
elementRect.height >= window.innerHeight * 0.8) {
|
||||
console.warn('[AliasVault Security] Large covering element detected:', element);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return !suspiciousCovering;
|
||||
} catch (error) {
|
||||
console.warn('[AliasVault Security] Field validation error:', error);
|
||||
return false; // Fail safely
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect potential decoy forms (multiple forms with similar fields).
|
||||
*/
|
||||
private detectDecoyForms(): boolean {
|
||||
try {
|
||||
// Find all forms on the page
|
||||
const allForms = Array.from(document.querySelectorAll('form'));
|
||||
|
||||
if (allForms.length <= 1) {
|
||||
return false; // Only one form, no decoy risk
|
||||
}
|
||||
|
||||
let suspiciousFormCount = 0;
|
||||
|
||||
for (const form of allForms) {
|
||||
const hasPasswordField = form.querySelector('input[type="password"]');
|
||||
const hasEmailField = form.querySelector('input[type="email"], input[name*="email" i], input[placeholder*="email" i]');
|
||||
const hasUsernameField = form.querySelector('input[type="text"], input[name*="user" i], input[placeholder*="user" i]');
|
||||
|
||||
// Count forms with login-like patterns
|
||||
if (hasPasswordField && (hasEmailField || hasUsernameField)) {
|
||||
const formRect = form.getBoundingClientRect();
|
||||
const isVisible = formRect.width > 0 && formRect.height > 0;
|
||||
|
||||
if (isVisible) {
|
||||
suspiciousFormCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If more than 2 visible login forms, it's suspicious
|
||||
if (suspiciousFormCount > 2) {
|
||||
console.warn('[AliasVault Security] Multiple login forms detected:', suspiciousFormCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.warn('[AliasVault Security] Decoy form detection error:', error);
|
||||
return false; // Don't block on detection errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value on an input element, handling both regular inputs and custom elements with shadow DOM.
|
||||
* @param element The element to set the value on
|
||||
* @param value The value to set
|
||||
*/
|
||||
private setElementValue(element: HTMLInputElement | HTMLSelectElement, value: string): void {
|
||||
// Try to set value directly on the element
|
||||
element.value = value;
|
||||
|
||||
// If it's a custom element with shadow DOM, try to find and fill the actual input
|
||||
if (element.shadowRoot) {
|
||||
const shadowInput = element.shadowRoot.querySelector('input, textarea') as HTMLInputElement;
|
||||
if (shadowInput) {
|
||||
shadowInput.value = value;
|
||||
// Trigger events on the shadow input as well
|
||||
this.triggerInputEvents(shadowInput, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Also check if the element contains a regular child input (non-shadow DOM)
|
||||
const childInput = element.querySelector('input, textarea') as HTMLInputElement;
|
||||
if (childInput && childInput !== element) {
|
||||
childInput.value = value;
|
||||
this.triggerInputEvents(childInput, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the basic fields of the form.
|
||||
* @param credential The credential to fill the form with.
|
||||
*/
|
||||
private fillBasicFields(credential: Credential): void {
|
||||
private async fillBasicFields(credential: Credential): Promise<void> {
|
||||
if (this.form.usernameField && credential.Username) {
|
||||
this.form.usernameField.value = credential.Username;
|
||||
this.triggerInputEvents(this.form.usernameField);
|
||||
}
|
||||
|
||||
if (this.form.passwordField && credential.Password) {
|
||||
this.fillPasswordField(this.form.passwordField, credential.Password);
|
||||
}
|
||||
|
||||
if (this.form.passwordConfirmField && credential.Password) {
|
||||
this.fillPasswordField(this.form.passwordConfirmField, credential.Password);
|
||||
await this.fillTextFieldWithTyping(this.form.usernameField, credential.Username);
|
||||
}
|
||||
|
||||
if (this.form.emailField && (credential.Alias?.Email !== undefined || credential.Username !== undefined)) {
|
||||
if (credential.Alias?.Email) {
|
||||
this.form.emailField.value = credential.Alias.Email;
|
||||
this.setElementValue(this.form.emailField, credential.Alias.Email);
|
||||
this.triggerInputEvents(this.form.emailField);
|
||||
} else if (credential.Username && !this.form.usernameField) {
|
||||
/*
|
||||
@@ -62,54 +299,138 @@ export class FormFiller {
|
||||
* from a previous password manager that only had username/password fields
|
||||
* or where the user manually created a credential with only a username/password.
|
||||
*/
|
||||
this.form.emailField.value = credential.Username;
|
||||
this.setElementValue(this.form.emailField, credential.Username);
|
||||
this.triggerInputEvents(this.form.emailField);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.form.emailConfirmField && credential.Alias?.Email) {
|
||||
this.form.emailConfirmField.value = credential.Alias.Email;
|
||||
this.setElementValue(this.form.emailConfirmField, credential.Alias.Email);
|
||||
this.triggerInputEvents(this.form.emailConfirmField);
|
||||
}
|
||||
|
||||
if (this.form.fullNameField && credential.Alias?.FirstName && credential.Alias?.LastName) {
|
||||
this.form.fullNameField.value = `${credential.Alias.FirstName} ${credential.Alias.LastName}`;
|
||||
this.setElementValue(this.form.fullNameField, `${credential.Alias.FirstName} ${credential.Alias.LastName}`);
|
||||
this.triggerInputEvents(this.form.fullNameField);
|
||||
}
|
||||
|
||||
if (this.form.firstNameField && credential.Alias?.FirstName) {
|
||||
this.form.firstNameField.value = credential.Alias.FirstName;
|
||||
this.setElementValue(this.form.firstNameField, credential.Alias.FirstName);
|
||||
this.triggerInputEvents(this.form.firstNameField);
|
||||
}
|
||||
|
||||
if (this.form.lastNameField && credential.Alias?.LastName) {
|
||||
this.form.lastNameField.value = credential.Alias.LastName;
|
||||
this.setElementValue(this.form.lastNameField, credential.Alias.LastName);
|
||||
this.triggerInputEvents(this.form.lastNameField);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill a text field with character-by-character typing to better simulate human input.
|
||||
* This method is similar to fillPasswordField but optimized for regular text fields.
|
||||
*
|
||||
* @param field The text field to fill.
|
||||
* @param text The text to fill the field with.
|
||||
*/
|
||||
private async fillTextFieldWithTyping(field: HTMLInputElement, text: string): Promise<void> {
|
||||
// Find the actual input element (could be in shadow DOM)
|
||||
let actualInput = field;
|
||||
|
||||
// Check for shadow DOM input
|
||||
if (field.shadowRoot) {
|
||||
const shadowInput = field.shadowRoot.querySelector('input, textarea') as HTMLInputElement;
|
||||
if (shadowInput) {
|
||||
actualInput = shadowInput;
|
||||
}
|
||||
} else if (field.tagName.toLowerCase() !== 'input' && field.tagName.toLowerCase() !== 'textarea') {
|
||||
// Check for child input (non-shadow DOM) only if field is not already an input
|
||||
const childInput = field.querySelector('input, textarea') as HTMLInputElement;
|
||||
if (childInput) {
|
||||
actualInput = childInput;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the field first without triggering events
|
||||
actualInput.value = '';
|
||||
|
||||
// Type each character with a small delay
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
actualInput.value += text[i];
|
||||
|
||||
/*
|
||||
* Small delay between characters to simulate human typing
|
||||
* This helps with sites that have input event handlers
|
||||
*/
|
||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 10 + 10));
|
||||
}
|
||||
|
||||
// Trigger events once after all typing is complete
|
||||
this.triggerInputEvents(actualInput, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill password fields sequentially to avoid visual conflicts.
|
||||
* First fills the main password field, then the confirm field if present.
|
||||
* @param credential The credential containing the password.
|
||||
*/
|
||||
private async fillPasswordFields(credential: Credential): Promise<void> {
|
||||
if (!credential.Password) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill main password field first
|
||||
if (this.form.passwordField) {
|
||||
await this.fillPasswordField(this.form.passwordField, credential.Password);
|
||||
}
|
||||
|
||||
// Then fill password confirm field after main field is complete
|
||||
if (this.form.passwordConfirmField) {
|
||||
await this.fillPasswordField(this.form.passwordConfirmField, credential.Password);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the password field with the given password. This uses a small delay between each character to simulate human typing.
|
||||
* Simulates actual keystroke behavior by appending characters one by one.
|
||||
* Supports both regular inputs and custom elements with shadow DOM.
|
||||
*
|
||||
* @param field The password field to fill.
|
||||
* @param password The password to fill the field with.
|
||||
*/
|
||||
private async fillPasswordField(field: HTMLInputElement, password: string): Promise<void> {
|
||||
// Clear the field first
|
||||
field.value = '';
|
||||
this.triggerInputEvents(field, true);
|
||||
// Find the actual input element (could be in shadow DOM)
|
||||
let actualInput = field;
|
||||
|
||||
// Type each character with a small delay
|
||||
for (const char of password) {
|
||||
// Append the character to the current value instead of using substring
|
||||
field.value += char;
|
||||
// Small random delay between 5-15ms to simulate human typing
|
||||
this.triggerInputEvents(field, false);
|
||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 10 + 5));
|
||||
// Check for shadow DOM input
|
||||
if (field.shadowRoot) {
|
||||
const shadowInput = field.shadowRoot.querySelector('input[type="password"], input') as HTMLInputElement;
|
||||
if (shadowInput) {
|
||||
actualInput = shadowInput;
|
||||
}
|
||||
} else if (field.tagName.toLowerCase() !== 'input') {
|
||||
// Check for child input (non-shadow DOM) only if field is not already an input
|
||||
const childInput = field.querySelector('input[type="password"], input') as HTMLInputElement;
|
||||
if (childInput) {
|
||||
actualInput = childInput;
|
||||
}
|
||||
}
|
||||
|
||||
this.triggerInputEvents(field, false);
|
||||
// Clear the field first without triggering events
|
||||
actualInput.value = '';
|
||||
|
||||
// Type each character with a small delay
|
||||
for (let i = 0; i < password.length; i++) {
|
||||
actualInput.value += password[i];
|
||||
|
||||
/*
|
||||
* Small delay between characters to simulate human typing
|
||||
* This helps with sites that have input event handlers
|
||||
*/
|
||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 10 + 10));
|
||||
}
|
||||
|
||||
// Trigger events once after all typing is complete
|
||||
this.triggerInputEvents(actualInput, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,13 +587,13 @@ export class FormFiller {
|
||||
private fillGenderFields(credential: Credential): void {
|
||||
switch (this.form.genderField.type) {
|
||||
case 'select':
|
||||
this.fillGenderSelect(credential.Alias.Gender);
|
||||
this.fillGenderSelect(credential.Alias.Gender as Gender | undefined);
|
||||
break;
|
||||
case 'radio':
|
||||
this.fillGenderRadio(credential.Alias.Gender);
|
||||
this.fillGenderRadio(credential.Alias.Gender as Gender | undefined);
|
||||
break;
|
||||
case 'text':
|
||||
this.fillGenderText(credential.Alias.Gender);
|
||||
this.fillGenderText(credential.Alias.Gender as Gender | undefined);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,4 +79,75 @@ describe('FormDetector generic tests', () => {
|
||||
expect(form).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Nested custom elements (parent-child duplicate prevention)', () => {
|
||||
describe('TrueNAS-style nested custom elements', () => {
|
||||
const htmlFile = 'nested-custom-elements.html';
|
||||
|
||||
it('should not detect both parent custom element and child input as separate password fields', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
|
||||
// Click on the actual password input element
|
||||
const passwordInput = document.getElementById('password-field');
|
||||
const formDetector = new FormDetector(document, passwordInput as HTMLElement);
|
||||
|
||||
// Get the detected form
|
||||
const form = formDetector.getForm();
|
||||
expect(form).toBeTruthy();
|
||||
|
||||
// Should detect only ONE password field
|
||||
expect(form?.passwordField).toBeTruthy();
|
||||
expect(form?.passwordConfirmField).toBeFalsy();
|
||||
|
||||
// The detected password field should be the actual input element
|
||||
expect(form?.passwordField?.tagName.toLowerCase()).toBe('input');
|
||||
expect(form?.passwordField?.type).toBe('password');
|
||||
expect(form?.passwordField?.id).toBe('password-field');
|
||||
});
|
||||
|
||||
it('should detect username field correctly without duplication', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
|
||||
const usernameInput = document.getElementById('username-field');
|
||||
const formDetector = new FormDetector(document, usernameInput as HTMLElement);
|
||||
|
||||
const form = formDetector.getForm();
|
||||
expect(form).toBeTruthy();
|
||||
|
||||
// Should detect the username field
|
||||
expect(form?.usernameField).toBeTruthy();
|
||||
expect(form?.usernameField?.tagName.toLowerCase()).toBe('input');
|
||||
expect(form?.usernameField?.id).toBe('username-field');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Nested custom elements with actual password confirm field', () => {
|
||||
const htmlFile = 'nested-custom-elements-confirm.html';
|
||||
|
||||
it('should correctly identify actual password confirm fields vs parent-child duplicates', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
|
||||
const passwordElement = document.getElementById('password-field');
|
||||
const formDetector = new FormDetector(document, passwordElement as HTMLElement);
|
||||
|
||||
const form = formDetector.getForm();
|
||||
expect(form).toBeTruthy();
|
||||
|
||||
// Should correctly detect both password and confirm as separate fields
|
||||
expect(form?.passwordField).toBeTruthy();
|
||||
expect(form?.passwordConfirmField).toBeTruthy();
|
||||
|
||||
// Both should be actual input elements
|
||||
expect(form?.passwordField?.tagName.toLowerCase()).toBe('input');
|
||||
expect(form?.passwordConfirmField?.tagName.toLowerCase()).toBe('input');
|
||||
|
||||
// They should be different elements
|
||||
expect(form?.passwordField?.id).toBe('password-field');
|
||||
expect(form?.passwordConfirmField?.id).toBe('password-confirm-field');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { FormDetector } from '../FormDetector';
|
||||
|
||||
describe('FormDetector Shadow DOM tests', () => {
|
||||
it('should detect faceplate-text-input with actual shadow DOM as autofill triggerable field', () => {
|
||||
const html = `
|
||||
<form>
|
||||
<fieldset>
|
||||
<faceplate-text-input id="login-username" name="username" autocomplete="username" required="">
|
||||
<span slot="label">Email or username</span>
|
||||
</faceplate-text-input>
|
||||
</fieldset>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const dom = new JSDOM(html, {
|
||||
url: 'http://localhost',
|
||||
runScripts: 'dangerously',
|
||||
resources: 'usable'
|
||||
});
|
||||
const document = dom.window.document;
|
||||
|
||||
// Get the custom element
|
||||
const faceplateElement = document.getElementById('login-username');
|
||||
expect(faceplateElement).toBeTruthy();
|
||||
expect(faceplateElement?.tagName.toLowerCase()).toBe('faceplate-text-input');
|
||||
|
||||
// Create shadow DOM like Reddit's actual implementation
|
||||
const shadowRoot = faceplateElement!.attachShadow({ mode: 'open' });
|
||||
|
||||
// Create the internal input element that would exist in the shadow DOM
|
||||
const shadowHTML = `
|
||||
<div class="faceplate-input-wrapper">
|
||||
<input type="text" name="username" autocomplete="username" class="faceplate-input" />
|
||||
<slot name="label"></slot>
|
||||
</div>
|
||||
`;
|
||||
shadowRoot.innerHTML = shadowHTML;
|
||||
|
||||
// Verify shadow DOM was created correctly
|
||||
const shadowInput = shadowRoot.querySelector('input');
|
||||
expect(shadowInput).toBeTruthy();
|
||||
expect(shadowInput?.type).toBe('text');
|
||||
|
||||
// Create a FormDetector with the custom element
|
||||
const detector = new FormDetector(document, faceplateElement as HTMLElement);
|
||||
|
||||
// Should detect it as an autofill triggerable field
|
||||
expect(detector.isAutofillTriggerableField()).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect faceplate-text-input with type attribute fallback as autofill triggerable field', () => {
|
||||
const html = `
|
||||
<form>
|
||||
<fieldset>
|
||||
<faceplate-text-input id="login-username" type="text" name="username" autocomplete="username" required="">
|
||||
<span slot="label">Email or username</span>
|
||||
</faceplate-text-input>
|
||||
</fieldset>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const dom = new JSDOM(html, {
|
||||
url: 'http://localhost',
|
||||
runScripts: 'dangerously',
|
||||
resources: 'usable'
|
||||
});
|
||||
const document = dom.window.document;
|
||||
|
||||
// Get the custom element
|
||||
const faceplateElement = document.getElementById('login-username');
|
||||
expect(faceplateElement).toBeTruthy();
|
||||
expect(faceplateElement?.tagName.toLowerCase()).toBe('faceplate-text-input');
|
||||
|
||||
// Create a FormDetector with the custom element (without shadow DOM for this test)
|
||||
const detector = new FormDetector(document, faceplateElement as HTMLElement);
|
||||
|
||||
// Should detect it as an autofill triggerable field using type attribute
|
||||
expect(detector.isAutofillTriggerableField()).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect faceplate-text-input with password shadow DOM as triggerable field', () => {
|
||||
const html = `
|
||||
<form>
|
||||
<fieldset>
|
||||
<faceplate-text-input id="login-password" name="password" autocomplete="current-password">
|
||||
<span slot="label">Password</span>
|
||||
</faceplate-text-input>
|
||||
</fieldset>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const dom = new JSDOM(html, {
|
||||
url: 'http://localhost',
|
||||
runScripts: 'dangerously',
|
||||
resources: 'usable'
|
||||
});
|
||||
const document = dom.window.document;
|
||||
|
||||
const faceplateElement = document.getElementById('login-password');
|
||||
|
||||
// Create shadow DOM with password input (like Reddit's actual implementation)
|
||||
const shadowRoot = faceplateElement!.attachShadow({ mode: 'open' });
|
||||
const shadowHTML = `
|
||||
<div class="faceplate-input-wrapper">
|
||||
<input type="password" name="password" autocomplete="current-password" class="faceplate-input" />
|
||||
<slot name="label"></slot>
|
||||
</div>
|
||||
`;
|
||||
shadowRoot.innerHTML = shadowHTML;
|
||||
|
||||
// Verify shadow DOM password input
|
||||
const shadowInput = shadowRoot.querySelector('input');
|
||||
expect(shadowInput?.type).toBe('password');
|
||||
|
||||
const detector = new FormDetector(document, faceplateElement as HTMLElement);
|
||||
|
||||
expect(detector.isAutofillTriggerableField()).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect custom element with email type as triggerable field', () => {
|
||||
const html = `
|
||||
<form>
|
||||
<custom-input id="email-field" type="email" name="email">
|
||||
<span slot="label">Email Address</span>
|
||||
</custom-input>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const dom = new JSDOM(html, {
|
||||
url: 'http://localhost',
|
||||
runScripts: 'dangerously',
|
||||
resources: 'usable'
|
||||
});
|
||||
const document = dom.window.document;
|
||||
|
||||
const customElement = document.getElementById('email-field');
|
||||
const detector = new FormDetector(document, customElement as HTMLElement);
|
||||
|
||||
expect(detector.isAutofillTriggerableField()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not detect non-input custom elements as triggerable fields', () => {
|
||||
const html = `
|
||||
<form>
|
||||
<custom-button id="submit-btn" type="button">
|
||||
<span>Submit</span>
|
||||
</custom-button>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const dom = new JSDOM(html, {
|
||||
url: 'http://localhost',
|
||||
runScripts: 'dangerously',
|
||||
resources: 'usable'
|
||||
});
|
||||
const document = dom.window.document;
|
||||
|
||||
const customButton = document.getElementById('submit-btn');
|
||||
const detector = new FormDetector(document, customButton as HTMLElement);
|
||||
|
||||
expect(detector.isAutofillTriggerableField()).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle real Reddit-like login form with shadow DOM', () => {
|
||||
const html = `
|
||||
<form>
|
||||
<fieldset class="relative mt-0 mb-0 ml-0 mr-0 p-0 border-0 flex flex-col flex-grow bg-transparent">
|
||||
<faceplate-text-input id="login-username" name="username" autocomplete="username" required=""
|
||||
helper-text-placeholder=" " aria-disabled="false" helper-text-aria-live="polite"
|
||||
appearance="secondary" faceplate-validity="invalid">
|
||||
<span slot="label">Email or username</span>
|
||||
</faceplate-text-input>
|
||||
</fieldset>
|
||||
<fieldset class="relative mt-0 mb-0 ml-0 mr-0 p-0 border-0 flex flex-col flex-grow bg-transparent">
|
||||
<faceplate-text-input id="login-password" name="password" autocomplete="current-password" required=""
|
||||
helper-text-placeholder=" " aria-disabled="false" helper-text-aria-live="polite"
|
||||
appearance="secondary">
|
||||
<span slot="label">Password</span>
|
||||
</faceplate-text-input>
|
||||
</fieldset>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const dom = new JSDOM(html, {
|
||||
url: 'http://localhost',
|
||||
runScripts: 'dangerously',
|
||||
resources: 'usable'
|
||||
});
|
||||
const document = dom.window.document;
|
||||
|
||||
// Set up shadow DOM for username field (no type attribute, just like Reddit)
|
||||
const usernameElement = document.getElementById('login-username');
|
||||
const usernameShadowRoot = usernameElement!.attachShadow({ mode: 'open' });
|
||||
usernameShadowRoot.innerHTML = `
|
||||
<div class="faceplate-input-wrapper">
|
||||
<input type="text" name="username" autocomplete="username" class="faceplate-input" />
|
||||
<slot name="label"></slot>
|
||||
<div class="faceplate-input-decorations"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Set up shadow DOM for password field (no type attribute, just like Reddit)
|
||||
const passwordElement = document.getElementById('login-password');
|
||||
const passwordShadowRoot = passwordElement!.attachShadow({ mode: 'open' });
|
||||
passwordShadowRoot.innerHTML = `
|
||||
<div class="faceplate-input-wrapper">
|
||||
<input type="password" name="password" autocomplete="current-password" class="faceplate-input" />
|
||||
<slot name="label"></slot>
|
||||
<div class="faceplate-input-decorations"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Test username field detection
|
||||
const usernameDetector = new FormDetector(document, usernameElement as HTMLElement);
|
||||
expect(usernameDetector.containsLoginForm()).toBe(true);
|
||||
expect(usernameDetector.isAutofillTriggerableField()).toBe(true);
|
||||
|
||||
// Test password field detection
|
||||
const passwordDetector = new FormDetector(document, passwordElement as HTMLElement);
|
||||
expect(passwordDetector.containsLoginForm()).toBe(true);
|
||||
expect(passwordDetector.isAutofillTriggerableField()).toBe(true);
|
||||
|
||||
// Test form extraction
|
||||
const form = usernameDetector.getForm();
|
||||
expect(form).toBeTruthy();
|
||||
|
||||
/**
|
||||
* The form should be able to detect both username and password fields
|
||||
* Note: Since the custom elements are now recognized, they should be included in the form detection
|
||||
*/
|
||||
expect(form?.form).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle custom elements in form detection with type attributes', () => {
|
||||
const html = `
|
||||
<form>
|
||||
<fieldset>
|
||||
<faceplate-text-input id="username-field" type="text" name="username">
|
||||
<span slot="label">Username</span>
|
||||
</faceplate-text-input>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<faceplate-text-input id="password-field" type="password" name="password">
|
||||
<span slot="label">Password</span>
|
||||
</faceplate-text-input>
|
||||
</fieldset>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const dom = new JSDOM(html, {
|
||||
url: 'http://localhost',
|
||||
runScripts: 'dangerously',
|
||||
resources: 'usable'
|
||||
});
|
||||
const document = dom.window.document;
|
||||
|
||||
const usernameElement = document.getElementById('username-field');
|
||||
const detector = new FormDetector(document, usernameElement as HTMLElement);
|
||||
|
||||
// Should contain login form
|
||||
expect(detector.containsLoginForm()).toBe(true);
|
||||
|
||||
// Should detect the field as triggerable
|
||||
expect(detector.isAutofillTriggerableField()).toBe(true);
|
||||
|
||||
// Should be able to get form
|
||||
const form = detector.getForm();
|
||||
expect(form).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { FormDetector } from '../FormDetector';
|
||||
|
||||
import { FormField, testField } from './TestUtils';
|
||||
|
||||
describe('FormDetector Slot-based Form tests', () => {
|
||||
it('contains tests for slot-based form field detection', () => {
|
||||
/**
|
||||
* This test suite verifies that FormDetector can properly detect
|
||||
* form fields that use slot-based labels (Web Components pattern).
|
||||
* This is common in modern web applications using custom elements.
|
||||
*/
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
describe('Slot-based login form detection', () => {
|
||||
const htmlFile = 'slot-based-form.html';
|
||||
|
||||
testField(FormField.Username, 'login-username', htmlFile);
|
||||
testField(FormField.Password, 'login-password', htmlFile);
|
||||
testField(FormField.Email, 'email-field', htmlFile);
|
||||
testField(FormField.FirstName, 'firstname-field', htmlFile);
|
||||
testField(FormField.LastName, 'lastname-field', htmlFile);
|
||||
});
|
||||
|
||||
describe('Direct slot label detection', () => {
|
||||
it('should detect input field with slot label for username', () => {
|
||||
const html = `
|
||||
<form>
|
||||
<div>
|
||||
<input id="test-username" name="user" type="text" />
|
||||
<span slot="label">username</span>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const dom = new JSDOM(html, {
|
||||
url: 'http://localhost',
|
||||
runScripts: 'dangerously',
|
||||
resources: 'usable'
|
||||
});
|
||||
const document = dom.window.document;
|
||||
const inputElement = document.getElementById('test-username');
|
||||
|
||||
const detector = new FormDetector(document, inputElement as HTMLElement);
|
||||
const form = detector.getForm();
|
||||
|
||||
// Since the slot contains "username", it should be detected as a username field
|
||||
expect(form?.usernameField).toBe(inputElement);
|
||||
});
|
||||
|
||||
it('should detect email field with slot helper-text', () => {
|
||||
const html = `
|
||||
<form>
|
||||
<web-component id="test-email" type="email">
|
||||
<span slot="helper-text">Enter your email address</span>
|
||||
</web-component>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const dom = new JSDOM(html, {
|
||||
url: 'http://localhost',
|
||||
runScripts: 'dangerously',
|
||||
resources: 'usable'
|
||||
});
|
||||
const document = dom.window.document;
|
||||
const inputElement = document.getElementById('test-email');
|
||||
|
||||
const detector = new FormDetector(document, inputElement as HTMLElement);
|
||||
const form = detector.getForm();
|
||||
|
||||
expect(form?.emailField).toBe(inputElement);
|
||||
});
|
||||
|
||||
it('should detect password field with nested slot label', () => {
|
||||
const html = `
|
||||
<form>
|
||||
<fieldset>
|
||||
<custom-password-input id="test-password" type="password">
|
||||
<div slot="label">
|
||||
<span>Your Password</span>
|
||||
</div>
|
||||
</custom-password-input>
|
||||
</fieldset>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const dom = new JSDOM(html, {
|
||||
url: 'http://localhost',
|
||||
runScripts: 'dangerously',
|
||||
resources: 'usable'
|
||||
});
|
||||
const document = dom.window.document;
|
||||
const inputElement = document.getElementById('test-password');
|
||||
|
||||
const detector = new FormDetector(document, inputElement as HTMLElement);
|
||||
const form = detector.getForm();
|
||||
|
||||
expect(form?.passwordField).toBe(inputElement);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Slot-based form with regular inputs', () => {
|
||||
it('should correctly identify input fields with slot labels', () => {
|
||||
const html = `
|
||||
<form>
|
||||
<div>
|
||||
<input id="email-input" type="email" name="email">
|
||||
<span slot="label">Email Address</span>
|
||||
</div>
|
||||
<div>
|
||||
<input id="pass-input" type="password" name="password">
|
||||
<span slot="label">Password</span>
|
||||
</div>
|
||||
<div>
|
||||
<input id="fname-input" type="text" name="fname">
|
||||
<span slot="label">First Name</span>
|
||||
</div>
|
||||
<div>
|
||||
<input id="lname-input" type="text" name="lname">
|
||||
<span slot="label">Last Name</span>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const dom = new JSDOM(html, {
|
||||
url: 'http://localhost',
|
||||
runScripts: 'dangerously',
|
||||
resources: 'usable'
|
||||
});
|
||||
const document = dom.window.document;
|
||||
|
||||
// Test email field detection
|
||||
const emailElement = document.getElementById('email-input');
|
||||
const emailDetector = new FormDetector(document, emailElement as HTMLElement);
|
||||
const emailForm = emailDetector.getForm();
|
||||
expect(emailForm?.emailField).toBe(emailElement);
|
||||
|
||||
// Test password field detection
|
||||
const passElement = document.getElementById('pass-input');
|
||||
const passDetector = new FormDetector(document, passElement as HTMLElement);
|
||||
const passForm = passDetector.getForm();
|
||||
expect(passForm?.passwordField).toBe(passElement);
|
||||
|
||||
// Test first name field detection
|
||||
const fnameElement = document.getElementById('fname-input');
|
||||
const fnameDetector = new FormDetector(document, fnameElement as HTMLElement);
|
||||
const fnameForm = fnameDetector.getForm();
|
||||
expect(fnameForm?.firstNameField).toBe(fnameElement);
|
||||
|
||||
// Test last name field detection
|
||||
const lnameElement = document.getElementById('lname-input');
|
||||
const lnameDetector = new FormDetector(document, lnameElement as HTMLElement);
|
||||
const lnameForm = lnameDetector.getForm();
|
||||
expect(lnameForm?.lastNameField).toBe(lnameElement);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -29,7 +29,7 @@ describe('FormFiller English', () => {
|
||||
});
|
||||
|
||||
describe('fillBirthdateFields with English month names', () => {
|
||||
it('should fill separate fields with English month names', () => {
|
||||
it('should fill separate fields with English month names', async () => {
|
||||
const { daySelect, monthSelect, yearSelect } = createDateSelects(document);
|
||||
|
||||
// Add month options with English month names
|
||||
@@ -52,7 +52,7 @@ describe('FormFiller English', () => {
|
||||
year: yearSelect as unknown as HTMLInputElement
|
||||
};
|
||||
|
||||
formFiller.fillFields(mockCredential);
|
||||
await formFiller.fillFields(mockCredential);
|
||||
|
||||
expect(daySelect.value).toBe('03');
|
||||
expect(monthSelect.value).toBe('February');
|
||||
|
||||
@@ -29,17 +29,17 @@ describe('FormFiller', () => {
|
||||
});
|
||||
|
||||
describe('fillBasicFields', () => {
|
||||
it('should fill username', () => {
|
||||
formFiller.fillFields(mockCredential);
|
||||
it('should fill username', async () => {
|
||||
await formFiller.fillFields(mockCredential);
|
||||
|
||||
expect(formFields.usernameField?.value).toBe('testuser');
|
||||
expect(wasTriggerCalledFor(mockTriggerInputEvents, formFields.usernameField)).toBe(true);
|
||||
});
|
||||
|
||||
it('should fill email and confirmation fields', () => {
|
||||
it('should fill email and confirmation fields', async () => {
|
||||
formFields.emailConfirmField = document.createElement('input');
|
||||
|
||||
formFiller.fillFields(mockCredential);
|
||||
await formFiller.fillFields(mockCredential);
|
||||
|
||||
expect(formFields.emailField?.value).toBe('test@example.com');
|
||||
expect(formFields.emailConfirmField?.value).toBe('test@example.com');
|
||||
@@ -47,12 +47,12 @@ describe('FormFiller', () => {
|
||||
expect(wasTriggerCalledFor(mockTriggerInputEvents, formFields.emailConfirmField)).toBe(true);
|
||||
});
|
||||
|
||||
it('should use username as email when no email is provided and no username field exists', () => {
|
||||
it('should use username as email when no email is provided and no username field exists', async () => {
|
||||
// Create a credential with an empty email string
|
||||
const credentialWithoutEmail = { ...mockCredential, Alias: { ...mockCredential.Alias, Email: '' } };
|
||||
formFields.usernameField = null;
|
||||
|
||||
formFiller.fillFields(credentialWithoutEmail);
|
||||
await formFiller.fillFields(credentialWithoutEmail);
|
||||
|
||||
expect(formFields.emailField?.value).toBe('testuser');
|
||||
expect(wasTriggerCalledFor(mockTriggerInputEvents, formFields.emailField)).toBe(true);
|
||||
@@ -61,7 +61,7 @@ describe('FormFiller', () => {
|
||||
it('should fill password and confirmation fields', async () => {
|
||||
formFields.passwordConfirmField = document.createElement('input');
|
||||
|
||||
formFiller.fillFields(mockCredential);
|
||||
await formFiller.fillFields(mockCredential);
|
||||
|
||||
// Delay for 150ms to ensure the password field is filled as it uses a small delay between each character.
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
@@ -72,8 +72,8 @@ describe('FormFiller', () => {
|
||||
expect(wasTriggerCalledFor(mockTriggerInputEvents, formFields.passwordConfirmField)).toBe(true);
|
||||
});
|
||||
|
||||
it('should fill name fields correctly', () => {
|
||||
formFiller.fillFields(mockCredential);
|
||||
it('should fill name fields correctly', async () => {
|
||||
await formFiller.fillFields(mockCredential);
|
||||
|
||||
expect(formFields.fullNameField?.value).toBe('John Doe');
|
||||
expect(formFields.firstNameField?.value).toBe('John');
|
||||
@@ -85,38 +85,38 @@ describe('FormFiller', () => {
|
||||
});
|
||||
|
||||
describe('fillBirthdateFields', () => {
|
||||
it('should fill single birthdate field with correct format', () => {
|
||||
formFiller.fillFields(mockCredential);
|
||||
it('should fill single birthdate field with correct format', async () => {
|
||||
await formFiller.fillFields(mockCredential);
|
||||
|
||||
expect(formFields.birthdateField.single?.value).toBe('1991-02-03');
|
||||
expect(wasTriggerCalledFor(mockTriggerInputEvents, formFields.birthdateField.single)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle different date formats (mm/dd/yyyy)', () => {
|
||||
it('should handle different date formats (mm/dd/yyyy)', async () => {
|
||||
formFields.birthdateField.format = 'mm/dd/yyyy';
|
||||
formFiller.fillFields(mockCredential);
|
||||
await formFiller.fillFields(mockCredential);
|
||||
expect(formFields.birthdateField.single?.value).toBe('02/03/1991');
|
||||
});
|
||||
|
||||
it('should handle different date formats (dd/mm/yyyy)', () => {
|
||||
it('should handle different date formats (dd/mm/yyyy)', async () => {
|
||||
formFields.birthdateField.format = 'dd/mm/yyyy';
|
||||
formFiller.fillFields(mockCredential);
|
||||
await formFiller.fillFields(mockCredential);
|
||||
expect(formFields.birthdateField.single?.value).toBe('03/02/1991');
|
||||
});
|
||||
|
||||
it('should handle different date formats (dd-mm-yyyy)', () => {
|
||||
it('should handle different date formats (dd-mm-yyyy)', async () => {
|
||||
formFields.birthdateField.format = 'dd-mm-yyyy';
|
||||
formFiller.fillFields(mockCredential);
|
||||
await formFiller.fillFields(mockCredential);
|
||||
expect(formFields.birthdateField.single?.value).toBe('03-02-1991');
|
||||
});
|
||||
|
||||
it('should handle different date formats (mm-dd-yyyy)', () => {
|
||||
it('should handle different date formats (mm-dd-yyyy)', async () => {
|
||||
formFields.birthdateField.format = 'mm-dd-yyyy';
|
||||
formFiller.fillFields(mockCredential);
|
||||
await formFiller.fillFields(mockCredential);
|
||||
expect(formFields.birthdateField.single?.value).toBe('02-03-1991');
|
||||
});
|
||||
|
||||
it('should fill separate day/month/year select fields', () => {
|
||||
it('should fill separate day/month/year select fields', async () => {
|
||||
const { daySelect, monthSelect, yearSelect } = createDateSelects(document);
|
||||
|
||||
// Add month options (1-12)
|
||||
@@ -136,7 +136,7 @@ describe('FormFiller', () => {
|
||||
year: yearSelect as unknown as HTMLInputElement
|
||||
};
|
||||
|
||||
formFiller.fillFields(mockCredential);
|
||||
await formFiller.fillFields(mockCredential);
|
||||
|
||||
expect(daySelect.value).toBe('03');
|
||||
expect(monthSelect.value).toBe('02');
|
||||
@@ -148,7 +148,7 @@ describe('FormFiller', () => {
|
||||
});
|
||||
|
||||
describe('fillGenderFields', () => {
|
||||
it('should fill gender select field', () => {
|
||||
it('should fill gender select field', async () => {
|
||||
const selectElement = document.createElement('select');
|
||||
|
||||
// Add options using createElement
|
||||
@@ -167,13 +167,13 @@ describe('FormFiller', () => {
|
||||
field: selectElement
|
||||
};
|
||||
|
||||
formFiller.fillFields(mockCredential);
|
||||
await formFiller.fillFields(mockCredential);
|
||||
|
||||
expect(selectElement.value).toBe('m');
|
||||
expect(wasTriggerCalledFor(mockTriggerInputEvents, selectElement)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle radio button gender fields', () => {
|
||||
it('should handle radio button gender fields', async () => {
|
||||
const maleRadio = document.createElement('input');
|
||||
maleRadio.type = 'radio';
|
||||
const femaleRadio = document.createElement('input');
|
||||
@@ -189,7 +189,7 @@ describe('FormFiller', () => {
|
||||
}
|
||||
};
|
||||
|
||||
formFiller.fillFields(mockCredential);
|
||||
await formFiller.fillFields(mockCredential);
|
||||
|
||||
expect(maleRadio.checked).toBe(true);
|
||||
expect(femaleRadio.checked).toBe(false);
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('FormFiller Dutch', () => {
|
||||
});
|
||||
|
||||
describe('fillBirthdateFields with Dutch month names', () => {
|
||||
it('should fill separate fields with Dutch month names', () => {
|
||||
it('should fill separate fields with Dutch month names', async () => {
|
||||
const { daySelect, monthSelect, yearSelect } = createDateSelects(document);
|
||||
|
||||
// Add month options with Dutch month names
|
||||
@@ -52,7 +52,7 @@ describe('FormFiller Dutch', () => {
|
||||
year: yearSelect as unknown as HTMLInputElement
|
||||
};
|
||||
|
||||
formFiller.fillFields(mockCredential);
|
||||
await formFiller.fillFields(mockCredential);
|
||||
|
||||
expect(daySelect.value).toBe('03');
|
||||
expect(monthSelect.value).toBe('Februari');
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Registration Form - Nested Custom Elements with Confirm</title>
|
||||
</head>
|
||||
<body>
|
||||
<form id="registration-form">
|
||||
<div class="field-group">
|
||||
<ix-input formcontrolname="username" type="text" ix-label="Username" name="username">
|
||||
<input id="username-field" type="text" aria-label="Username" name="username" class="mat-input-element">
|
||||
</ix-input>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<ix-input formcontrolname="password" type="password" ix-label="Password" name="password">
|
||||
<input id="password-field" type="password" aria-label="Password" name="password" class="mat-input-element">
|
||||
</ix-input>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<ix-input formcontrolname="passwordConfirm" type="password" ix-label="Confirm Password" name="passwordConfirm">
|
||||
<input id="password-confirm-field" type="password" aria-label="Confirm Password" name="passwordConfirm" class="mat-input-element">
|
||||
</ix-input>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>TrueNAS Login - Nested Custom Elements</title>
|
||||
</head>
|
||||
<body>
|
||||
<form id="login-form">
|
||||
<div class="field-group">
|
||||
<ix-input id="username-wrapper" formcontrolname="username" type="text" ix-label="Username" name="username">
|
||||
<ix-label><label><span>Username</span></label></ix-label>
|
||||
<div class="input-container">
|
||||
<input id="username-field" type="text" aria-label="Username" name="username" class="mat-input-element">
|
||||
</div>
|
||||
</ix-input>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<ix-input id="password-wrapper" formcontrolname="password" type="password" ix-label="Password" name="password">
|
||||
<ix-label><label><span>Password</span></label></ix-label>
|
||||
<div class="input-container">
|
||||
<input id="password-field" type="password" aria-label="Password" name="password" class="mat-input-element">
|
||||
</div>
|
||||
</ix-input>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user