mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
feat(i18n): initialize internationalization and update Vite config for locale handling
- Added `initI18n` function call in `main.tsx` to bootstrap internationalization before rendering the app. - Updated Vite configuration to handle specific locale files for English and Chinese.
This commit is contained in:
@@ -808,12 +808,18 @@ async function apiKey(request: Request, env: Env, userId: string, rotate: boolea
|
||||
// Generate a random alphanumeric string of the given length using crypto.getRandomValues.
|
||||
function randomStringAlphanum(length: number): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const array = new Uint8Array(length);
|
||||
crypto.getRandomValues(array);
|
||||
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars[array[i] % chars.length];
|
||||
const maxUnbiased = Math.floor(256 / chars.length) * chars.length;
|
||||
const bytes = new Uint8Array(Math.max(16, length));
|
||||
|
||||
while (result.length < length) {
|
||||
crypto.getRandomValues(bytes);
|
||||
for (const value of bytes) {
|
||||
if (value >= maxUnbiased) continue;
|
||||
result += chars[value % chars.length];
|
||||
if (result.length >= length) break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+68
-2
@@ -6,6 +6,17 @@ import { StorageService } from './storage';
|
||||
// The client already does heavy PBKDF2 (600k iterations).
|
||||
// This second layer only needs to be non-trivial, not expensive.
|
||||
const SERVER_HASH_ITERATIONS = 100_000;
|
||||
const AUTH_CONTEXT_CACHE_TTL_MS = 15 * 1000;
|
||||
|
||||
interface CachedUserEntry {
|
||||
user: User | null;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
interface CachedDeviceEntry {
|
||||
device: Awaited<ReturnType<StorageService['getDevice']>>;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
export interface VerifiedAccessContext {
|
||||
payload: JWTPayload;
|
||||
@@ -14,11 +25,65 @@ export interface VerifiedAccessContext {
|
||||
|
||||
export class AuthService {
|
||||
private storage: StorageService;
|
||||
private static userCache = new Map<string, CachedUserEntry>();
|
||||
private static deviceCache = new Map<string, CachedDeviceEntry>();
|
||||
|
||||
constructor(private env: Env) {
|
||||
this.storage = new StorageService(env.DB);
|
||||
}
|
||||
|
||||
private readCachedUser(userId: string): User | null | undefined {
|
||||
const cached = AuthService.userCache.get(userId);
|
||||
if (!cached) return undefined;
|
||||
if (cached.expiresAt <= Date.now()) {
|
||||
AuthService.userCache.delete(userId);
|
||||
return undefined;
|
||||
}
|
||||
return cached.user;
|
||||
}
|
||||
|
||||
private writeCachedUser(userId: string, user: User | null): void {
|
||||
AuthService.userCache.set(userId, {
|
||||
user,
|
||||
expiresAt: Date.now() + AUTH_CONTEXT_CACHE_TTL_MS,
|
||||
});
|
||||
}
|
||||
|
||||
private async getCachedUser(userId: string): Promise<User | null> {
|
||||
const cached = this.readCachedUser(userId);
|
||||
if (cached !== undefined) return cached;
|
||||
const user = await this.storage.getUserById(userId);
|
||||
this.writeCachedUser(userId, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
private readCachedDevice(userId: string, deviceId: string) {
|
||||
const cacheKey = `${userId}:${deviceId}`;
|
||||
const cached = AuthService.deviceCache.get(cacheKey);
|
||||
if (!cached) return undefined;
|
||||
if (cached.expiresAt <= Date.now()) {
|
||||
AuthService.deviceCache.delete(cacheKey);
|
||||
return undefined;
|
||||
}
|
||||
return cached.device;
|
||||
}
|
||||
|
||||
private writeCachedDevice(userId: string, deviceId: string, device: Awaited<ReturnType<StorageService['getDevice']>>): void {
|
||||
const cacheKey = `${userId}:${deviceId}`;
|
||||
AuthService.deviceCache.set(cacheKey, {
|
||||
device,
|
||||
expiresAt: Date.now() + AUTH_CONTEXT_CACHE_TTL_MS,
|
||||
});
|
||||
}
|
||||
|
||||
private async getCachedDevice(userId: string, deviceId: string) {
|
||||
const cached = this.readCachedDevice(userId, deviceId);
|
||||
if (cached !== undefined) return cached;
|
||||
const device = await this.storage.getDevice(userId, deviceId);
|
||||
this.writeCachedDevice(userId, deviceId, device);
|
||||
return device;
|
||||
}
|
||||
|
||||
// Second-layer hash: PBKDF2-SHA256(clientHash, email-salt, iterations).
|
||||
// Ensures database contents alone cannot be used to authenticate (pass-the-hash defense).
|
||||
// Result is prefixed with "$s$" to distinguish from legacy raw client hashes.
|
||||
@@ -97,15 +162,16 @@ export class AuthService {
|
||||
const payload = await verifyJWT(parts[1], this.env.JWT_SECRET);
|
||||
if (!payload) return null;
|
||||
|
||||
const user = await this.storage.getUserById(payload.sub);
|
||||
const user = await this.getCachedUser(payload.sub);
|
||||
if (!user) return null;
|
||||
if (user.status !== 'active') return null;
|
||||
|
||||
if (payload.sstamp !== user.securityStamp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (payload.did) {
|
||||
const device = await this.storage.getDevice(user.id, payload.did);
|
||||
const device = await this.getCachedDevice(user.id, payload.did);
|
||||
if (!device) return null;
|
||||
if (!payload.dstamp || payload.dstamp !== device.sessionStamp) return null;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,43 @@ interface CipherRow {
|
||||
deleted_at: string | null;
|
||||
}
|
||||
|
||||
const CIPHER_SCALAR_DATA_KEYS = new Set([
|
||||
'id',
|
||||
'userId',
|
||||
'user_id',
|
||||
'type',
|
||||
'folderId',
|
||||
'folder_id',
|
||||
'name',
|
||||
'notes',
|
||||
'favorite',
|
||||
'reprompt',
|
||||
'key',
|
||||
'createdAt',
|
||||
'created_at',
|
||||
'creationDate',
|
||||
'updatedAt',
|
||||
'updated_at',
|
||||
'revisionDate',
|
||||
'archivedAt',
|
||||
'archived_at',
|
||||
'archivedDate',
|
||||
'deletedAt',
|
||||
'deleted_at',
|
||||
'deletedDate',
|
||||
]);
|
||||
|
||||
function buildCipherData(cipher: Cipher, folderId: string | null): string {
|
||||
const payload: Record<string, unknown> = {
|
||||
...cipher,
|
||||
folderId,
|
||||
};
|
||||
for (const key of CIPHER_SCALAR_DATA_KEYS) {
|
||||
delete payload[key];
|
||||
}
|
||||
return JSON.stringify(payload);
|
||||
}
|
||||
|
||||
function parseCipherRow(row: CipherRow | null | undefined): Cipher | null {
|
||||
if (!row?.data) return null;
|
||||
try {
|
||||
@@ -68,10 +105,7 @@ export async function getCipher(db: D1Database, id: string): Promise<Cipher | nu
|
||||
|
||||
export async function saveCipher(db: D1Database, safeBind: SafeBind, cipher: Cipher): Promise<void> {
|
||||
const folderId = normalizeOptionalId(cipher.folderId);
|
||||
const data = JSON.stringify({
|
||||
...cipher,
|
||||
folderId,
|
||||
});
|
||||
const data = buildCipherData(cipher, folderId);
|
||||
const stmt = db.prepare(
|
||||
'INSERT INTO ciphers(id, user_id, type, folder_id, name, notes, favorite, data, reprompt, key, created_at, updated_at, archived_at, deleted_at) ' +
|
||||
'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ' +
|
||||
@@ -117,8 +151,7 @@ export async function bulkSoftDeleteCiphers(
|
||||
if (!uniqueIds.length) return null;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const patch = JSON.stringify({ deletedAt: now, updatedAt: now });
|
||||
const chunkSize = sqlChunkSize(4);
|
||||
const chunkSize = sqlChunkSize(3);
|
||||
|
||||
for (let i = 0; i < uniqueIds.length; i += chunkSize) {
|
||||
const chunk = uniqueIds.slice(i, i + chunkSize);
|
||||
@@ -126,10 +159,11 @@ export async function bulkSoftDeleteCiphers(
|
||||
await db
|
||||
.prepare(
|
||||
`UPDATE ciphers
|
||||
SET deleted_at = ?, updated_at = ?, data = json_patch(data, ?)
|
||||
SET deleted_at = ?, updated_at = ?,
|
||||
data = json_remove(data, '$.deletedAt', '$.deletedDate', '$.updatedAt', '$.revisionDate')
|
||||
WHERE user_id = ? AND id IN (${placeholders})`
|
||||
)
|
||||
.bind(now, now, patch, userId, ...chunk)
|
||||
.bind(now, now, userId, ...chunk)
|
||||
.run();
|
||||
}
|
||||
|
||||
@@ -148,8 +182,7 @@ export async function bulkRestoreCiphers(
|
||||
if (!uniqueIds.length) return null;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const patch = JSON.stringify({ deletedAt: null, updatedAt: now });
|
||||
const chunkSize = sqlChunkSize(3);
|
||||
const chunkSize = sqlChunkSize(2);
|
||||
|
||||
for (let i = 0; i < uniqueIds.length; i += chunkSize) {
|
||||
const chunk = uniqueIds.slice(i, i + chunkSize);
|
||||
@@ -157,10 +190,11 @@ export async function bulkRestoreCiphers(
|
||||
await db
|
||||
.prepare(
|
||||
`UPDATE ciphers
|
||||
SET deleted_at = NULL, updated_at = ?, data = json_patch(data, ?)
|
||||
SET deleted_at = NULL, updated_at = ?,
|
||||
data = json_remove(data, '$.deletedAt', '$.deletedDate', '$.updatedAt', '$.revisionDate')
|
||||
WHERE user_id = ? AND id IN (${placeholders})`
|
||||
)
|
||||
.bind(now, patch, userId, ...chunk)
|
||||
.bind(now, userId, ...chunk)
|
||||
.run();
|
||||
}
|
||||
|
||||
@@ -262,8 +296,7 @@ export async function bulkMoveCiphers(
|
||||
const now = new Date().toISOString();
|
||||
const normalizedFolderId = normalizeOptionalId(folderId);
|
||||
const uniqueIds = sanitizeIds(ids);
|
||||
const patch = JSON.stringify({ folderId: normalizedFolderId, updatedAt: now });
|
||||
const chunkSize = sqlChunkSize(4);
|
||||
const chunkSize = sqlChunkSize(3);
|
||||
|
||||
for (let i = 0; i < uniqueIds.length; i += chunkSize) {
|
||||
const chunk = uniqueIds.slice(i, i + chunkSize);
|
||||
@@ -271,10 +304,11 @@ export async function bulkMoveCiphers(
|
||||
await db
|
||||
.prepare(
|
||||
`UPDATE ciphers
|
||||
SET folder_id = ?, updated_at = ?, data = json_patch(data, ?)
|
||||
SET folder_id = ?, updated_at = ?,
|
||||
data = json_remove(data, '$.folderId', '$.folder_id', '$.updatedAt', '$.revisionDate')
|
||||
WHERE user_id = ? AND id IN (${placeholders})`
|
||||
)
|
||||
.bind(normalizedFolderId, now, patch, userId, ...chunk)
|
||||
.bind(normalizedFolderId, now, userId, ...chunk)
|
||||
.run();
|
||||
}
|
||||
|
||||
@@ -293,8 +327,7 @@ export async function bulkArchiveCiphers(
|
||||
if (!uniqueIds.length) return null;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const patch = JSON.stringify({ archivedAt: now, archivedDate: now, updatedAt: now });
|
||||
const chunkSize = sqlChunkSize(4);
|
||||
const chunkSize = sqlChunkSize(3);
|
||||
|
||||
for (let i = 0; i < uniqueIds.length; i += chunkSize) {
|
||||
const chunk = uniqueIds.slice(i, i + chunkSize);
|
||||
@@ -302,10 +335,11 @@ export async function bulkArchiveCiphers(
|
||||
await db
|
||||
.prepare(
|
||||
`UPDATE ciphers
|
||||
SET archived_at = ?, updated_at = ?, data = json_patch(data, ?)
|
||||
SET archived_at = ?, updated_at = ?,
|
||||
data = json_remove(data, '$.archivedAt', '$.archivedDate', '$.updatedAt', '$.revisionDate')
|
||||
WHERE user_id = ? AND id IN (${placeholders}) AND deleted_at IS NULL`
|
||||
)
|
||||
.bind(now, now, patch, userId, ...chunk)
|
||||
.bind(now, now, userId, ...chunk)
|
||||
.run();
|
||||
}
|
||||
|
||||
@@ -324,8 +358,7 @@ export async function bulkUnarchiveCiphers(
|
||||
if (!uniqueIds.length) return null;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const patch = JSON.stringify({ archivedAt: null, archivedDate: null, updatedAt: now });
|
||||
const chunkSize = sqlChunkSize(3);
|
||||
const chunkSize = sqlChunkSize(2);
|
||||
|
||||
for (let i = 0; i < uniqueIds.length; i += chunkSize) {
|
||||
const chunk = uniqueIds.slice(i, i + chunkSize);
|
||||
@@ -333,10 +366,11 @@ export async function bulkUnarchiveCiphers(
|
||||
await db
|
||||
.prepare(
|
||||
`UPDATE ciphers
|
||||
SET archived_at = NULL, updated_at = ?, data = json_patch(data, ?)
|
||||
SET archived_at = NULL, updated_at = ?,
|
||||
data = json_remove(data, '$.archivedAt', '$.archivedDate', '$.updatedAt', '$.revisionDate')
|
||||
WHERE user_id = ? AND id IN (${placeholders})`
|
||||
)
|
||||
.bind(now, patch, userId, ...chunk)
|
||||
.bind(now, userId, ...chunk)
|
||||
.run();
|
||||
}
|
||||
|
||||
|
||||
@@ -39,14 +39,14 @@ export async function clearFolderFromCiphers(
|
||||
folderId: string
|
||||
): Promise<void> {
|
||||
const now = new Date().toISOString();
|
||||
const patch = JSON.stringify({ folderId: null, updatedAt: now });
|
||||
await db
|
||||
.prepare(
|
||||
`UPDATE ciphers
|
||||
SET folder_id = NULL, updated_at = ?, data = json_patch(data, ?)
|
||||
SET folder_id = NULL, updated_at = ?,
|
||||
data = json_remove(data, '$.folderId', '$.folder_id', '$.updatedAt', '$.revisionDate')
|
||||
WHERE user_id = ? AND folder_id = ?`
|
||||
)
|
||||
.bind(now, patch, userId, folderId)
|
||||
.bind(now, userId, folderId)
|
||||
.run();
|
||||
}
|
||||
|
||||
@@ -61,8 +61,7 @@ export async function bulkDeleteFolders(
|
||||
if (!uniqueIds.length) return null;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const patch = JSON.stringify({ folderId: null, updatedAt: now });
|
||||
const chunkSize = sqlChunkSize(3);
|
||||
const chunkSize = sqlChunkSize(2);
|
||||
|
||||
for (let i = 0; i < uniqueIds.length; i += chunkSize) {
|
||||
const chunk = uniqueIds.slice(i, i + chunkSize);
|
||||
@@ -70,10 +69,11 @@ export async function bulkDeleteFolders(
|
||||
await db
|
||||
.prepare(
|
||||
`UPDATE ciphers
|
||||
SET folder_id = NULL, updated_at = ?, data = json_patch(data, ?)
|
||||
SET folder_id = NULL, updated_at = ?,
|
||||
data = json_remove(data, '$.folderId', '$.folder_id', '$.updatedAt', '$.revisionDate')
|
||||
WHERE user_id = ? AND folder_id IN (${placeholders})`
|
||||
)
|
||||
.bind(now, patch, userId, ...chunk)
|
||||
.bind(now, userId, ...chunk)
|
||||
.run();
|
||||
|
||||
await db
|
||||
|
||||
+31
-84
@@ -1,6 +1,8 @@
|
||||
import { JWTPayload } from '../types';
|
||||
import { LIMITS } from '../config/limits';
|
||||
|
||||
const hmacKeyCache = new Map<string, Promise<CryptoKey>>();
|
||||
|
||||
// Base64 URL encode
|
||||
function base64UrlEncode(data: Uint8Array): string {
|
||||
const base64 = btoa(String.fromCharCode(...data));
|
||||
@@ -19,6 +21,23 @@ function base64UrlDecode(str: string): Uint8Array {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function getHmacKey(secret: string): Promise<CryptoKey> {
|
||||
const cacheKey = secret;
|
||||
let cached = hmacKeyCache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
cached = crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign', 'verify']
|
||||
);
|
||||
hmacKeyCache.set(cacheKey, cached);
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Create JWT
|
||||
export async function createJWT(payload: Omit<JWTPayload, 'iat' | 'exp' | 'iss' | 'premium' | 'email_verified' | 'amr'>, secret: string, expiresIn: number = LIMITS.auth.accessTokenTtlSeconds): Promise<string> {
|
||||
const header = { alg: 'HS256', typ: 'JWT' };
|
||||
@@ -40,13 +59,7 @@ export async function createJWT(payload: Omit<JWTPayload, 'iat' | 'exp' | 'iss'
|
||||
|
||||
const data = `${headerB64}.${payloadB64}`;
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
const key = await getHmacKey(secret);
|
||||
|
||||
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
|
||||
const signatureB64 = base64UrlEncode(new Uint8Array(signature));
|
||||
@@ -63,13 +76,7 @@ export async function verifyJWT(token: string, secret: string): Promise<JWTPaylo
|
||||
const [headerB64, payloadB64, signatureB64] = parts;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['verify']
|
||||
);
|
||||
const key = await getHmacKey(secret);
|
||||
|
||||
const data = `${headerB64}.${payloadB64}`;
|
||||
const signature = base64UrlDecode(signatureB64);
|
||||
@@ -133,13 +140,7 @@ export async function createFileDownloadToken(
|
||||
|
||||
const data = `${headerB64}.${payloadB64}`;
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
const key = await getHmacKey(secret);
|
||||
|
||||
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
|
||||
const signatureB64 = base64UrlEncode(new Uint8Array(signature));
|
||||
@@ -159,13 +160,7 @@ export async function verifyFileDownloadToken(
|
||||
const [headerB64, payloadB64, signatureB64] = parts;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['verify']
|
||||
);
|
||||
const key = await getHmacKey(secret);
|
||||
|
||||
const data = `${headerB64}.${payloadB64}`;
|
||||
const signature = base64UrlDecode(signatureB64);
|
||||
@@ -205,13 +200,7 @@ export async function createAttachmentUploadToken(
|
||||
const payloadB64 = base64UrlEncode(encoder.encode(JSON.stringify(payload)));
|
||||
const data = `${headerB64}.${payloadB64}`;
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
const key = await getHmacKey(secret);
|
||||
|
||||
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
|
||||
const signatureB64 = base64UrlEncode(new Uint8Array(signature));
|
||||
@@ -229,13 +218,7 @@ export async function verifyAttachmentUploadToken(
|
||||
const [headerB64, payloadB64, signatureB64] = parts;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['verify']
|
||||
);
|
||||
const key = await getHmacKey(secret);
|
||||
|
||||
const data = `${headerB64}.${payloadB64}`;
|
||||
const signature = base64UrlDecode(signatureB64);
|
||||
@@ -285,13 +268,7 @@ export async function createSendFileDownloadToken(
|
||||
const payloadB64 = base64UrlEncode(encoder.encode(JSON.stringify(payload)));
|
||||
const data = `${headerB64}.${payloadB64}`;
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
const key = await getHmacKey(secret);
|
||||
|
||||
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
|
||||
const signatureB64 = base64UrlEncode(new Uint8Array(signature));
|
||||
@@ -309,13 +286,7 @@ export async function verifySendFileDownloadToken(
|
||||
const [headerB64, payloadB64, signatureB64] = parts;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['verify']
|
||||
);
|
||||
const key = await getHmacKey(secret);
|
||||
|
||||
const data = `${headerB64}.${payloadB64}`;
|
||||
const signature = base64UrlDecode(signatureB64);
|
||||
@@ -361,13 +332,7 @@ export async function createSendFileUploadToken(
|
||||
const payloadB64 = base64UrlEncode(encoder.encode(JSON.stringify(payload)));
|
||||
const data = `${headerB64}.${payloadB64}`;
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
const key = await getHmacKey(secret);
|
||||
|
||||
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
|
||||
const signatureB64 = base64UrlEncode(new Uint8Array(signature));
|
||||
@@ -385,13 +350,7 @@ export async function verifySendFileUploadToken(
|
||||
const [headerB64, payloadB64, signatureB64] = parts;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['verify']
|
||||
);
|
||||
const key = await getHmacKey(secret);
|
||||
|
||||
const data = `${headerB64}.${payloadB64}`;
|
||||
const signature = base64UrlDecode(signatureB64);
|
||||
@@ -430,13 +389,7 @@ export async function createSendAccessToken(sendId: string, secret: string): Pro
|
||||
const payloadB64 = base64UrlEncode(encoder.encode(JSON.stringify(payload)));
|
||||
const data = `${headerB64}.${payloadB64}`;
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
const key = await getHmacKey(secret);
|
||||
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
|
||||
const signatureB64 = base64UrlEncode(new Uint8Array(signature));
|
||||
return `${data}.${signatureB64}`;
|
||||
@@ -450,13 +403,7 @@ export async function verifySendAccessToken(token: string, secret: string): Prom
|
||||
const [headerB64, payloadB64, signatureB64] = parts;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['verify']
|
||||
);
|
||||
const key = await getHmacKey(secret);
|
||||
|
||||
const data = `${headerB64}.${payloadB64}`;
|
||||
const signature = base64UrlDecode(signatureB64);
|
||||
|
||||
@@ -85,6 +85,144 @@ function extractImportIdMaps(cipherMap: ImportedCipherMapEntry[] | null) {
|
||||
return { byIndex, bySourceId };
|
||||
}
|
||||
|
||||
function createOptimisticCipherId(): string {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return `optimistic:${crypto.randomUUID()}`;
|
||||
}
|
||||
return `optimistic:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
function optimisticCipherFromDraft(draft: VaultDraft, current?: Cipher | null): Cipher {
|
||||
const now = new Date().toISOString();
|
||||
const type = Number(draft.type || current?.type || 1) || 1;
|
||||
const next: Cipher = {
|
||||
...(current || {}),
|
||||
id: current?.id || createOptimisticCipherId(),
|
||||
type,
|
||||
folderId: draft.folderId || null,
|
||||
favorite: !!draft.favorite,
|
||||
reprompt: draft.reprompt ? 1 : 0,
|
||||
name: draft.name || '',
|
||||
notes: draft.notes || '',
|
||||
decName: draft.name || '',
|
||||
decNotes: draft.notes || '',
|
||||
creationDate: current?.creationDate || now,
|
||||
revisionDate: now,
|
||||
deletedDate: current?.deletedDate || null,
|
||||
archivedDate: current?.archivedDate || null,
|
||||
};
|
||||
|
||||
if (type === 1) {
|
||||
next.login = {
|
||||
...(current?.login || {}),
|
||||
username: draft.loginUsername || '',
|
||||
password: draft.loginPassword || '',
|
||||
totp: draft.loginTotp || '',
|
||||
decUsername: draft.loginUsername || '',
|
||||
decPassword: draft.loginPassword || '',
|
||||
decTotp: draft.loginTotp || '',
|
||||
uris: draft.loginUris.map((uri) => ({
|
||||
...(uri.extra || {}),
|
||||
uri: uri.uri || '',
|
||||
decUri: uri.uri || '',
|
||||
match: uri.match ?? null,
|
||||
})),
|
||||
fido2Credentials: draft.loginFido2Credentials.map((credential) => ({ ...credential })),
|
||||
};
|
||||
} else {
|
||||
next.login = null;
|
||||
}
|
||||
|
||||
if (type === 3) {
|
||||
next.card = {
|
||||
...(current?.card || {}),
|
||||
cardholderName: draft.cardholderName || '',
|
||||
number: draft.cardNumber || '',
|
||||
brand: draft.cardBrand || '',
|
||||
expMonth: draft.cardExpMonth || '',
|
||||
expYear: draft.cardExpYear || '',
|
||||
code: draft.cardCode || '',
|
||||
decCardholderName: draft.cardholderName || '',
|
||||
decNumber: draft.cardNumber || '',
|
||||
decBrand: draft.cardBrand || '',
|
||||
decExpMonth: draft.cardExpMonth || '',
|
||||
decExpYear: draft.cardExpYear || '',
|
||||
decCode: draft.cardCode || '',
|
||||
};
|
||||
} else {
|
||||
next.card = null;
|
||||
}
|
||||
|
||||
if (type === 4) {
|
||||
next.identity = {
|
||||
...(current?.identity || {}),
|
||||
title: draft.identTitle || '',
|
||||
firstName: draft.identFirstName || '',
|
||||
middleName: draft.identMiddleName || '',
|
||||
lastName: draft.identLastName || '',
|
||||
username: draft.identUsername || '',
|
||||
company: draft.identCompany || '',
|
||||
ssn: draft.identSsn || '',
|
||||
passportNumber: draft.identPassportNumber || '',
|
||||
licenseNumber: draft.identLicenseNumber || '',
|
||||
email: draft.identEmail || '',
|
||||
phone: draft.identPhone || '',
|
||||
address1: draft.identAddress1 || '',
|
||||
address2: draft.identAddress2 || '',
|
||||
address3: draft.identAddress3 || '',
|
||||
city: draft.identCity || '',
|
||||
state: draft.identState || '',
|
||||
postalCode: draft.identPostalCode || '',
|
||||
country: draft.identCountry || '',
|
||||
decTitle: draft.identTitle || '',
|
||||
decFirstName: draft.identFirstName || '',
|
||||
decMiddleName: draft.identMiddleName || '',
|
||||
decLastName: draft.identLastName || '',
|
||||
decUsername: draft.identUsername || '',
|
||||
decCompany: draft.identCompany || '',
|
||||
decSsn: draft.identSsn || '',
|
||||
decPassportNumber: draft.identPassportNumber || '',
|
||||
decLicenseNumber: draft.identLicenseNumber || '',
|
||||
decEmail: draft.identEmail || '',
|
||||
decPhone: draft.identPhone || '',
|
||||
decAddress1: draft.identAddress1 || '',
|
||||
decAddress2: draft.identAddress2 || '',
|
||||
decAddress3: draft.identAddress3 || '',
|
||||
decCity: draft.identCity || '',
|
||||
decState: draft.identState || '',
|
||||
decPostalCode: draft.identPostalCode || '',
|
||||
decCountry: draft.identCountry || '',
|
||||
};
|
||||
} else {
|
||||
next.identity = null;
|
||||
}
|
||||
|
||||
if (type === 5) {
|
||||
next.sshKey = {
|
||||
...(current?.sshKey || {}),
|
||||
privateKey: draft.sshPrivateKey || '',
|
||||
publicKey: draft.sshPublicKey || '',
|
||||
keyFingerprint: draft.sshFingerprint || '',
|
||||
fingerprint: draft.sshFingerprint || '',
|
||||
decPrivateKey: draft.sshPrivateKey || '',
|
||||
decPublicKey: draft.sshPublicKey || '',
|
||||
decFingerprint: draft.sshFingerprint || '',
|
||||
};
|
||||
} else {
|
||||
next.sshKey = null;
|
||||
}
|
||||
|
||||
next.fields = draft.customFields.map((field) => ({
|
||||
type: field.type,
|
||||
name: field.label,
|
||||
value: field.value,
|
||||
decName: field.label,
|
||||
decValue: field.value,
|
||||
}));
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
export default function useVaultSendActions(options: UseVaultSendActionsOptions) {
|
||||
const {
|
||||
authedFetch,
|
||||
@@ -142,6 +280,20 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
||||
});
|
||||
}
|
||||
|
||||
async function decryptAndReplaceOptimistic(optimisticId: string, encrypted: Cipher) {
|
||||
if (!session?.symEncKey || !session?.symMacKey) {
|
||||
await refetchCiphers();
|
||||
return;
|
||||
}
|
||||
const encKey = base64ToBytes(session.symEncKey);
|
||||
const macKey = base64ToBytes(session.symMacKey);
|
||||
const decrypted = await decryptSingleCipher(encrypted, encKey, macKey);
|
||||
patchDecryptedCiphers((prev) => {
|
||||
const next = prev.filter((cipher) => cipher.id !== optimisticId && cipher.id !== decrypted.id);
|
||||
return [decrypted, ...next];
|
||||
});
|
||||
}
|
||||
|
||||
function removeCipherFromState(id: string) {
|
||||
patchDecryptedCiphers((prev) => prev.filter((c) => c.id !== id));
|
||||
}
|
||||
@@ -244,6 +396,8 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
||||
|
||||
async createVaultItem(draft: VaultDraft, attachments: File[] = []) {
|
||||
if (!session) return;
|
||||
const optimistic = optimisticCipherFromDraft(draft, null);
|
||||
patchDecryptedCiphers((prev) => [optimistic, ...prev.filter((cipher) => cipher.id !== optimistic.id)]);
|
||||
try {
|
||||
const created = await createCipher(authedFetch, session, draft);
|
||||
for (const file of attachments) {
|
||||
@@ -251,10 +405,11 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
||||
setAttachmentUploadPercent(0);
|
||||
await uploadCipherAttachment(authedFetch, session, created.id, file, undefined, setAttachmentUploadPercent);
|
||||
}
|
||||
await decryptAndPatch(created);
|
||||
await decryptAndReplaceOptimistic(optimistic.id, created);
|
||||
syncVaultCoreInBackground({ includeFolders: !!draft.folderId || attachments.length > 0 });
|
||||
onNotify('success', t('txt_item_created'));
|
||||
} catch (error) {
|
||||
patchDecryptedCiphers((prev) => prev.filter((cipher) => cipher.id !== optimistic.id));
|
||||
onNotify('error', error instanceof Error ? error.message : t('txt_create_item_failed'));
|
||||
throw error;
|
||||
} finally {
|
||||
@@ -267,6 +422,24 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
||||
if (!session) return;
|
||||
const addFiles = Array.isArray(options?.addFiles) ? options.addFiles : [];
|
||||
const removeAttachmentIds = Array.isArray(options?.removeAttachmentIds) ? options.removeAttachmentIds : [];
|
||||
const previousCipher: Cipher = {
|
||||
...cipher,
|
||||
login: cipher.login ? { ...cipher.login, uris: cipher.login.uris ? [...cipher.login.uris] : cipher.login.uris } : cipher.login,
|
||||
card: cipher.card ? { ...cipher.card } : cipher.card,
|
||||
identity: cipher.identity ? { ...cipher.identity } : cipher.identity,
|
||||
sshKey: cipher.sshKey ? { ...cipher.sshKey } : cipher.sshKey,
|
||||
fields: cipher.fields ? cipher.fields.map((field) => ({ ...field })) : cipher.fields,
|
||||
attachments: cipher.attachments ? cipher.attachments.map((attachment) => ({ ...attachment })) : cipher.attachments,
|
||||
passwordHistory: cipher.passwordHistory ? cipher.passwordHistory.map((entry) => ({ ...entry })) : cipher.passwordHistory,
|
||||
};
|
||||
const optimistic = optimisticCipherFromDraft(draft, cipher);
|
||||
if (removeAttachmentIds.length || addFiles.length) {
|
||||
const removedSet = new Set(removeAttachmentIds.map((id) => String(id || '').trim()).filter(Boolean));
|
||||
optimistic.attachments = (cipher.attachments || [])
|
||||
.filter((attachment) => !removedSet.has(String(attachment?.id || '').trim()))
|
||||
.map((attachment) => ({ ...attachment }));
|
||||
}
|
||||
patchCipherBatch([cipher.id], () => optimistic);
|
||||
try {
|
||||
const updated = await updateCipher(authedFetch, session, cipher, draft);
|
||||
for (const attachmentId of removeAttachmentIds) {
|
||||
@@ -288,6 +461,7 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
||||
});
|
||||
onNotify('success', t('txt_item_updated'));
|
||||
} catch (error) {
|
||||
patchCipherBatch([cipher.id], () => previousCipher);
|
||||
onNotify('error', error instanceof Error ? error.message : t('txt_update_item_failed'));
|
||||
throw error;
|
||||
} finally {
|
||||
@@ -315,36 +489,48 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
||||
},
|
||||
|
||||
async deleteVaultItem(cipher: Cipher) {
|
||||
const previousCipher = { ...cipher };
|
||||
const deletedDate = new Date().toISOString();
|
||||
patchCipherBatch([cipher.id], (current) => ({ ...current, deletedDate, archivedDate: null, revisionDate: deletedDate }));
|
||||
try {
|
||||
const deleted = await deleteCipher(authedFetch, cipher.id);
|
||||
await decryptAndPatch(deleted);
|
||||
syncVaultCoreInBackground({ includeFolders: true });
|
||||
onNotify('success', t('txt_item_deleted'));
|
||||
} catch (error) {
|
||||
patchCipherBatch([cipher.id], () => previousCipher);
|
||||
onNotify('error', error instanceof Error ? error.message : t('txt_delete_item_failed'));
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async archiveVaultItem(cipher: Cipher) {
|
||||
const previousCipher = { ...cipher };
|
||||
const archivedDate = new Date().toISOString();
|
||||
patchCipherBatch([cipher.id], (current) => ({ ...current, archivedDate, deletedDate: null, revisionDate: archivedDate }));
|
||||
try {
|
||||
const archived = await archiveCipher(authedFetch, cipher.id);
|
||||
await decryptAndPatch(archived);
|
||||
syncVaultCoreInBackground({ includeFolders: true });
|
||||
onNotify('success', t('txt_item_archived'));
|
||||
} catch (error) {
|
||||
patchCipherBatch([cipher.id], () => previousCipher);
|
||||
onNotify('error', error instanceof Error ? error.message : t('txt_archive_item_failed'));
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async unarchiveVaultItem(cipher: Cipher) {
|
||||
const previousCipher = { ...cipher };
|
||||
const revisionDate = new Date().toISOString();
|
||||
patchCipherBatch([cipher.id], (current) => ({ ...current, archivedDate: null, revisionDate }));
|
||||
try {
|
||||
const unarchived = await unarchiveCipher(authedFetch, cipher.id);
|
||||
await decryptAndPatch(unarchived);
|
||||
syncVaultCoreInBackground({ includeFolders: true });
|
||||
onNotify('success', t('txt_item_unarchived'));
|
||||
} catch (error) {
|
||||
patchCipherBatch([cipher.id], () => previousCipher);
|
||||
onNotify('error', error instanceof Error ? error.message : t('txt_unarchive_item_failed'));
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -382,12 +382,37 @@ export async function getPasswordHint(email: string): Promise<{ masterPasswordHi
|
||||
|
||||
export function createAuthedFetch(getSession: () => SessionState | null, setSession: SessionSetter) {
|
||||
return async function authedFetch(input: string, init: RequestInit = {}): Promise<Response> {
|
||||
const retryableRequest = async (headers: Headers): Promise<Response> => {
|
||||
const maxAttempts = 3;
|
||||
let lastError: unknown;
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
||||
try {
|
||||
const response = await fetch(input, { ...init, headers });
|
||||
if (response.status !== 429 && (response.status < 500 || response.status >= 600)) {
|
||||
return response;
|
||||
}
|
||||
lastError = new Error(`HTTP ${response.status}`);
|
||||
if (attempt === maxAttempts - 1) {
|
||||
return response;
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempt === maxAttempts - 1) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const delayMs = 250 * (2 ** attempt) + Math.floor(Math.random() * 120);
|
||||
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
|
||||
}
|
||||
throw lastError instanceof Error ? lastError : new Error('Request failed');
|
||||
};
|
||||
|
||||
const session = getSession();
|
||||
if (!session?.accessToken) throw new Error('Unauthorized');
|
||||
const headers = new Headers(init.headers || {});
|
||||
headers.set('Authorization', `Bearer ${session.accessToken}`);
|
||||
|
||||
let resp = await fetch(input, { ...init, headers });
|
||||
let resp = await retryableRequest(headers);
|
||||
if (resp.status !== 401 || (!session.refreshToken && session.authMode !== 'web-cookie')) return resp;
|
||||
|
||||
const refreshed = await refreshAccessToken(session);
|
||||
@@ -410,7 +435,7 @@ export function createAuthedFetch(getSession: () => SessionState | null, setSess
|
||||
|
||||
const retryHeaders = new Headers(init.headers || {});
|
||||
retryHeaders.set('Authorization', `Bearer ${nextSession.accessToken}`);
|
||||
resp = await fetch(input, { ...init, headers: retryHeaders });
|
||||
resp = await retryableRequest(retryHeaders);
|
||||
return resp;
|
||||
};
|
||||
}
|
||||
|
||||
+37
-1697
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,843 @@
|
||||
// Generated from the previous monolithic i18n table; keep values as plain strings for tree-friendly locale chunks.
|
||||
const en: Record<string, string> = {
|
||||
nav_account_settings: "Account Settings",
|
||||
nav_admin_panel: "Admin Panel",
|
||||
nav_device_management: "Device Management",
|
||||
nav_my_vault: "My Vault",
|
||||
nav_sends: "Sends",
|
||||
nav_backup_strategy: "Cloud Backup",
|
||||
nav_import_export: "Import & Export",
|
||||
backup_strategy_title: "Cloud Backup",
|
||||
backup_strategy_under_construction: "Under construction.",
|
||||
import_export_title: "Import & Export",
|
||||
import_export_under_construction: "Under construction.",
|
||||
txt_backup_export: "Export Backup",
|
||||
txt_backup_import: "Restore",
|
||||
txt_backup_include_attachments: "Include attachments",
|
||||
txt_backup_export_description: "Download a full instance backup ZIP for manual safekeeping.",
|
||||
txt_backup_import_description: "Upload a previously exported backup ZIP and restore it into this instance.",
|
||||
txt_backup_exporting: "Exporting...",
|
||||
txt_backup_importing: "Restoring...",
|
||||
txt_backup_restoring: "Restoring...",
|
||||
txt_backup_export_success: "Backup exported",
|
||||
txt_backup_import_success_relogin: "Backup restored. Please sign in again.",
|
||||
txt_backup_restore_success_relogin: "Backup restored. Please sign in again.",
|
||||
txt_backup_restore_completed_verified: "Backup file integrity verification passed.",
|
||||
txt_backup_restore_completed_without_checksum: "Backup restored. No filename integrity marker was available for verification.",
|
||||
txt_backup_remote_restore_completed_verified: "Remote backup integrity verification passed.",
|
||||
txt_backup_remote_restore_completed_without_checksum: "Remote backup restored. No filename integrity marker was available for verification.",
|
||||
txt_backup_restore_skipped_summary: "{reason}. Skipped {attachments} attachment(s).",
|
||||
txt_backup_restore_skipped_reason_default: "Some files could not be restored",
|
||||
txt_backup_export_failed: "Backup export failed",
|
||||
txt_backup_import_failed: "Backup restore failed",
|
||||
txt_backup_restore_failed: "Backup restore failed",
|
||||
txt_backup_integrity_check_failed: "Backup integrity verification failed",
|
||||
txt_backup_center_title: "Instance Backup",
|
||||
txt_backup_center_description: "Keep local exports for manual restore, and configure one daily remote backup target for unattended protection.",
|
||||
txt_backup_restore_note: "Restoring will overwrite the current instance if you choose the replace flow.",
|
||||
txt_backup_manual: "Manual Backup",
|
||||
txt_backup_manual_description: "Export a ZIP right now, or import a ZIP back into this instance.",
|
||||
txt_backup_destinations_title: "Backup Destinations",
|
||||
txt_backup_destinations_description: "Keep multiple WebDAV and E3 targets here. Select one on the left to edit or browse it.",
|
||||
txt_backup_recommend_title: "Recommended Storage",
|
||||
txt_backup_recommend_open_signup: "Open Signup",
|
||||
txt_backup_recommend_open_signup_aff: "Open Signup (AFF)",
|
||||
txt_backup_recommend_open_guide: "Open Guide",
|
||||
txt_backup_recommend_empty: "No recommendations yet.",
|
||||
txt_backup_recommend_referral_label: "Referral Code",
|
||||
txt_backup_recommend_referral_note: "Use it during signup to get 5 GB extra. The author receives 2 GB.",
|
||||
txt_backup_recommend_infinicloud_summary: "Only an email address is needed. 20 GB free, 25 GB total with the referral code.",
|
||||
txt_backup_recommend_infinicloud_step_1: "Register an InfiniCLOUD account with just your email address.",
|
||||
txt_backup_recommend_infinicloud_step_2_prefix: "Open",
|
||||
txt_backup_recommend_infinicloud_step_2_suffix: "and turn on Apps Connection.",
|
||||
txt_backup_recommend_infinicloud_step_3: "Use Connection ID as your WebDAV username and Apps Password as your WebDAV password.",
|
||||
txt_backup_recommend_infinicloud_step_4: "Enter referral code 2HC5E in Referral Bonus at the bottom of My Page to receive 5 GB extra.",
|
||||
txt_backup_recommend_open_password: "Password Settings",
|
||||
txt_backup_recommend_open_storage: "Open Storage",
|
||||
txt_backup_recommend_koofr_summary: "Only an email address is needed. 10 GB free, and it can bridge Google Drive, OneDrive, and Dropbox through WebDAV.",
|
||||
txt_backup_recommend_koofr_password_link: "Password Settings",
|
||||
txt_backup_recommend_koofr_storage_link: "Storage",
|
||||
txt_backup_recommend_koofr_step_1: "Register a Koofr account with just your email address.",
|
||||
txt_backup_recommend_koofr_step_2_prefix: "Open",
|
||||
txt_backup_recommend_koofr_step_2_suffix: ", generate a new app password, use your email address as the WebDAV username, and use the app password as the WebDAV password.",
|
||||
txt_backup_recommend_koofr_step_3: "Koofr's own WebDAV address is https://app.koofr.net/dav/Koofr.",
|
||||
txt_backup_recommend_koofr_step_4: "Koofr can also connect Google Drive, OneDrive, and Dropbox. Free users can connect up to two storage accounts.",
|
||||
txt_backup_recommend_koofr_step_5_prefix: "Open",
|
||||
txt_backup_recommend_koofr_step_5_suffix: ", click Connect in the left sidebar, and choose the cloud storage you want to attach.",
|
||||
txt_backup_recommend_koofr_dav_intro: "After a storage account is connected, keep the same email and app password, and only switch the WebDAV address:",
|
||||
txt_backup_recommend_koofr_dav_self: "Koofr",
|
||||
txt_backup_recommend_pcloud_summary: "Only an email address is needed. Up to 10 GB free, with standard WebDAV access.",
|
||||
txt_backup_recommend_pcloud_step_1: "Register a pCloud account with just your email address.",
|
||||
txt_backup_recommend_pcloud_step_2: "Use https://webdav.pcloud.com/ as the WebDAV server URL.",
|
||||
txt_backup_recommend_pcloud_step_3: "Use your registration email as the WebDAV username and your account password as the WebDAV password.",
|
||||
txt_backup_add_destination: "Add Destination",
|
||||
txt_backup_schedule_panel_title: "Automatic Schedule",
|
||||
txt_backup_schedule_panel_note: "Each destination can keep its own daily backup schedule.",
|
||||
txt_backup_scheduled_target: "Scheduled Target",
|
||||
txt_backup_destination_active_badge: "Auto On",
|
||||
txt_backup_destination_idle_badge: "Auto Off",
|
||||
txt_backup_destination_last_success: "Last success: {time}",
|
||||
txt_backup_destination_never_run: "No successful run yet",
|
||||
txt_backup_destination_detail_title: "Destination Details",
|
||||
txt_backup_destination_detail_note: "",
|
||||
txt_backup_destination_name: "Destination Name",
|
||||
txt_backup_set_scheduled_target: "Use For Daily Backup",
|
||||
txt_backup_delete_destination: "Delete",
|
||||
txt_backup_destination_deleted: "Backup destination deleted",
|
||||
txt_backup_delete_destination_confirm_message: "Delete backup destination \"{name}\"? This cannot be undone.",
|
||||
txt_backup_select_destination: "Select a backup destination from the list first.",
|
||||
txt_backup_remote_save_first: "Save this destination first before browsing its remote backup files.",
|
||||
txt_backup_automation: "Automatic Backup",
|
||||
txt_backup_automation_description: "Pick a destination, save the credentials, and let the worker upload one backup every day.",
|
||||
txt_backup_settings_saved: "Backup settings saved",
|
||||
txt_backup_settings_save_failed: "Saving backup settings failed",
|
||||
txt_backup_settings_load_failed: "Loading backup settings failed",
|
||||
txt_backup_save_settings: "Save Settings",
|
||||
txt_backup_saving: "Saving...",
|
||||
txt_backup_enable_action: "Enable",
|
||||
txt_backup_disable_action: "Disable",
|
||||
txt_backup_run_now: "Run Remote Backup Now",
|
||||
txt_backup_run_manual: "Run Manually",
|
||||
txt_backup_running_now: "Running...",
|
||||
txt_backup_remote_run_success: "Remote backup completed",
|
||||
txt_backup_remote_run_success_verified: "Remote backup completed and integrity verification passed.",
|
||||
txt_backup_remote_run_failed: "Remote backup failed",
|
||||
txt_backup_remote_title: "Remote Backups",
|
||||
txt_backup_remote_note: "Browse the saved destination and choose a backup ZIP to download or restore.",
|
||||
txt_backup_remote_saved_basis: "Remote browsing uses the last saved destination settings, not unsaved form edits.",
|
||||
txt_backup_remote_refresh: "Refresh",
|
||||
txt_backup_remote_root: "Root",
|
||||
txt_backup_remote_up: "Up",
|
||||
txt_backup_remote_open: "Open",
|
||||
txt_backup_remote_download: "Download",
|
||||
txt_backup_remote_downloading: "Downloading...",
|
||||
txt_backup_remote_restore: "Restore",
|
||||
txt_backup_remote_restore_stage_prepare: "Preparing remote backup restore...",
|
||||
txt_backup_remote_restore_stage_replace: "Clearing current data and restoring remote backup...",
|
||||
txt_backup_progress_kicker: "Backup Task",
|
||||
txt_backup_progress_subject: "Current item: {name}",
|
||||
txt_backup_restore_progress_kicker: "Restore Progress",
|
||||
txt_backup_restore_progress_local_title: "Restoring local backup",
|
||||
txt_backup_restore_progress_remote_title: "Restoring remote backup",
|
||||
txt_backup_export_progress_title: "Exporting backup",
|
||||
txt_backup_remote_run_progress_title: "Running remote backup",
|
||||
txt_backup_restore_progress_file: "Current file: {name}",
|
||||
txt_backup_restore_progress_elapsed: "{seconds}s elapsed",
|
||||
txt_backup_archive_progress_collect_title: "Collecting vault data",
|
||||
txt_backup_archive_progress_collect_detail: "The server is reading database tables and assembling the backup payload.",
|
||||
txt_backup_archive_progress_collect_with_attachments_detail: "The server is reading database tables and collecting attachment metadata for the backup payload.",
|
||||
txt_backup_archive_progress_package_title: "Packaging backup archive",
|
||||
txt_backup_archive_progress_package_detail: "The server is generating the backup ZIP and computing its checksum prefix.",
|
||||
txt_backup_archive_progress_package_with_attachments_detail: "The server is generating the backup ZIP metadata and computing its checksum prefix for the attachment-aware export.",
|
||||
txt_backup_archive_progress_ready_title: "Preparing download",
|
||||
txt_backup_archive_progress_ready_detail: "The backup archive is ready and is being returned to the browser.",
|
||||
txt_backup_export_progress_fetch_attachments_title: "Downloading attachment files",
|
||||
txt_backup_export_progress_fetch_attachments_detail: "The browser is fetching attachment objects and adding them into the export package.",
|
||||
txt_backup_export_progress_rebuild_title: "Rebuilding export archive",
|
||||
txt_backup_export_progress_rebuild_detail: "The browser is rebuilding the final ZIP and refreshing its checksum suffix.",
|
||||
txt_backup_export_progress_save_title: "Saving export file",
|
||||
txt_backup_export_progress_save_detail: "The browser is preparing the final backup file for download.",
|
||||
txt_backup_export_progress_complete_title: "Export completed",
|
||||
txt_backup_export_progress_complete_detail: "The backup export is ready.",
|
||||
txt_backup_export_progress_failed_title: "Export failed",
|
||||
txt_backup_export_progress_failed_detail: "The backup export could not be completed.",
|
||||
txt_backup_remote_run_progress_prepare_title: "Preparing remote backup",
|
||||
txt_backup_remote_run_progress_prepare_detail: "The server is loading the selected destination and preparing this backup run.",
|
||||
txt_backup_remote_run_progress_sync_attachments_title: "Checking attachment index",
|
||||
txt_backup_remote_run_progress_sync_attachments_detail: "The server is comparing attachment metadata so only missing attachment objects are uploaded.",
|
||||
txt_backup_remote_run_progress_sync_attachments_skipped_detail: "This backup does not include attachments, so attachment synchronization is skipped.",
|
||||
txt_backup_remote_run_progress_upload_title: "Uploading backup archive",
|
||||
txt_backup_remote_run_progress_upload_detail: "The server is uploading the backup ZIP to the remote destination.",
|
||||
txt_backup_remote_run_progress_verify_title: "Verifying uploaded archive",
|
||||
txt_backup_remote_run_progress_verify_detail: "The server is downloading the uploaded ZIP back and verifying its checksum and size.",
|
||||
txt_backup_remote_run_progress_cleanup_title: "Cleaning older backups",
|
||||
txt_backup_remote_run_progress_cleanup_detail: "The server is pruning older backup files according to the retention policy.",
|
||||
txt_backup_remote_run_progress_complete_title: "Remote backup completed",
|
||||
txt_backup_remote_run_progress_complete_detail: "The remote backup has been uploaded and verified successfully.",
|
||||
txt_backup_remote_run_progress_failed_title: "Remote backup failed",
|
||||
txt_backup_remote_run_progress_failed_detail: "The remote backup could not be completed.",
|
||||
txt_backup_restore_progress_local_upload_title: "Uploading backup archive",
|
||||
txt_backup_restore_progress_local_upload_detail: "The selected ZIP is being sent to the server for processing.",
|
||||
txt_backup_restore_progress_local_shadow_title: "Creating shadow workspace",
|
||||
txt_backup_restore_progress_local_shadow_detail: "The server is preparing an isolated restore area so the current data remains untouched until validation passes.",
|
||||
txt_backup_restore_progress_local_data_title: "Writing vault data",
|
||||
txt_backup_restore_progress_local_data_detail: "The server is importing users, folders, vault items, and related metadata into shadow tables.",
|
||||
txt_backup_restore_progress_local_files_title: "Restoring attachment files",
|
||||
txt_backup_restore_progress_local_files_detail: "The server is writing attachment objects back to storage and removing any attachment rows that cannot be restored.",
|
||||
txt_backup_restore_progress_local_finalize_title: "Validating and switching data",
|
||||
txt_backup_restore_progress_local_finalize_detail: "The server is performing final validation and then swapping the verified restore data into the live tables.",
|
||||
txt_backup_restore_progress_remote_fetch_title: "Reading remote backup",
|
||||
txt_backup_restore_progress_remote_fetch_detail: "The server is downloading the selected backup package from the remote destination.",
|
||||
txt_backup_restore_progress_remote_shadow_title: "Creating shadow workspace",
|
||||
txt_backup_restore_progress_remote_shadow_detail: "The server is preparing an isolated restore area so the current data remains untouched until validation passes.",
|
||||
txt_backup_restore_progress_remote_data_title: "Writing vault data",
|
||||
txt_backup_restore_progress_remote_data_detail: "The server is importing users, folders, vault items, and related metadata into shadow tables.",
|
||||
txt_backup_restore_progress_remote_files_title: "Restoring remote attachments",
|
||||
txt_backup_restore_progress_remote_files_detail: "The server is fetching required attachment objects from remote storage and writing them back into local storage.",
|
||||
txt_backup_restore_progress_remote_finalize_title: "Validating and switching data",
|
||||
txt_backup_restore_progress_remote_finalize_detail: "The server is performing final validation and then switching the verified restore data into the live tables.",
|
||||
txt_backup_remote_loading: "Loading remote backups...",
|
||||
txt_backup_remote_cached_empty: "Click Refresh to load this destination.",
|
||||
txt_backup_remote_empty: "No backup files found in this folder.",
|
||||
txt_backup_remote_folder: "Folder",
|
||||
txt_backup_remote_unknown_time: "Unknown time",
|
||||
txt_backup_remote_current_path: "Current Folder",
|
||||
txt_backup_remote_load_failed: "Loading remote backups failed",
|
||||
txt_backup_remote_invalid_response: "Invalid remote backup response",
|
||||
txt_backup_remote_download_failed: "Downloading remote backup failed",
|
||||
txt_backup_remote_delete_success: "Remote backup deleted",
|
||||
txt_backup_remote_delete_failed: "Deleting remote backup failed",
|
||||
txt_backup_remote_delete_confirm_message: "Delete backup file \"{name}\"? This cannot be undone.",
|
||||
txt_backup_remote_deleting: "Deleting...",
|
||||
txt_backup_remote_restore_failed: "Restoring remote backup failed",
|
||||
txt_backup_restore_checksum_warning_title: "Backup Integrity Warning",
|
||||
txt_backup_restore_checksum_warning_message: "The selected backup file \"{name}\" failed filename integrity verification. Expected prefix {expected}, actual prefix {actual}. The file may be incomplete or corrupted. Continuing may restore damaged data.",
|
||||
txt_backup_remote_restore_checksum_warning_message: "The remote backup file \"{name}\" failed filename integrity verification. Expected prefix {expected}, actual prefix {actual}. The file may be corrupted during upload or storage. Continuing may restore damaged data and may cause serious data loss.",
|
||||
txt_backup_restore_checksum_warning_message_fallback: "The selected backup file failed integrity verification. Continuing may restore damaged data.",
|
||||
txt_backup_restore_checksum_warning_confirm: "Continue Restore",
|
||||
txt_backup_remote_restore_invalid_response: "Invalid remote backup restore response",
|
||||
txt_backup_remote_run_invalid_response: "Invalid remote backup run response",
|
||||
txt_backup_settings_invalid_response: "Invalid backup settings response",
|
||||
txt_backup_import_invalid_response: "Invalid backup import response",
|
||||
txt_backup_destination: "Backup Destination",
|
||||
txt_backup_protocol_webdav: "WebDAV",
|
||||
txt_backup_protocol_e3: "E3",
|
||||
txt_backup_recommend_group_webdav: "WebDAV",
|
||||
txt_backup_recommend_group_s3: "S3",
|
||||
txt_backup_destination_name_default_webdav: "WebDAV {index}",
|
||||
txt_backup_destination_name_default_e3: "E3 {index}",
|
||||
txt_backup_type: "Backup Type",
|
||||
txt_backup_destination_reserved: "Reserved Slot",
|
||||
txt_backup_time: "Backup Time",
|
||||
txt_backup_start_time: "Start Time",
|
||||
txt_backup_timezone: "Timezone",
|
||||
txt_backup_interval_hours: "Every",
|
||||
txt_backup_interval_hours_suffix: "hours",
|
||||
txt_backup_interval_hours_presets: "Quick interval presets",
|
||||
txt_backup_frequency: "Frequency",
|
||||
txt_backup_frequency_daily: "Daily",
|
||||
txt_backup_frequency_weekly: "Weekly",
|
||||
txt_backup_frequency_monthly: "Monthly",
|
||||
txt_backup_day_of_week: "Day of Week",
|
||||
txt_backup_day_of_month: "Day of Month",
|
||||
txt_backup_weekday_monday: "Monday",
|
||||
txt_backup_weekday_tuesday: "Tuesday",
|
||||
txt_backup_weekday_wednesday: "Wednesday",
|
||||
txt_backup_weekday_thursday: "Thursday",
|
||||
txt_backup_weekday_friday: "Friday",
|
||||
txt_backup_weekday_saturday: "Saturday",
|
||||
txt_backup_weekday_sunday: "Sunday",
|
||||
txt_backup_retention_count: "Keep",
|
||||
txt_backup_retention_count_suffix: "items",
|
||||
txt_backup_retention_count_hint: "Leave empty to keep all backup files. New destinations default to 30.",
|
||||
txt_backup_destination_include_attachments: "Include attachments",
|
||||
txt_backup_include_attachments_help_button: "Attachment backup help",
|
||||
txt_backup_include_attachments_help: "Attachments are stored incrementally in the remote attachments folder, so later backups usually only upload new files. Deleting an attachment locally does not remove earlier remote copies. During restore, NodeWarden reads the required files from the attachments folder and skips any attachment that is no longer available.",
|
||||
txt_backup_enable_schedule: "Enable automatic daily backup",
|
||||
txt_backup_schedule_note: "The worker checks the schedule every 5 minutes. It starts at the selected time in the selected timezone, then repeats by the chosen hour interval, and resets from that start time each day.",
|
||||
txt_backup_schedule_disabled: "Disabled",
|
||||
txt_backup_schedule_status: "Schedule",
|
||||
txt_backup_schedule_summary: "Start at {time}, every {interval} hours ({timezone})",
|
||||
txt_backup_schedule_empty: "No automatic backup plans are enabled yet.",
|
||||
txt_backup_last_success: "Last Success",
|
||||
txt_backup_last_target: "Last Target",
|
||||
txt_backup_last_file: "Last File",
|
||||
txt_backup_last_error_prefix: "Last Error",
|
||||
txt_backup_none_yet: "No remote backup has completed yet",
|
||||
txt_backup_not_configured: "Not configured",
|
||||
txt_backup_never: "Never",
|
||||
txt_backup_unknown_size: "Unknown size",
|
||||
txt_backup_webdav_url: "WebDAV Server URL",
|
||||
txt_backup_webdav_username: "WebDAV Username",
|
||||
txt_backup_webdav_password: "WebDAV Password",
|
||||
txt_backup_webdav_path: "Remote Folder",
|
||||
txt_backup_e3_endpoint: "E3 Endpoint",
|
||||
txt_backup_e3_bucket: "Bucket",
|
||||
txt_backup_e3_region: "Region",
|
||||
txt_backup_e3_access_key: "Access Key",
|
||||
txt_backup_e3_secret_key: "Secret Key",
|
||||
txt_backup_e3_path: "Remote Path",
|
||||
txt_backup_reserved_name: "Reserved Provider Name",
|
||||
txt_backup_reserved_notes: "Reserved Notes",
|
||||
txt_backup_reserved_notes_placeholder: "Leave a note for the next destination type",
|
||||
txt_backup_reserved_hint: "This slot is reserved for a future destination. You can save notes now, but automatic uploads stay disabled.",
|
||||
txt_backup_file: "Backup File",
|
||||
txt_backup_file_required: "Please select a backup file",
|
||||
txt_backup_no_file_selected: "No backup file selected",
|
||||
txt_backup_selected_file_name: "Selected file: {name}",
|
||||
txt_backup_replace_confirm_title: "Replace Current Instance Data",
|
||||
txt_backup_replace_confirm_message: "The current instance already contains data. Continue restoring and replace the current instance data with the selected backup after verification succeeds?",
|
||||
txt_backup_clear_and_import: "Replace and Import",
|
||||
txt_backup_clear_and_restore: "Replace and Restore",
|
||||
txt_access_count: "Access Count",
|
||||
txt_accessed_count_times: "Accessed {count} times",
|
||||
txt_actions: "Actions",
|
||||
txt_add: "Add",
|
||||
txt_add_field: "Add Field",
|
||||
txt_add_website: "Add Website",
|
||||
txt_added: "Added",
|
||||
txt_additional_options: "Additional Options",
|
||||
txt_address: "Address",
|
||||
txt_address_1: "Address 1",
|
||||
txt_address_2: "Address 2",
|
||||
txt_address_3: "Address 3",
|
||||
txt_all_device_authorizations_revoked: "All device trust revoked",
|
||||
txt_all_invites_deleted: "All invites deleted",
|
||||
txt_all_items: "All Items",
|
||||
txt_all_sends: "All Sends",
|
||||
txt_android: "Android",
|
||||
txt_are_you_sure_you_want_to_delete_count_selected_items: "Are you sure you want to delete {count} selected items?",
|
||||
txt_are_you_sure_you_want_to_delete_count_selected_items_permanently: "Are you sure you want to permanently delete {count} selected items?",
|
||||
txt_are_you_sure_you_want_to_delete_this_item: "Are you sure you want to delete this item?",
|
||||
txt_are_you_sure_you_want_to_delete_this_passkey: "Are you sure you want to delete this passkey?",
|
||||
txt_are_you_sure_you_want_to_log_out: "Are you sure you want to log out?",
|
||||
txt_authenticator_key: "Authenticator Key",
|
||||
txt_authorized_devices: "Authorized Devices",
|
||||
txt_auto_copy_link_after_save: "Auto copy link after save",
|
||||
txt_autofill_options: "Autofill Options",
|
||||
txt_back_to_login: "Back To Login",
|
||||
txt_ban: "Ban",
|
||||
txt_boolean: "Boolean",
|
||||
txt_brand: "Brand",
|
||||
txt_bulk_delete_failed: "Bulk delete failed",
|
||||
txt_bulk_permanent_delete_failed: "Bulk permanent delete failed",
|
||||
txt_bulk_restore_failed: "Bulk restore failed",
|
||||
txt_bulk_delete_sends_failed: "Bulk delete sends failed",
|
||||
txt_bulk_move_failed: "Bulk move failed",
|
||||
txt_cancel: "Cancel",
|
||||
txt_continue: "Continue",
|
||||
txt_card: "Card",
|
||||
txt_card_details: "Card Details",
|
||||
txt_cardholder_name: "Cardholder Name",
|
||||
txt_change_master_password: "Change Master Password",
|
||||
txt_change_password: "Change Password",
|
||||
txt_change_password_failed: "Change password failed",
|
||||
txt_change_password_confirm_and_sign_out_all_devices: "Changing the master password will sign out all devices, including this web session. Continue?",
|
||||
txt_copy_failed: "Copy failed",
|
||||
txt_checked: "Checked",
|
||||
txt_choose_destination_folder: "Choose destination folder.",
|
||||
txt_chrome_browser: "Chrome Browser",
|
||||
txt_chrome_extension: "Chrome Extension",
|
||||
txt_city_town: "City / Town",
|
||||
txt_code: "Code",
|
||||
txt_company: "Company",
|
||||
txt_configure_custom_field_values: "Configure custom field values.",
|
||||
txt_confirm: "Confirm",
|
||||
txt_confirm_master_password: "Confirm Master Password",
|
||||
txt_confirm_password: "Confirm Password",
|
||||
txt_copy: "Copy",
|
||||
txt_code_copied: "Code copied",
|
||||
txt_copy_code: "Copy Code",
|
||||
txt_copy_link: "Copy Link",
|
||||
txt_copy_secret: "Copy Secret",
|
||||
txt_country: "Country",
|
||||
txt_create: "Create",
|
||||
txt_create_account: "Create Account",
|
||||
txt_registering: "Creating account...",
|
||||
txt_create_folder: "Create Folder",
|
||||
txt_create_folder_failed: "Create folder failed",
|
||||
txt_create_item_failed: "Create item failed",
|
||||
txt_create_send_failed: "Create send failed",
|
||||
txt_create_timed_invite: "Create Timed Invite",
|
||||
txt_created_value: "Created: {value}",
|
||||
txt_current_new_password_is_required: "Current/new password is required",
|
||||
txt_current_password: "Current Password",
|
||||
txt_custom_fields: "Custom Fields",
|
||||
txt_decrypt_failed: "(Decrypt failed)",
|
||||
txt_decrypt_failed_2: "Decrypt failed",
|
||||
txt_delete: "Delete",
|
||||
txt_delete_all: "Delete All",
|
||||
txt_delete_all_invite_codes_active_inactive: "Delete all invite codes (active/inactive)?",
|
||||
txt_delete_all_invites: "Delete all invites",
|
||||
txt_delete_item: "Delete Item",
|
||||
txt_delete_passkey: "Delete Passkey",
|
||||
txt_delete_item_failed: "Delete item failed",
|
||||
txt_delete_permanently: "Delete Permanently",
|
||||
txt_archive: "Archive",
|
||||
txt_archive_item: "Archive Item",
|
||||
txt_archive_item_message: "After archiving, this item will be excluded from general search results and autofill suggestions.",
|
||||
txt_archive_selected_items: "Archive Items",
|
||||
txt_archive_selected_items_message: "After archiving, {count} selected items will be excluded from general search results and autofill suggestions.",
|
||||
txt_archived: "Archived",
|
||||
txt_archive_selected: "Archive",
|
||||
txt_item_archived: "Item archived",
|
||||
txt_item_unarchived: "Item unarchived",
|
||||
txt_archived_selected_items: "Archived selected items",
|
||||
txt_unarchived_selected_items: "Unarchived selected items",
|
||||
txt_archive_item_failed: "Archive item failed",
|
||||
txt_unarchive_item_failed: "Unarchive item failed",
|
||||
txt_bulk_archive_failed: "Bulk archive failed",
|
||||
txt_bulk_unarchive_failed: "Bulk unarchive failed",
|
||||
txt_unarchive: "Unarchive",
|
||||
txt_delete_selected: "Delete",
|
||||
txt_delete_selected_items: "Delete Selected Items",
|
||||
txt_delete_selected_items_permanently: "Delete Selected Items Permanently",
|
||||
txt_delete_send_failed: "Delete send failed",
|
||||
txt_delete_this_user_and_all_user_data: "Delete this user and all user data?",
|
||||
txt_delete_user: "Delete user",
|
||||
txt_deleted_selected_items: "Deleted selected items",
|
||||
txt_deleted_selected_items_permanently: "Permanently deleted selected items",
|
||||
txt_restored_selected_items: "Restored selected items",
|
||||
txt_deleted_selected_sends: "Deleted selected sends",
|
||||
txt_deletion_date: "Deletion Date",
|
||||
txt_deletion_days: "Deletion Days",
|
||||
txt_device: "Device",
|
||||
txt_device_authorization_revoked: "Device trust revoked",
|
||||
txt_device_management: "Device Management",
|
||||
txt_device_note: "Device Note",
|
||||
txt_device_note_required: "Device name is required",
|
||||
txt_device_note_updated: "Device name updated",
|
||||
txt_device_removed: "Device removed",
|
||||
txt_load_devices_failed: "Failed to load devices",
|
||||
txt_disable_this_send: "Disable this send",
|
||||
txt_disable_totp: "Disable TOTP",
|
||||
txt_disable_totp_failed: "Disable TOTP failed",
|
||||
txt_download: "Download",
|
||||
txt_downloading: "Downloading...",
|
||||
txt_downloading_percent: "Downloading {percent}%",
|
||||
txt_attachment: "Attachment",
|
||||
txt_uploading_attachment_named: "Uploading {name}...",
|
||||
txt_uploading_attachment_named_percent: "Uploading {name} {percent}%",
|
||||
txt_uploading_file_named: "Uploading {name}...",
|
||||
txt_uploading_file_named_percent: "Uploading {name} {percent}%",
|
||||
txt_download_failed: "Download failed",
|
||||
txt_edge_browser: "Edge Browser",
|
||||
txt_edge_extension: "Edge Extension",
|
||||
txt_edit: "Edit",
|
||||
txt_edit_send: "Edit Send",
|
||||
txt_email: "Email",
|
||||
txt_email_password_and_recovery_code_are_required: "Email, password and recovery code are required",
|
||||
txt_enable_totp: "Enable TOTP",
|
||||
txt_enable_totp_failed: "Enable TOTP failed",
|
||||
txt_enabled: "Enabled",
|
||||
txt_encrypted_file: "Encrypted File",
|
||||
txt_encrypted_file_2: "Encrypted file",
|
||||
txt_enter_a_folder_name: "Enter a folder name.",
|
||||
txt_enter_master_password_to_disable_two_step_verification: "Enter master password to disable two-step verification.",
|
||||
txt_enter_master_password_to_continue: "Enter your master password to continue.",
|
||||
txt_enter_master_password_to_view_this_item: "Enter master password to view this item.",
|
||||
txt_expiration_date: "Expiration Date",
|
||||
txt_expiration_days_0_never: "Expiration Days (0 = never)",
|
||||
txt_expires_at: "Expires At",
|
||||
txt_expires_at_value: "Expires at: {value}",
|
||||
txt_expiry: "Expiry",
|
||||
txt_expiry_month: "Expiry Month",
|
||||
txt_expiry_year: "Expiry Year",
|
||||
txt_failed_to_open_send: "Failed to open send",
|
||||
txt_favorite: "Favorite",
|
||||
txt_favorites: "Favorites",
|
||||
txt_duplicates: "Duplicates",
|
||||
txt_field: "Field",
|
||||
txt_field_label: "Field Label",
|
||||
txt_field_label_is_required: "Field label is required.",
|
||||
txt_field_type: "Field Type",
|
||||
txt_field_value: "Field Value",
|
||||
txt_file: "File",
|
||||
txt_file_name: "File Name",
|
||||
txt_file_send: "File Send",
|
||||
txt_file_size: "File Size",
|
||||
txt_fingerprint: "Fingerprint",
|
||||
txt_firefox_browser: "Firefox Browser",
|
||||
txt_firefox_extension: "Firefox Extension",
|
||||
txt_first_name: "First Name",
|
||||
txt_folder: "Folder",
|
||||
txt_folder_created: "Folder created",
|
||||
txt_folder_name: "Folder Name",
|
||||
txt_folder_name_is_required: "Folder name is required",
|
||||
txt_folders: "Folders",
|
||||
txt_hidden: "Hidden",
|
||||
txt_hide: "Hide",
|
||||
txt_identity: "Identity",
|
||||
txt_identity_details: "Identity Details",
|
||||
txt_ie_browser: "IE Browser",
|
||||
txt_invite_code_optional: "Invite Code (Not required for the first account; required for all others)",
|
||||
txt_invite_created: "Invite created",
|
||||
txt_invite_revoked: "Invite revoked",
|
||||
txt_invite_validity_hours: "Invite validity (hours)",
|
||||
txt_invites: "Invites",
|
||||
txt_ios: "iOS",
|
||||
txt_item: "Item",
|
||||
txt_item_created: "Item created",
|
||||
txt_item_deleted: "Item deleted",
|
||||
txt_item_history: "Item History",
|
||||
txt_password_history: "Password History",
|
||||
txt_password_updated_value: "Password updated: {value}",
|
||||
txt_item_name_is_required: "Item name is required.",
|
||||
txt_item_updated: "Item updated",
|
||||
txt_last_edited_value: "Last edited: {value}",
|
||||
txt_last_name: "Last Name",
|
||||
txt_last_seen: "Last Seen",
|
||||
txt_license_number: "License Number",
|
||||
txt_link_copied: "Link copied",
|
||||
txt_linked: "Linked",
|
||||
txt_linux_desktop: "Linux Desktop",
|
||||
txt_loading: "Loading...",
|
||||
txt_loading_nodewarden: "Loading NodeWarden...",
|
||||
txt_jwt_warning_title: "Server Security Warning",
|
||||
txt_jwt_warning_subtitle: "JWT secret is not configured safely.",
|
||||
txt_jwt_title_missing: "JWT_SECRET is missing",
|
||||
txt_jwt_title_too_short: "JWT_SECRET is too short",
|
||||
txt_jwt_title_default: "JWT_SECRET is using the default value",
|
||||
txt_jwt_reason_missing: "JWT secret is missing.",
|
||||
txt_jwt_reason_default: "JWT secret is still the default/sample value.",
|
||||
txt_jwt_reason_too_short: "JWT secret is too short. Minimum length is {min}.",
|
||||
txt_jwt_how_to_fix_add: "How to add JWT_SECRET",
|
||||
txt_jwt_how_to_fix_replace: "How to replace JWT_SECRET",
|
||||
txt_jwt_add_step_1: "Use the 32-character generator below and copy a new key.",
|
||||
txt_jwt_add_step_2_prefix: "Go to Cloudflare Dashboard -> Workers & Pages -> Your Service -> ",
|
||||
txt_jwt_add_step_2_suffix: " -> Variables and Secrets -> Add",
|
||||
txt_jwt_add_step_3: "Save and wait for redeploy, then refresh this page.",
|
||||
txt_jwt_replace_step_1: "Use the 32-character generator below and create a stronger key (minimum {min} characters).",
|
||||
txt_jwt_replace_step_2_prefix: "Go to Cloudflare Dashboard -> Workers & Pages -> Your Service -> ",
|
||||
txt_jwt_replace_step_2_suffix: " -> Variables and Secrets -> Update JWT_SECRET",
|
||||
txt_jwt_replace_step_3: "Save and wait for redeploy, then refresh this page.",
|
||||
txt_jwt_secret_type_label: "Type:",
|
||||
txt_jwt_secret_type_value: "Secret",
|
||||
txt_jwt_secret_name_label: "Variable name:",
|
||||
txt_jwt_secret_value_label: "Value:",
|
||||
txt_jwt_secret_value_requirement: "Random string with at least {min} characters",
|
||||
txt_jwt_what_is: "What is JWT?",
|
||||
txt_jwt_what_is_body: "JWT_SECRET is the server-side signing key used to issue and verify login tokens. If it is missing, too short, or still using the sample value, the instance is not safe to use normally.",
|
||||
txt_how_to_fix: "How to fix",
|
||||
txt_jwt_fix_step_1: "Open your deployment environment variables.",
|
||||
txt_jwt_fix_step_2: "If your current key is not random enough, use the 32-character generator below.",
|
||||
txt_jwt_fix_step_3: "Cloudflare Dashboard -> Workers & Pages -> Your Service -> Settings -> Variables and Secrets, update JWT_SECRET.",
|
||||
txt_jwt_fix_step_4: "Save and wait for redeploy, then refresh this page to verify.",
|
||||
txt_random_secret_generator: "Random Secret Generator",
|
||||
txt_copied: "Copied",
|
||||
txt_log_in: "Log In",
|
||||
txt_logging_in: "Logging in...",
|
||||
txt_log_out: "Log Out",
|
||||
txt_lock: "Lock",
|
||||
txt_menu: "Menu",
|
||||
txt_settings: "Settings",
|
||||
txt_back: "Back",
|
||||
txt_login: "Login",
|
||||
txt_login_credentials: "Login Credentials",
|
||||
txt_login_failed: "Login failed",
|
||||
txt_login_success: "Login success",
|
||||
txt_macos_desktop: "macOS Desktop",
|
||||
txt_manage_authorized_devices_and_30_day_totp_trusted_sessions: "Manage authorized devices and 30-day TOTP trusted sessions.",
|
||||
txt_manage_device_sessions_and_30_day_totp_trusted_sessions: "Manage device sessions and 30-day TOTP trusted sessions.",
|
||||
txt_master_password: "Master Password",
|
||||
txt_master_password_changed_please_login_again: "Master password changed. Please login again.",
|
||||
txt_master_password_changed_signing_out_everywhere: "Master password changed. Signing out all devices.",
|
||||
txt_master_password_is_required: "Master password is required",
|
||||
txt_master_password_is_required_2: "Master password is required.",
|
||||
txt_master_password_must_be_at_least_12_chars: "Master password must be at least 12 chars",
|
||||
txt_master_password_reprompt: "Master password reprompt",
|
||||
txt_master_password_reprompt_2: "Master Password Reprompt",
|
||||
txt_max_access_count: "Max Access Count",
|
||||
txt_middle_name: "Middle Name",
|
||||
txt_drag_to_reorder: "Drag to reorder",
|
||||
txt_move: "Move",
|
||||
txt_move_selected_items: "Move Selected Items",
|
||||
txt_moved_selected_items: "Moved selected items",
|
||||
txt_name: "Name",
|
||||
txt_name_is_required: "Name is required",
|
||||
txt_new_password: "New Password",
|
||||
txt_nothing_to_copy: "Nothing to copy",
|
||||
txt_new_password_must_be_at_least_12_chars: "New password must be at least 12 chars",
|
||||
txt_new_passwords_do_not_match: "New passwords do not match",
|
||||
txt_new_send: "New Send",
|
||||
txt_next: "Next",
|
||||
txt_no: "No",
|
||||
txt_no_devices_found: "No devices found.",
|
||||
txt_no_folder: "No Folder",
|
||||
txt_no_items: "No items",
|
||||
txt_no_username: "(No username)",
|
||||
txt_no_verification_codes: "No verification codes",
|
||||
txt_no_name: "(No Name)",
|
||||
txt_no_sends: "No sends",
|
||||
txt_nodewarden_send: "NodeWarden Send",
|
||||
txt_not_trusted: "Not trusted",
|
||||
txt_note: "Note",
|
||||
txt_notes: "Notes",
|
||||
txt_replace_device_name_with_note: "Set a custom name for this device without changing its detected system type.",
|
||||
txt_number: "Number",
|
||||
txt_open: "Open",
|
||||
txt_opera_browser: "Opera Browser",
|
||||
txt_opera_extension: "Opera Extension",
|
||||
txt_or: "or",
|
||||
txt_options: "Options",
|
||||
txt_passport_number: "Passport Number",
|
||||
txt_password: "Password",
|
||||
txt_password_is_already_verified: "Password is already verified.",
|
||||
txt_passwords_do_not_match: "Passwords do not match",
|
||||
txt_password_hint: "Password Hint",
|
||||
txt_password_hint_optional: "Password Hint (optional)",
|
||||
txt_password_hint_placeholder: "A clue only you would understand",
|
||||
txt_password_hint_register_placeholder: "This hint can be shown directly on the web login page.",
|
||||
txt_password_hint_register_help: "This hint can be shown directly on the web login page. Do not include your master password, recovery code, or anything that can reveal it outright.",
|
||||
txt_password_hint_login_help: "Forgot the master password? Reveal the hint you saved during registration.",
|
||||
txt_password_hint_login_note: "Only a hint is shown here. It should help you remember the password, not expose it.",
|
||||
txt_show_password_hint: "Show Password Hint",
|
||||
txt_hide_password_hint: "Hide Password Hint",
|
||||
txt_loading_password_hint: "Loading hint...",
|
||||
txt_password_hint_not_set: "No password hint is available for this email.",
|
||||
txt_password_hint_load_failed: "Failed to load password hint",
|
||||
txt_password_hint_too_long: "Password hint must be 120 characters or fewer",
|
||||
txt_passkey: "Passkey",
|
||||
txt_passkeys: "Passkeys",
|
||||
txt_passkey_created_at_value: "Created on {value}",
|
||||
txt_phone: "Phone",
|
||||
txt_please_input_email_and_password: "Please input email and password",
|
||||
txt_please_input_master_password: "Please input master password",
|
||||
txt_please_input_totp_code: "Please input TOTP code",
|
||||
txt_please_select_a_file: "Please select a file",
|
||||
txt_postal_code: "Postal Code",
|
||||
txt_prev: "Prev",
|
||||
txt_private_key: "Private Key",
|
||||
txt_profile: "Profile",
|
||||
txt_profile_unavailable: "Profile unavailable",
|
||||
txt_profile_updated: "Profile updated",
|
||||
txt_public_key: "Public Key",
|
||||
txt_recover_2fa_failed: "Recover 2FA failed",
|
||||
txt_recover_two_step_login: "Recover Two-step Login",
|
||||
txt_recovered_but_auto_login_failed_please_sign_in: "Recovered but auto-login failed, please sign in.",
|
||||
txt_recovery_code: "Recovery Code",
|
||||
txt_recovery_code_and_api_key: "Recovery Code and API Key",
|
||||
txt_recovery_code_copied: "Recovery code copied",
|
||||
txt_recovery_code_is_empty: "Recovery code is empty",
|
||||
txt_recovery_code_loaded: "Recovery code loaded",
|
||||
txt_api_key: "API Key",
|
||||
txt_view_api_key: "View API Key",
|
||||
txt_rotate_api_key: "Rotate API Key",
|
||||
txt_api_key_copied: "API key copied",
|
||||
txt_api_key_loaded: "API key loaded",
|
||||
txt_api_key_rotated: "API key rotated",
|
||||
txt_rotate_api_key_confirm: "Rotate API key? The current key will stop working immediately.",
|
||||
txt_api_key_is_empty: "API key is empty",
|
||||
txt_api_key_dialog_intro: "Your API key can be used to authenticate with the Bitwarden CLI.",
|
||||
txt_api_key_warning_body: "Your API key is an alternative authentication mechanism. Keep it secret.",
|
||||
txt_oauth_client_credentials: "OAuth 2.0 Client Credentials",
|
||||
txt_client_id: "client_id",
|
||||
txt_client_secret: "client_secret",
|
||||
txt_scope: "scope",
|
||||
txt_grant_type: "grant_type",
|
||||
txt_refresh: "Refresh",
|
||||
txt_refresh_in_seconds_s: "Refresh in {seconds}s",
|
||||
txt_regenerate: "Regenerate",
|
||||
txt_registration_succeeded_please_sign_in: "Registration succeeded. Please sign in.",
|
||||
txt_remove: "Remove",
|
||||
txt_remove_device: "Remove device",
|
||||
txt_remove_device_2: "Remove Device",
|
||||
txt_remove_all_devices: "Remove all devices",
|
||||
txt_remove_all_devices_and_clear_all_2fa_trust: "Remove all devices and clear all 2FA trust?",
|
||||
txt_remove_all_devices_and_sign_out_all_sessions: "Remove all devices, clear all trust, and sign out every device?",
|
||||
txt_remove_device_name_and_clear_its_2fa_trust: "Remove device \"{name}\" and clear its 2FA trust?",
|
||||
txt_remove_device_and_sign_out_name: "Remove device \"{name}\", clear its trust, and sign it out?",
|
||||
txt_reveal: "Reveal",
|
||||
txt_restore: "Restore",
|
||||
txt_revoke: "Revoke",
|
||||
txt_revoke_30_day_totp_trust_for_name: "Revoke 30-day TOTP trust for \"{name}\"?",
|
||||
txt_revoke_30_day_totp_trust_from_all_devices: "Revoke 30-day TOTP trust from all devices?",
|
||||
txt_revoke_all_trusted: "Revoke All Trusted",
|
||||
txt_revoke_all_trusted_devices: "Revoke all device trust",
|
||||
txt_revoke_device_authorization: "Revoke device trust",
|
||||
txt_revoke_device_trust_failed: "Failed to revoke device trust",
|
||||
txt_revoke_all_device_trust_failed: "Failed to revoke all device trust",
|
||||
txt_revoke_trust: "Revoke Trust",
|
||||
txt_untrust: "Untrust",
|
||||
txt_update_device_note_failed: "Update device note failed",
|
||||
txt_role: "Role",
|
||||
txt_save: "Save",
|
||||
txt_save_profile: "Save Profile",
|
||||
txt_save_profile_failed: "Save profile failed",
|
||||
txt_search_sends: "Search sends...",
|
||||
txt_search_your_secure_vault: "Search your secure vault...",
|
||||
txt_clear_search: "Clear search",
|
||||
txt_clear_search_esc: "Clear search (Esc)",
|
||||
txt_sort: "Sort",
|
||||
|
||||
txt_sort_last_edited: "Modified",
|
||||
txt_sort_created: "Created",
|
||||
txt_sort_name: "A-Z",
|
||||
txt_secret_and_code_are_required: "Secret and code are required",
|
||||
txt_secret_copied: "Secret copied",
|
||||
txt_secure_note: "Secure Note",
|
||||
txt_security_code: "Security Code",
|
||||
txt_security_code_cvv: "Security Code (CVV)",
|
||||
txt_select_all: "Select All",
|
||||
txt_select_duplicate_items: "Select Duplicates",
|
||||
txt_select_an_item: "Select an item",
|
||||
txt_send_created: "Send created",
|
||||
txt_send_deleted: "Send deleted",
|
||||
txt_send_details: "Send Details",
|
||||
txt_send_file: "send-file",
|
||||
txt_send_unavailable: "Send unavailable.",
|
||||
txt_send_updated: "Send updated",
|
||||
txt_sign_out: "Sign Out",
|
||||
txt_ssh_key: "SSH Key",
|
||||
txt_ssn: "SSN",
|
||||
txt_state_province: "State / Province",
|
||||
txt_status: "Status",
|
||||
txt_online: "Online",
|
||||
txt_offline: "Offline",
|
||||
txt_submit: "Submit",
|
||||
txt_sync: "Sync",
|
||||
txt_sync_vault: "Sync Vault",
|
||||
txt_switch_to_dark_mode: "Switch to dark mode",
|
||||
txt_switch_to_light_mode: "Switch to light mode",
|
||||
txt_dash: "-",
|
||||
txt_text: "Text",
|
||||
txt_text_2fa_recovered: "2FA recovered",
|
||||
txt_text_2fa_recovered_new_recovery_code_code: "2FA recovered. New recovery code: {code}",
|
||||
txt_text_3: "------",
|
||||
txt_text_is_required: "Text is required",
|
||||
txt_text_send: "Text Send",
|
||||
txt_this_is_a_one_time_code_after_it_is_used_a_new_code_is_generated_automatically: "This is a one-time code. After it is used, a new code is generated automatically.",
|
||||
txt_this_item_requires_master_password_every_time_before_viewing_details: "This item requires master password every time before viewing details.",
|
||||
txt_this_link_is_missing_decryption_key: "This link is missing decryption key.",
|
||||
txt_this_send_is_password_protected: "This send is password protected.",
|
||||
txt_title: "Title",
|
||||
txt_totp: "TOTP",
|
||||
txt_totp_code: "TOTP Code",
|
||||
txt_totp_disabled: "TOTP disabled",
|
||||
txt_totp_enabled: "TOTP enabled",
|
||||
txt_totp_is_enabled_for_this_account: "TOTP is enabled for this account.",
|
||||
txt_total_items_count: "{count} items",
|
||||
txt_totp_secret: "TOTP Secret",
|
||||
txt_totp_verify_failed: "TOTP verify failed",
|
||||
txt_attachments: "Attachments",
|
||||
txt_upload_attachments: "Upload attachments",
|
||||
txt_new_attachments: "New attachments",
|
||||
txt_marked_for_removal_count: "{count} attachment(s) will be removed on save",
|
||||
txt_trash: "Trash",
|
||||
txt_trust_this_device_for_30_days: "Trust this device for 30 days",
|
||||
txt_trusted_until: "Trusted Until",
|
||||
txt_two_step_verification: "Two-step verification",
|
||||
txt_type: "Type",
|
||||
txt_type_type: "Type {type}",
|
||||
txt_unban: "Unban",
|
||||
txt_unchecked: "Unchecked",
|
||||
txt_unknown_device: "Unknown device",
|
||||
txt_unlock: "Unlock",
|
||||
txt_unlocking: "Unlocking...",
|
||||
txt_unlock_details: "Unlock Details",
|
||||
txt_unlock_failed: "Unlock failed",
|
||||
txt_unlock_failed_master_password_is_incorrect: "Unlock failed. Master password is incorrect.",
|
||||
txt_unlock_item: "Unlock Item",
|
||||
txt_unlock_send: "Unlock Send",
|
||||
txt_unlock_vault: "Unlock Vault",
|
||||
txt_unlocked: "Unlocked",
|
||||
txt_all_devices_removed: "All devices removed",
|
||||
txt_remove_device_failed: "Failed to remove device",
|
||||
txt_remove_all_devices_failed: "Failed to remove all devices",
|
||||
txt_update_item_failed: "Update item failed",
|
||||
txt_update_send_failed: "Update send failed",
|
||||
txt_use_recovery_code: "Use Recovery Code",
|
||||
txt_use_your_one_time_recovery_code_to_disable_two_step_verification: "Use your one-time recovery code to disable two-step verification.",
|
||||
txt_user_deleted: "User deleted",
|
||||
txt_user_status_updated: "User status updated",
|
||||
txt_username: "Username",
|
||||
txt_uri_match_default_base_domain: "Default (Base Domain)",
|
||||
txt_uri_match_base_domain: "Base Domain",
|
||||
txt_uri_match_host: "Host",
|
||||
txt_uri_match_exact: "Exact",
|
||||
txt_uri_match_never: "Never",
|
||||
txt_uri_match_starts_with: "Starts With",
|
||||
txt_uri_match_regular_expression: "Regular Expression",
|
||||
txt_users: "Users",
|
||||
txt_vault_synced: "Vault synced",
|
||||
txt_verification_code: "Verification Code",
|
||||
txt_verify: "Verify",
|
||||
txt_warning: "Warning",
|
||||
txt_view_recovery_code: "View Recovery Code",
|
||||
txt_web: "Web",
|
||||
txt_website: "Website",
|
||||
txt_websites: "Websites",
|
||||
txt_windows_desktop: "Windows Desktop",
|
||||
txt_yes: "Yes",
|
||||
|
||||
};
|
||||
|
||||
en.txt_auto_lock = 'Auto-lock';
|
||||
en.txt_auto_lock_description = 'Locks after inactivity. Closing and reopening the page always starts locked.';
|
||||
en.txt_auto_lock_updated = 'Auto-lock updated';
|
||||
en.txt_session_timeout = 'Session timeout';
|
||||
en.txt_session_timeout_updated = 'Session timeout updated';
|
||||
en.txt_timeout_time = 'Timeout time';
|
||||
en.txt_timeout_action = 'Timeout action';
|
||||
en.txt_timeout_action_logout = 'Log out';
|
||||
en.txt_timeout_action_lock = 'Lock';
|
||||
en.txt_in_planning = 'In planning';
|
||||
en.txt_security_preferences = 'Security Preferences';
|
||||
en.txt_timeout_1_minute = '1 minute';
|
||||
en.txt_timeout_5_minutes = '5 minutes';
|
||||
en.txt_timeout_15_minutes = '15 minutes';
|
||||
en.txt_timeout_30_minutes = '30 minutes';
|
||||
en.txt_timeout_never = 'Never';
|
||||
en.txt_lock_after_1_minute = 'After 1 minute';
|
||||
en.txt_lock_after_5_minutes = 'After 5 minutes';
|
||||
en.txt_lock_after_15_minutes = 'After 15 minutes';
|
||||
en.txt_lock_after_30_minutes = 'After 30 minutes';
|
||||
en.txt_lock_after_never = 'Never for inactivity';
|
||||
en.txt_import = 'Import';
|
||||
en.txt_export = 'Export';
|
||||
en.txt_format = 'Format';
|
||||
en.txt_source_file = 'Source file';
|
||||
en.txt_folder_handling = 'Folder handling';
|
||||
en.txt_import_folder_mode_original = 'Original path from import file';
|
||||
en.txt_import_folder_mode_none = 'No folder';
|
||||
en.txt_import_folder_mode_target = 'One selected folder';
|
||||
en.txt_target_folder = 'Target folder';
|
||||
en.txt_select_folder_placeholder = '-- Select folder --';
|
||||
en.txt_import_vault_data_hint = 'Import vault data into your current account.';
|
||||
en.txt_export_vault_data_hint = 'Export vault data from your current account.';
|
||||
en.txt_import_export_title = 'Import & Export';
|
||||
en.txt_encrypted_mode = 'Encrypted mode';
|
||||
en.txt_account_verification = 'Account verification';
|
||||
en.txt_password_verification = 'Password verification';
|
||||
en.txt_file_password = 'File password';
|
||||
en.txt_zip_password_optional = 'ZIP password (optional)';
|
||||
en.txt_zip_password = 'ZIP password';
|
||||
en.txt_close = 'Close';
|
||||
en.txt_total = 'Total';
|
||||
en.txt_import_success = 'Import successful';
|
||||
en.txt_import_success_number_of_items = 'Imported {count} item(s) in total.';
|
||||
en.txt_import_attachment_summary = 'Imported {imported} of {total} attachment(s).';
|
||||
en.txt_import_failed_attachments_title = '{count} attachment(s) were not imported:';
|
||||
en.txt_import_attachment_target_not_found = 'Matching imported item not found.';
|
||||
en.txt_upload_attachment_failed = 'Attachment upload failed.';
|
||||
en.txt_import_file_password_required = 'Please enter file password.';
|
||||
en.txt_import_invalid_zip_password = 'Invalid ZIP password.';
|
||||
en.txt_export_completed = 'Export completed';
|
||||
en.txt_export_failed = 'Export failed';
|
||||
en.txt_import_invalid_password_protected_file = 'Invalid password-protected export file.';
|
||||
en.txt_import_decrypt_failed = 'Failed to decrypt import file.';
|
||||
en.txt_import_empty_zip_archive = 'Empty zip archive.';
|
||||
en.txt_import_no_json_found_in_zip = 'No importable JSON data found in zip archive.';
|
||||
en.txt_import_data_json_not_found = 'data.json not found in zip archive.';
|
||||
en.txt_import_zip_password_required = 'ZIP password is required.';
|
||||
en.txt_import_invalid_json_file = 'Invalid JSON file';
|
||||
en.txt_import_failed = 'Import failed';
|
||||
en.txt_import_encrypted_file_title = 'Import encrypted file';
|
||||
en.txt_import_encrypted_file_message = 'This Bitwarden export is password-protected. Enter the export file password to continue.';
|
||||
en.txt_import_encrypted_zip_title = 'Import encrypted ZIP';
|
||||
en.txt_import_encrypted_zip_message = 'This ZIP archive is password-protected. Enter the ZIP password to continue.';
|
||||
en.txt_new_type_header = 'New {type}';
|
||||
en.txt_edit_type_header = 'Edit {type}';
|
||||
en.txt_delete_folder = 'Delete Folder';
|
||||
en.txt_delete_folder_message = 'Delete folder "{name}"? Items inside will move to No Folder.';
|
||||
en.txt_delete_all_folders = 'Delete All Folders';
|
||||
en.txt_delete_all_folders_message = 'Delete all folders? Items inside will move to No Folder.';
|
||||
en.txt_folder_not_found = 'Folder not found';
|
||||
en.txt_folder_deleted = 'Folder deleted';
|
||||
en.txt_folder_updated = 'Folder updated';
|
||||
en.txt_folders_deleted = 'Folders deleted';
|
||||
en.txt_update_folder_failed = 'Update folder failed';
|
||||
en.txt_delete_folder_failed = 'Delete folder failed';
|
||||
en.txt_delete_all_folders_failed = 'Delete all folders failed';
|
||||
en.txt_other = 'Other';
|
||||
en.txt_vault_key_unavailable = 'Vault key unavailable. Please unlock vault and try again.';
|
||||
en.txt_vault_not_ready = 'Vault is not ready yet';
|
||||
en.txt_unsupported_export_format = 'Unsupported export format';
|
||||
en.txt_invalid_encrypted_export = 'Invalid encrypted export file.';
|
||||
en.txt_export_belongs_to_another_account = 'This encrypted export belongs to another account.';
|
||||
en.txt_invalid_argon2id_params = 'Invalid Argon2id parameters in export file.';
|
||||
en.txt_unsupported_kdf_type = 'Unsupported kdfType: {type}';
|
||||
en.txt_invalid_file_password = 'Invalid file password.';
|
||||
en.txt_failed_to_map_attachments = 'Failed to map {count} attachment(s) to imported items.';
|
||||
|
||||
export default en;
|
||||
@@ -0,0 +1,845 @@
|
||||
// Generated from English defaults plus Chinese translations so zh-CN can load without the English chunk.
|
||||
const zhCN: Record<string, string> = {
|
||||
"nav_account_settings": "账户设置",
|
||||
"nav_admin_panel": "用户管理",
|
||||
"nav_device_management": "设备管理",
|
||||
"nav_my_vault": "我的密码库",
|
||||
"nav_sends": "Send",
|
||||
"nav_backup_strategy": "云端备份",
|
||||
"nav_import_export": "导入导出",
|
||||
"backup_strategy_title": "云端备份",
|
||||
"backup_strategy_under_construction": "正在搭建中",
|
||||
"import_export_title": "导入导出",
|
||||
"import_export_under_construction": "正在搭建中",
|
||||
"txt_backup_export": "导出备份",
|
||||
"txt_backup_import": "还原",
|
||||
"txt_backup_include_attachments": "包含附件",
|
||||
"txt_backup_export_description": "下载一个完整的实例备份 ZIP,手动保管即可。",
|
||||
"txt_backup_import_description": "上传之前导出的备份 ZIP,并还原到当前实例。",
|
||||
"txt_backup_exporting": "正在导出...",
|
||||
"txt_backup_importing": "正在还原...",
|
||||
"txt_backup_restoring": "正在还原...",
|
||||
"txt_backup_export_success": "备份已导出",
|
||||
"txt_backup_import_success_relogin": "备份已还原,请重新登录",
|
||||
"txt_backup_restore_success_relogin": "备份已还原,请重新登录",
|
||||
"txt_backup_restore_completed_verified": "备份文件完整性校验已通过。",
|
||||
"txt_backup_restore_completed_without_checksum": "备份已还原,但文件名中未提供可校验的完整性标记。",
|
||||
"txt_backup_remote_restore_completed_verified": "远程备份完整性校验已通过。",
|
||||
"txt_backup_remote_restore_completed_without_checksum": "远程备份已还原,但文件名中未提供可校验的完整性标记。",
|
||||
"txt_backup_restore_skipped_summary": "{reason},已跳过 {attachments} 个附件",
|
||||
"txt_backup_restore_skipped_reason_default": "部分文件无法还原",
|
||||
"txt_backup_export_failed": "备份导出失败",
|
||||
"txt_backup_import_failed": "备份还原失败",
|
||||
"txt_backup_restore_failed": "备份还原失败",
|
||||
"txt_backup_integrity_check_failed": "备份完整性校验失败",
|
||||
"txt_backup_center_title": "实例备份",
|
||||
"txt_backup_center_description": "把本地导出和远程自动备份放在一起管理,既方便手动恢复,也能每天自动留一份。",
|
||||
"txt_backup_restore_note": "还原会覆盖当前实例;如果当前已有数据,系统会要求你确认“清空后还原”。",
|
||||
"txt_backup_manual": "手动备份",
|
||||
"txt_backup_manual_description": "现在就导出 ZIP,或者把之前导出的 ZIP 恢复到当前实例。",
|
||||
"txt_backup_destinations_title": "备份地点",
|
||||
"txt_backup_destinations_description": "把多个 WebDAV、E3 地点统一放在这里。左侧选一个,右侧编辑和浏览它。",
|
||||
"txt_backup_recommend_title": "推荐储存库",
|
||||
"txt_backup_recommend_open_signup": "前往注册",
|
||||
"txt_backup_recommend_open_signup_aff": "前往注册(含 AFF)",
|
||||
"txt_backup_recommend_open_guide": "查看教程",
|
||||
"txt_backup_recommend_empty": "暂时没有推荐",
|
||||
"txt_backup_recommend_referral_label": "推荐码",
|
||||
"txt_backup_recommend_referral_note": "注册时填写可额外获得 5 GB,作者会收到 2 GB。",
|
||||
"txt_backup_recommend_infinicloud_summary": "只需邮箱即可注册。免费 20 GB;填写推荐码后总计 25 GB。",
|
||||
"txt_backup_recommend_infinicloud_step_1": "先用邮箱注册一个 InfiniCLOUD 账号。",
|
||||
"txt_backup_recommend_infinicloud_step_2_prefix": "进入",
|
||||
"txt_backup_recommend_infinicloud_step_2_suffix": ",然后开启 Turn on Apps Connection。",
|
||||
"txt_backup_recommend_infinicloud_step_3": "Connection ID 用作 WebDAV 用户名,Apps Password 用作 WebDAV 密码。",
|
||||
"txt_backup_recommend_infinicloud_step_4": "在 My Page 最下面的 Referral Bonus 填入推荐码 2HC5E,可额外获得 5 GB。",
|
||||
"txt_backup_recommend_open_password": "密码设置",
|
||||
"txt_backup_recommend_open_storage": "打开储存连接",
|
||||
"txt_backup_recommend_koofr_summary": "只需邮箱即可注册使用。免费 10 GB,并且可以通过 WebDAV 接到 Google Drive、OneDrive、Dropbox。",
|
||||
"txt_backup_recommend_koofr_password_link": "密码设置",
|
||||
"txt_backup_recommend_koofr_storage_link": "Storage",
|
||||
"txt_backup_recommend_koofr_step_1": "先用邮箱注册一个 Koofr 账号。",
|
||||
"txt_backup_recommend_koofr_step_2_prefix": "打开",
|
||||
"txt_backup_recommend_koofr_step_2_suffix": ",生成新的应用密码。注册邮箱用作 WebDAV 用户名,应用密码用作 WebDAV 密码。",
|
||||
"txt_backup_recommend_koofr_step_3": "Koofr 自己的 WebDAV 地址是 https://app.koofr.net/dav/Koofr。",
|
||||
"txt_backup_recommend_koofr_step_4": "Koofr 最方便的地方,是还能接 Google Drive、OneDrive、Dropbox 这三大云盘;免费用户最多能连接两个。",
|
||||
"txt_backup_recommend_koofr_step_5_prefix": "打开",
|
||||
"txt_backup_recommend_koofr_step_5_suffix": ",在左侧栏点击“连接”,选择你要连接的储存即可。",
|
||||
"txt_backup_recommend_koofr_dav_intro": "连接好储存后,账号和应用密码都不变,只需要切换 WebDAV 地址:",
|
||||
"txt_backup_recommend_koofr_dav_self": "Koofr",
|
||||
"txt_backup_recommend_pcloud_summary": "只需邮箱即可注册。免费最高 10 GB,并且自带标准 WebDAV 访问。",
|
||||
"txt_backup_recommend_pcloud_step_1": "先用邮箱注册一个 pCloud 账号。",
|
||||
"txt_backup_recommend_pcloud_step_2": "WebDAV 地址填写 https://webdav.pcloud.com/ 。",
|
||||
"txt_backup_recommend_pcloud_step_3": "注册邮箱用作 WebDAV 用户名,注册密码用作 WebDAV 密码。",
|
||||
"txt_backup_add_destination": "新增地点",
|
||||
"txt_backup_schedule_panel_title": "自动备份计划",
|
||||
"txt_backup_schedule_panel_note": "每个备份地点都可以单独配置自己的每日自动备份计划。",
|
||||
"txt_backup_scheduled_target": "当前计划目标",
|
||||
"txt_backup_destination_active_badge": "已启用计划",
|
||||
"txt_backup_destination_idle_badge": "未启用计划",
|
||||
"txt_backup_destination_last_success": "上次成功:{time}",
|
||||
"txt_backup_destination_never_run": "还没有成功执行过",
|
||||
"txt_backup_destination_detail_title": "地点详情",
|
||||
"txt_backup_destination_detail_note": "",
|
||||
"txt_backup_destination_name": "地点名称",
|
||||
"txt_backup_set_scheduled_target": "设为每日备份目标",
|
||||
"txt_backup_delete_destination": "删除",
|
||||
"txt_backup_destination_deleted": "备份地点已删除",
|
||||
"txt_backup_delete_destination_confirm_message": "删除备份地点“{name}”?此操作不可撤销。",
|
||||
"txt_backup_select_destination": "请先从左侧列表选择一个备份地点",
|
||||
"txt_backup_remote_save_first": "请先保存这个备份地点,再浏览它的远端备份文件",
|
||||
"txt_backup_automation": "自动备份",
|
||||
"txt_backup_automation_description": "选择备份地点,保存连接信息后,系统会按设定时间每天自动上传一份备份。",
|
||||
"txt_backup_settings_saved": "备份设置已保存",
|
||||
"txt_backup_settings_save_failed": "备份设置保存失败",
|
||||
"txt_backup_settings_load_failed": "备份设置加载失败",
|
||||
"txt_backup_save_settings": "保存设置",
|
||||
"txt_backup_saving": "正在保存...",
|
||||
"txt_backup_enable_action": "启用",
|
||||
"txt_backup_disable_action": "停用",
|
||||
"txt_backup_run_now": "立即执行远程备份",
|
||||
"txt_backup_run_manual": "手动执行",
|
||||
"txt_backup_running_now": "执行中...",
|
||||
"txt_backup_remote_run_success": "远程备份已完成",
|
||||
"txt_backup_remote_run_success_verified": "远程备份已完成,且完整性校验已通过。",
|
||||
"txt_backup_remote_run_failed": "远程备份失败",
|
||||
"txt_backup_remote_title": "远端备份",
|
||||
"txt_backup_remote_note": "浏览已保存的备份地点,选择某个备份 ZIP 后可以下载,也可以直接还原。",
|
||||
"txt_backup_remote_saved_basis": "远端浏览使用的是“已保存”的备份地点配置,不会读取你当前未保存的表单内容。",
|
||||
"txt_backup_remote_refresh": "刷新",
|
||||
"txt_backup_remote_root": "根目录",
|
||||
"txt_backup_remote_up": "上一级",
|
||||
"txt_backup_remote_open": "打开",
|
||||
"txt_backup_remote_download": "下载",
|
||||
"txt_backup_remote_downloading": "下载中...",
|
||||
"txt_backup_remote_restore": "还原",
|
||||
"txt_backup_remote_restore_stage_prepare": "正在读取远端备份并检查可恢复内容...",
|
||||
"txt_backup_remote_restore_stage_replace": "正在清空当前数据并还原远端备份,请稍候...",
|
||||
"txt_backup_progress_kicker": "备份任务",
|
||||
"txt_backup_progress_subject": "当前对象:{name}",
|
||||
"txt_backup_restore_progress_kicker": "还原进度",
|
||||
"txt_backup_restore_progress_local_title": "正在还原本地备份",
|
||||
"txt_backup_restore_progress_remote_title": "正在还原远端备份",
|
||||
"txt_backup_export_progress_title": "正在导出备份",
|
||||
"txt_backup_remote_run_progress_title": "正在执行远程备份",
|
||||
"txt_backup_restore_progress_file": "当前文件:{name}",
|
||||
"txt_backup_restore_progress_elapsed": "已耗时 {seconds} 秒",
|
||||
"txt_backup_archive_progress_collect_title": "正在收集密码库数据",
|
||||
"txt_backup_archive_progress_collect_detail": "服务器正在读取数据库表,并整理备份所需的数据内容。",
|
||||
"txt_backup_archive_progress_collect_with_attachments_detail": "服务器正在读取数据库表,并整理附件元数据与备份内容。",
|
||||
"txt_backup_archive_progress_package_title": "正在打包备份压缩包",
|
||||
"txt_backup_archive_progress_package_detail": "服务器正在生成备份 ZIP,并计算文件名校验前缀。",
|
||||
"txt_backup_archive_progress_package_with_attachments_detail": "服务器正在生成带附件信息的备份 ZIP 元数据,并计算文件名校验前缀。",
|
||||
"txt_backup_archive_progress_ready_title": "正在准备下载",
|
||||
"txt_backup_archive_progress_ready_detail": "备份压缩包已经生成,服务器正在把它返回给浏览器。",
|
||||
"txt_backup_export_progress_fetch_attachments_title": "正在下载附件文件",
|
||||
"txt_backup_export_progress_fetch_attachments_detail": "浏览器正在读取附件对象,并把它们补入导出备份包。",
|
||||
"txt_backup_export_progress_rebuild_title": "正在重建导出压缩包",
|
||||
"txt_backup_export_progress_rebuild_detail": "浏览器正在重建最终 ZIP,并刷新文件名里的校验后缀。",
|
||||
"txt_backup_export_progress_save_title": "正在保存导出文件",
|
||||
"txt_backup_export_progress_save_detail": "浏览器正在准备最终的备份文件下载。",
|
||||
"txt_backup_export_progress_complete_title": "备份导出已完成",
|
||||
"txt_backup_export_progress_complete_detail": "导出备份已经准备完成。",
|
||||
"txt_backup_export_progress_failed_title": "备份导出失败",
|
||||
"txt_backup_export_progress_failed_detail": "导出备份未能完成。",
|
||||
"txt_backup_remote_run_progress_prepare_title": "正在准备远程备份",
|
||||
"txt_backup_remote_run_progress_prepare_detail": "服务器正在读取当前备份目标,并准备执行这次远程备份。",
|
||||
"txt_backup_remote_run_progress_sync_attachments_title": "正在检查附件索引",
|
||||
"txt_backup_remote_run_progress_sync_attachments_detail": "服务器正在比对附件索引,只会上传缺失或不一致的附件对象。",
|
||||
"txt_backup_remote_run_progress_sync_attachments_skipped_detail": "当前备份未包含附件,因此跳过附件同步。",
|
||||
"txt_backup_remote_run_progress_upload_title": "正在上传备份压缩包",
|
||||
"txt_backup_remote_run_progress_upload_detail": "服务器正在把备份 ZIP 上传到远程备份目标。",
|
||||
"txt_backup_remote_run_progress_verify_title": "正在校验已上传压缩包",
|
||||
"txt_backup_remote_run_progress_verify_detail": "服务器正在回读刚上传的 ZIP,并校验它的哈希和大小。",
|
||||
"txt_backup_remote_run_progress_cleanup_title": "正在清理旧备份",
|
||||
"txt_backup_remote_run_progress_cleanup_detail": "服务器正在按保留策略清理旧备份文件。",
|
||||
"txt_backup_remote_run_progress_complete_title": "远程备份已完成",
|
||||
"txt_backup_remote_run_progress_complete_detail": "远程备份已上传完成,并通过完整性校验。",
|
||||
"txt_backup_remote_run_progress_failed_title": "远程备份失败",
|
||||
"txt_backup_remote_run_progress_failed_detail": "远程备份未能完成。",
|
||||
"txt_backup_restore_progress_local_upload_title": "正在上传备份包",
|
||||
"txt_backup_restore_progress_local_upload_detail": "已选 ZIP 正在发送到服务器,服务器收到后会开始执行还原。",
|
||||
"txt_backup_restore_progress_local_shadow_title": "正在创建影子恢复区",
|
||||
"txt_backup_restore_progress_local_shadow_detail": "服务器正在准备独立的影子数据区,只有校验通过后才会替换正式数据。",
|
||||
"txt_backup_restore_progress_local_data_title": "正在写入密码库数据",
|
||||
"txt_backup_restore_progress_local_data_detail": "服务器正在把用户、文件夹、密码条目和相关元数据写入影子表。",
|
||||
"txt_backup_restore_progress_local_files_title": "正在恢复附件文件",
|
||||
"txt_backup_restore_progress_local_files_detail": "服务器正在把附件对象写回存储,并剔除无法恢复的附件记录。",
|
||||
"txt_backup_restore_progress_local_finalize_title": "正在校验并完成切换",
|
||||
"txt_backup_restore_progress_local_finalize_detail": "服务器正在执行最终校验,校验通过后会把已验证的数据切换为正式数据。",
|
||||
"txt_backup_restore_progress_remote_fetch_title": "正在读取远端备份包",
|
||||
"txt_backup_restore_progress_remote_fetch_detail": "服务器正在从远端备份目标下载你选中的备份包。",
|
||||
"txt_backup_restore_progress_remote_shadow_title": "正在创建影子恢复区",
|
||||
"txt_backup_restore_progress_remote_shadow_detail": "服务器正在准备独立的影子数据区,只有校验通过后才会替换正式数据。",
|
||||
"txt_backup_restore_progress_remote_data_title": "正在写入密码库数据",
|
||||
"txt_backup_restore_progress_remote_data_detail": "服务器正在把用户、文件夹、密码条目和相关元数据写入影子表。",
|
||||
"txt_backup_restore_progress_remote_files_title": "正在恢复远端附件",
|
||||
"txt_backup_restore_progress_remote_files_detail": "服务器正在从远端存储读取所需附件,并写回到当前实例的附件存储。",
|
||||
"txt_backup_restore_progress_remote_finalize_title": "正在校验并完成切换",
|
||||
"txt_backup_restore_progress_remote_finalize_detail": "服务器正在执行最终校验,校验通过后会把已验证的数据切换为正式数据。",
|
||||
"txt_backup_remote_loading": "正在读取远端备份...",
|
||||
"txt_backup_remote_cached_empty": "点击“刷新”后读取",
|
||||
"txt_backup_remote_empty": "这个目录下还没有备份文件",
|
||||
"txt_backup_remote_folder": "文件夹",
|
||||
"txt_backup_remote_unknown_time": "未知时间",
|
||||
"txt_backup_remote_current_path": "当前目录",
|
||||
"txt_backup_remote_load_failed": "读取远端备份失败",
|
||||
"txt_backup_remote_invalid_response": "远端备份响应无效",
|
||||
"txt_backup_remote_download_failed": "下载远端备份失败",
|
||||
"txt_backup_remote_delete_success": "远端备份已删除",
|
||||
"txt_backup_remote_delete_failed": "删除远端备份失败",
|
||||
"txt_backup_remote_delete_confirm_message": "删除备份文件“{name}”?此操作不可撤销。",
|
||||
"txt_backup_remote_deleting": "删除中...",
|
||||
"txt_backup_remote_restore_failed": "还原远端备份失败",
|
||||
"txt_backup_restore_checksum_warning_title": "备份完整性警告",
|
||||
"txt_backup_restore_checksum_warning_message": "所选备份文件“{name}”未通过文件名完整性校验。期望前缀为 {expected},实际计算结果为 {actual}。该文件可能不完整或已经损坏。继续还原可能会导入受损数据。",
|
||||
"txt_backup_remote_restore_checksum_warning_message": "远程备份文件“{name}”未通过文件名完整性校验。期望前缀为 {expected},实际计算结果为 {actual}。该文件可能在上传或存储过程中损坏。继续还原可能会导入受损数据,并可能造成严重后果。",
|
||||
"txt_backup_restore_checksum_warning_message_fallback": "所选备份文件未通过完整性校验。继续还原可能会导入受损数据。",
|
||||
"txt_backup_restore_checksum_warning_confirm": "继续还原",
|
||||
"txt_backup_remote_restore_invalid_response": "远端备份还原响应无效",
|
||||
"txt_backup_remote_run_invalid_response": "远端备份执行响应无效",
|
||||
"txt_backup_settings_invalid_response": "备份设置响应无效",
|
||||
"txt_backup_import_invalid_response": "备份还原响应无效",
|
||||
"txt_backup_destination": "备份地点",
|
||||
"txt_backup_protocol_webdav": "WebDAV",
|
||||
"txt_backup_protocol_e3": "E3",
|
||||
"txt_backup_recommend_group_webdav": "WebDAV",
|
||||
"txt_backup_recommend_group_s3": "S3",
|
||||
"txt_backup_destination_name_default_webdav": "WebDAV {index}",
|
||||
"txt_backup_destination_name_default_e3": "E3 {index}",
|
||||
"txt_backup_type": "备份类型",
|
||||
"txt_backup_destination_reserved": "预留位置",
|
||||
"txt_backup_time": "备份时间",
|
||||
"txt_backup_start_time": "开始时间",
|
||||
"txt_backup_timezone": "时区",
|
||||
"txt_backup_interval_hours": "每隔",
|
||||
"txt_backup_interval_hours_suffix": "小时",
|
||||
"txt_backup_interval_hours_presets": "快捷时间预设",
|
||||
"txt_backup_frequency": "备份频率",
|
||||
"txt_backup_frequency_daily": "每天",
|
||||
"txt_backup_frequency_weekly": "每周",
|
||||
"txt_backup_frequency_monthly": "每月",
|
||||
"txt_backup_day_of_week": "星期",
|
||||
"txt_backup_day_of_month": "日期",
|
||||
"txt_backup_weekday_monday": "周一",
|
||||
"txt_backup_weekday_tuesday": "周二",
|
||||
"txt_backup_weekday_wednesday": "周三",
|
||||
"txt_backup_weekday_thursday": "周四",
|
||||
"txt_backup_weekday_friday": "周五",
|
||||
"txt_backup_weekday_saturday": "周六",
|
||||
"txt_backup_weekday_sunday": "周日",
|
||||
"txt_backup_retention_count": "只保留",
|
||||
"txt_backup_retention_count_suffix": "个",
|
||||
"txt_backup_retention_count_hint": "留空表示不限,新建备份地点默认保留 30 个",
|
||||
"txt_backup_destination_include_attachments": "包含附件",
|
||||
"txt_backup_include_attachments_help_button": "附件备份说明",
|
||||
"txt_backup_include_attachments_help": "附件会以增量方式保存在远端的 attachments 文件夹中,后续备份通常只上传新增文件。你在本地删除附件时,已经备份到远端的旧文件不会自动删除。恢复时会按需从 attachments 文件夹读取对应附件,找不到的附件会自动跳过。",
|
||||
"txt_backup_enable_schedule": "启用每日自动备份",
|
||||
"txt_backup_schedule_note": "Worker 每 5 分钟检查一次计划。会先按你选择的时区和开始时间起跑,再按小时间隔继续执行;到了下一天,会重新从开始时间开始。",
|
||||
"txt_backup_schedule_disabled": "未启用",
|
||||
"txt_backup_schedule_status": "计划状态",
|
||||
"txt_backup_schedule_summary": "从 {time} 开始,每隔 {interval} 小时({timezone})",
|
||||
"txt_backup_schedule_empty": "还没有启用任何自动备份计划",
|
||||
"txt_backup_last_success": "上次成功时间",
|
||||
"txt_backup_last_target": "上次备份位置",
|
||||
"txt_backup_last_file": "上次备份文件",
|
||||
"txt_backup_last_error_prefix": "上次错误",
|
||||
"txt_backup_none_yet": "还没有成功完成过远程备份",
|
||||
"txt_backup_not_configured": "尚未配置",
|
||||
"txt_backup_never": "从未",
|
||||
"txt_backup_unknown_size": "大小未知",
|
||||
"txt_backup_webdav_url": "WebDAV 服务地址",
|
||||
"txt_backup_webdav_username": "WebDAV 用户名",
|
||||
"txt_backup_webdav_password": "WebDAV 密码",
|
||||
"txt_backup_webdav_path": "远程目录",
|
||||
"txt_backup_e3_endpoint": "E3 Endpoint",
|
||||
"txt_backup_e3_bucket": "Bucket",
|
||||
"txt_backup_e3_region": "Region",
|
||||
"txt_backup_e3_access_key": "Access Key",
|
||||
"txt_backup_e3_secret_key": "Secret Key",
|
||||
"txt_backup_e3_path": "远程路径",
|
||||
"txt_backup_reserved_name": "预留类型名称",
|
||||
"txt_backup_reserved_notes": "预留备注",
|
||||
"txt_backup_reserved_notes_placeholder": "给下一个备份地点先留个说明",
|
||||
"txt_backup_reserved_hint": "这个位置先预留给后续备份地点。你现在可以先保存备注,但自动上传不会启用。",
|
||||
"txt_backup_file": "备份文件",
|
||||
"txt_backup_file_required": "请选择备份文件",
|
||||
"txt_backup_no_file_selected": "尚未选择备份文件",
|
||||
"txt_backup_selected_file_name": "已选择文件:{name}",
|
||||
"txt_backup_replace_confirm_title": "替换当前实例数据",
|
||||
"txt_backup_replace_confirm_message": "当前实例里已经有数据。确认后,系统会先完成校验与恢复准备,只有在恢复成功后才会用所选备份替换当前实例数据。是否继续?",
|
||||
"txt_backup_clear_and_import": "替换并导入",
|
||||
"txt_backup_clear_and_restore": "替换并还原",
|
||||
"txt_access_count": "访问次数",
|
||||
"txt_accessed_count_times": "已访问 {count} 次",
|
||||
"txt_actions": "操作",
|
||||
"txt_add": "新增",
|
||||
"txt_add_field": "添加字段",
|
||||
"txt_add_website": "添加网站",
|
||||
"txt_added": "已添加",
|
||||
"txt_additional_options": "附加选项",
|
||||
"txt_address": "地址",
|
||||
"txt_address_1": "地址 1",
|
||||
"txt_address_2": "地址 2",
|
||||
"txt_address_3": "地址 3",
|
||||
"txt_all_device_authorizations_revoked": "已撤销所有设备信任",
|
||||
"txt_all_invites_deleted": "已删除所有邀请码",
|
||||
"txt_all_items": "所有项目",
|
||||
"txt_all_sends": "所有发送",
|
||||
"txt_android": "安卓",
|
||||
"txt_are_you_sure_you_want_to_delete_count_selected_items": "确认删除所选的 {count} 个项目?",
|
||||
"txt_are_you_sure_you_want_to_delete_count_selected_items_permanently": "确认永久删除所选的 {count} 个项目?",
|
||||
"txt_are_you_sure_you_want_to_delete_this_item": "确认删除此项目?",
|
||||
"txt_are_you_sure_you_want_to_delete_this_passkey": "确认删除这个通行密钥?",
|
||||
"txt_are_you_sure_you_want_to_log_out": "确认要退出登录吗?",
|
||||
"txt_authenticator_key": "验证器密钥",
|
||||
"txt_authorized_devices": "已授权设备",
|
||||
"txt_auto_copy_link_after_save": "保存后自动复制链接",
|
||||
"txt_autofill_options": "自动填充选项",
|
||||
"txt_back_to_login": "返回登录",
|
||||
"txt_ban": "封禁",
|
||||
"txt_boolean": "布尔",
|
||||
"txt_brand": "品牌",
|
||||
"txt_bulk_delete_failed": "批量删除失败",
|
||||
"txt_bulk_permanent_delete_failed": "批量永久删除失败",
|
||||
"txt_bulk_restore_failed": "批量恢复失败",
|
||||
"txt_bulk_delete_sends_failed": "批量删除发送失败",
|
||||
"txt_bulk_move_failed": "批量移动失败",
|
||||
"txt_cancel": "取消",
|
||||
"txt_continue": "继续",
|
||||
"txt_card": "银行卡",
|
||||
"txt_card_details": "银行卡详情",
|
||||
"txt_cardholder_name": "持卡人姓名",
|
||||
"txt_change_master_password": "修改主密码",
|
||||
"txt_change_password": "修改密码",
|
||||
"txt_change_password_failed": "修改密码失败",
|
||||
"txt_change_password_confirm_and_sign_out_all_devices": "修改主密码后会强制退出所有设备,包括当前网页端。确认继续吗",
|
||||
"txt_copy_failed": "复制失败",
|
||||
"txt_checked": "已勾选",
|
||||
"txt_choose_destination_folder": "选择目标文件夹。",
|
||||
"txt_chrome_browser": "Chrome 浏览器",
|
||||
"txt_chrome_extension": "Chrome 扩展",
|
||||
"txt_city_town": "城市 / 城镇",
|
||||
"txt_code": "代码",
|
||||
"txt_company": "公司",
|
||||
"txt_configure_custom_field_values": "配置自定义字段值。",
|
||||
"txt_confirm": "确认",
|
||||
"txt_confirm_master_password": "确认主密码",
|
||||
"txt_confirm_password": "确认密码",
|
||||
"txt_copy": "复制",
|
||||
"txt_code_copied": "验证码已复制",
|
||||
"txt_copy_code": "复制代码",
|
||||
"txt_copy_link": "复制链接",
|
||||
"txt_copy_secret": "复制密钥",
|
||||
"txt_country": "国家",
|
||||
"txt_create": "创建",
|
||||
"txt_create_account": "创建账户",
|
||||
"txt_registering": "正在注册...",
|
||||
"txt_create_folder": "创建文件夹",
|
||||
"txt_create_folder_failed": "创建文件夹失败",
|
||||
"txt_create_item_failed": "创建项目失败",
|
||||
"txt_create_send_failed": "创建发送失败",
|
||||
"txt_create_timed_invite": "创建时效邀请码",
|
||||
"txt_created_value": "创建于:{value}",
|
||||
"txt_current_new_password_is_required": "需要输入当前密码和新密码",
|
||||
"txt_current_password": "当前密码",
|
||||
"txt_custom_fields": "自定义字段",
|
||||
"txt_decrypt_failed": "(解密失败)",
|
||||
"txt_decrypt_failed_2": "解密失败",
|
||||
"txt_delete": "删除",
|
||||
"txt_delete_all": "全部删除",
|
||||
"txt_delete_all_invite_codes_active_inactive": "删除所有邀请码(有效/无效)?",
|
||||
"txt_delete_all_invites": "删除所有邀请码",
|
||||
"txt_delete_item": "删除项目",
|
||||
"txt_delete_passkey": "删除通行密钥",
|
||||
"txt_delete_item_failed": "删除项目失败",
|
||||
"txt_delete_permanently": "永久删除",
|
||||
"txt_archive": "归档",
|
||||
"txt_archive_item": "归档项目",
|
||||
"txt_archive_item_message": "归档后,此项目将被排除在一般搜索结果和自动填充建议之外。",
|
||||
"txt_archive_selected_items": "归档项目",
|
||||
"txt_archive_selected_items_message": "归档后,所选的 {count} 个项目将被排除在一般搜索结果和自动填充建议之外。",
|
||||
"txt_archived": "已归档",
|
||||
"txt_archive_selected": "归档",
|
||||
"txt_item_archived": "项目已归档",
|
||||
"txt_item_unarchived": "项目已取消归档",
|
||||
"txt_archived_selected_items": "已归档所选项目",
|
||||
"txt_unarchived_selected_items": "已取消归档所选项目",
|
||||
"txt_archive_item_failed": "归档项目失败",
|
||||
"txt_unarchive_item_failed": "取消归档项目失败",
|
||||
"txt_bulk_archive_failed": "批量归档失败",
|
||||
"txt_bulk_unarchive_failed": "批量取消归档失败",
|
||||
"txt_unarchive": "取消归档",
|
||||
"txt_delete_selected": "删除",
|
||||
"txt_delete_selected_items": "删除所选项目",
|
||||
"txt_delete_selected_items_permanently": "Delete Selected Items Permanently",
|
||||
"txt_delete_send_failed": "删除发送失败",
|
||||
"txt_delete_this_user_and_all_user_data": "删除此用户及其所有数据?",
|
||||
"txt_delete_user": "删除用户",
|
||||
"txt_deleted_selected_items": "已删除所选项目",
|
||||
"txt_deleted_selected_items_permanently": "已永久删除所选项目",
|
||||
"txt_restored_selected_items": "已恢复所选项目",
|
||||
"txt_deleted_selected_sends": "已删除所选发送",
|
||||
"txt_deletion_date": "删除日期",
|
||||
"txt_deletion_days": "删除天数",
|
||||
"txt_device": "设备",
|
||||
"txt_device_authorization_revoked": "设备信任已撤销",
|
||||
"txt_device_management": "设备管理",
|
||||
"txt_device_note": "备注",
|
||||
"txt_device_note_required": "设备名称不能为空",
|
||||
"txt_device_note_updated": "设备名称已更新",
|
||||
"txt_device_removed": "设备已移除",
|
||||
"txt_load_devices_failed": "加载设备失败",
|
||||
"txt_disable_this_send": "禁用此发送",
|
||||
"txt_disable_totp": "停用 TOTP",
|
||||
"txt_disable_totp_failed": "禁用 TOTP 失败",
|
||||
"txt_download": "下载",
|
||||
"txt_downloading": "下载中...",
|
||||
"txt_downloading_percent": "下载中 {percent}%",
|
||||
"txt_attachment": "附件",
|
||||
"txt_uploading_attachment_named": "正在上传 {name}...",
|
||||
"txt_uploading_attachment_named_percent": "正在上传 {name} {percent}%",
|
||||
"txt_uploading_file_named": "正在上传 {name}...",
|
||||
"txt_uploading_file_named_percent": "正在上传 {name} {percent}%",
|
||||
"txt_download_failed": "下载失败",
|
||||
"txt_edge_browser": "Edge 浏览器",
|
||||
"txt_edge_extension": "Edge 扩展",
|
||||
"txt_edit": "编辑",
|
||||
"txt_edit_send": "编辑发送",
|
||||
"txt_email": "邮箱",
|
||||
"txt_email_password_and_recovery_code_are_required": "需要输入邮箱、密码和恢复代码",
|
||||
"txt_enable_totp": "启用 TOTP",
|
||||
"txt_enable_totp_failed": "启用 TOTP 失败",
|
||||
"txt_enabled": "已启用",
|
||||
"txt_encrypted_file": "加密文件",
|
||||
"txt_encrypted_file_2": "加密文件",
|
||||
"txt_enter_a_folder_name": "请输入文件夹名称",
|
||||
"txt_enter_master_password_to_disable_two_step_verification": "输入主密码以禁用两步验证",
|
||||
"txt_enter_master_password_to_continue": "输入主密码以继续",
|
||||
"txt_enter_master_password_to_view_this_item": "输入主密码以查看此项目",
|
||||
"txt_expiration_date": "过期日期",
|
||||
"txt_expiration_days_0_never": "过期天数(0 表示不过期)",
|
||||
"txt_expires_at": "过期时间",
|
||||
"txt_expires_at_value": "过期于:{value}",
|
||||
"txt_expiry": "有效期",
|
||||
"txt_expiry_month": "有效期月",
|
||||
"txt_expiry_year": "有效期年",
|
||||
"txt_failed_to_open_send": "打开发送失败",
|
||||
"txt_favorite": "收藏",
|
||||
"txt_favorites": "收藏",
|
||||
"txt_duplicates": "重复项",
|
||||
"txt_field": "字段",
|
||||
"txt_field_label": "字段标签",
|
||||
"txt_field_label_is_required": "字段标签不能为空",
|
||||
"txt_field_type": "字段类型",
|
||||
"txt_field_value": "字段值",
|
||||
"txt_file": "文件",
|
||||
"txt_file_name": "文件名",
|
||||
"txt_file_send": "文件发送",
|
||||
"txt_file_size": "文件大小",
|
||||
"txt_fingerprint": "指纹",
|
||||
"txt_firefox_browser": "Firefox 浏览器",
|
||||
"txt_firefox_extension": "Firefox 扩展",
|
||||
"txt_first_name": "名",
|
||||
"txt_folder": "文件夹",
|
||||
"txt_folder_created": "文件夹已创建",
|
||||
"txt_folder_name": "文件夹名称",
|
||||
"txt_folder_name_is_required": "文件夹名称不能为空",
|
||||
"txt_folders": "文件夹",
|
||||
"txt_hidden": "隐藏",
|
||||
"txt_hide": "隐藏",
|
||||
"txt_identity": "身份",
|
||||
"txt_identity_details": "身份详情",
|
||||
"txt_ie_browser": "IE 浏览器",
|
||||
"txt_invite_code_optional": "邀请码(首位注册者无需填写,其他人必填)",
|
||||
"txt_invite_created": "邀请码已创建",
|
||||
"txt_invite_revoked": "邀请码已撤销",
|
||||
"txt_invite_validity_hours": "邀请码有效期(小时)",
|
||||
"txt_invites": "邀请码",
|
||||
"txt_ios": "iOS",
|
||||
"txt_item": "项目",
|
||||
"txt_item_created": "项目已创建",
|
||||
"txt_item_deleted": "项目已删除",
|
||||
"txt_item_history": "项目历史",
|
||||
"txt_password_history": "密码历史记录",
|
||||
"txt_password_updated_value": "密码新于: {value}",
|
||||
"txt_item_name_is_required": "项目名称不能为空",
|
||||
"txt_item_updated": "项目已更新",
|
||||
"txt_last_edited_value": "最后编辑:{value}",
|
||||
"txt_last_name": "姓",
|
||||
"txt_last_seen": "最后在线",
|
||||
"txt_license_number": "证件号",
|
||||
"txt_link_copied": "链接已复制",
|
||||
"txt_linked": "已关联",
|
||||
"txt_linux_desktop": "Linux 桌面端",
|
||||
"txt_loading": "加载中...",
|
||||
"txt_loading_nodewarden": "正在加载 NodeWarden...",
|
||||
"txt_jwt_warning_title": "JWT_SECRET 配置警告",
|
||||
"txt_jwt_warning_subtitle": "JWT 密钥当前不安全,请先修复后再继续。",
|
||||
"txt_jwt_title_missing": "未检测到 JWT_SECRET",
|
||||
"txt_jwt_title_too_short": "JWT_SECRET 长度过短",
|
||||
"txt_jwt_title_default": "JWT_SECRET使用默认值",
|
||||
"txt_jwt_reason_missing": "未检测到 JWT_SECRET。",
|
||||
"txt_jwt_reason_default": "JWT_SECRET 仍在使用默认示例值。",
|
||||
"txt_jwt_reason_too_short": "JWT_SECRET 长度过短,至少需要 {min} 位。",
|
||||
"txt_jwt_how_to_fix_add": "处理步骤(添加 JWT_SECRET)",
|
||||
"txt_jwt_how_to_fix_replace": "处理步骤(更换 JWT_SECRET)",
|
||||
"txt_jwt_add_step_1": "使用下方 32 位随机生成器,复制一个新密钥。",
|
||||
"txt_jwt_add_step_2_prefix": "到 Cloudflare 控制台 -> Workers 和 Pages -> 你的服务 -> ",
|
||||
"txt_jwt_add_step_2_suffix": " -> 变量和机密 -> 新增",
|
||||
"txt_jwt_add_step_3": "保存并等待重新部署完成,然后刷新本页确认。",
|
||||
"txt_jwt_replace_step_1": "使用下方 32 位随机生成器,生成更强的密钥(至少 {min} 位)。",
|
||||
"txt_jwt_replace_step_2_prefix": "到 Cloudflare 控制台 -> Workers 和 Pages -> 你的服务 -> ",
|
||||
"txt_jwt_replace_step_2_suffix": " -> 变量和机密 -> 更新 JWT_SECRET",
|
||||
"txt_jwt_replace_step_3": "保存并等待重新部署完成,然后刷新本页确认。",
|
||||
"txt_jwt_secret_type_label": "类型:",
|
||||
"txt_jwt_secret_type_value": "密钥",
|
||||
"txt_jwt_secret_name_label": "变量名称:",
|
||||
"txt_jwt_secret_value_label": "值:",
|
||||
"txt_jwt_secret_value_requirement": "最低 {min} 位随机字符",
|
||||
"txt_jwt_what_is": "JWT 是什么",
|
||||
"txt_jwt_what_is_body": "JWT_SECRET 是服务端用来签发和校验登录令牌的密钥。如果它缺失、过短,或者仍然使用示例值,实例就不能安全地正常使用。",
|
||||
"txt_how_to_fix": "处理步骤(添加 / 更换)",
|
||||
"txt_jwt_fix_step_1": "你可以继续下一步,不影响使用。",
|
||||
"txt_jwt_fix_step_2": "如果当前密钥不是强随机值,建议使用下方 32 位生成器。",
|
||||
"txt_jwt_fix_step_3": "到 Cloudflare 控制台 -> Workers 和 Pages -> 你的服务 -> 设置 -> 变量和机密,更新 JWT_SECRET。",
|
||||
"txt_jwt_fix_step_4": "保存并等待重新部署完成,然后刷新本页确认。",
|
||||
"txt_random_secret_generator": "随机密钥生成器",
|
||||
"txt_copied": "已复制",
|
||||
"txt_log_in": "登录",
|
||||
"txt_logging_in": "正在登录...",
|
||||
"txt_log_out": "退出",
|
||||
"txt_lock": "锁定",
|
||||
"txt_menu": "菜单",
|
||||
"txt_settings": "设置",
|
||||
"txt_back": "返回",
|
||||
"txt_login": "登录",
|
||||
"txt_login_credentials": "登录信息",
|
||||
"txt_login_failed": "登录失败",
|
||||
"txt_login_success": "登录成功",
|
||||
"txt_macos_desktop": "macOS 桌面端",
|
||||
"txt_manage_authorized_devices_and_30_day_totp_trusted_sessions": "管理已授权设备和 30 天 TOTP 受信会话。",
|
||||
"txt_manage_device_sessions_and_30_day_totp_trusted_sessions": "管理设备会话和 30 天 TOTP 受信状态。",
|
||||
"txt_master_password": "主密码",
|
||||
"txt_master_password_changed_please_login_again": "主密码已修改,请重新登录",
|
||||
"txt_master_password_changed_signing_out_everywhere": "主密码已修改,正在退出所有设备",
|
||||
"txt_master_password_is_required": "主密码不能为空",
|
||||
"txt_master_password_is_required_2": "请输入主密码",
|
||||
"txt_master_password_must_be_at_least_12_chars": "主密码至少需要 12 个字符",
|
||||
"txt_master_password_reprompt": "主密码二次确认",
|
||||
"txt_master_password_reprompt_2": "主密码二次确认",
|
||||
"txt_max_access_count": "最大访问次数",
|
||||
"txt_middle_name": "中间名",
|
||||
"txt_drag_to_reorder": "拖动调整顺序",
|
||||
"txt_move": "移动",
|
||||
"txt_move_selected_items": "移动所选项目",
|
||||
"txt_moved_selected_items": "已移动所选项目",
|
||||
"txt_name": "名称",
|
||||
"txt_name_is_required": "名称不能为空",
|
||||
"txt_new_password": "新密码",
|
||||
"txt_nothing_to_copy": "没有可复制的内容",
|
||||
"txt_new_password_must_be_at_least_12_chars": "新密码至少需要 12 个字符",
|
||||
"txt_new_passwords_do_not_match": "两次输入的新密码不一致",
|
||||
"txt_new_send": "新建发送",
|
||||
"txt_next": "下一页",
|
||||
"txt_no": "否",
|
||||
"txt_no_devices_found": "未找到设备",
|
||||
"txt_no_folder": "无文件夹",
|
||||
"txt_no_items": "没有项目",
|
||||
"txt_no_username": "无用户名",
|
||||
"txt_no_verification_codes": "没有验证码",
|
||||
"txt_no_name": "(无名称)",
|
||||
"txt_no_sends": "没有发送",
|
||||
"txt_nodewarden_send": "NodeWarden 发送",
|
||||
"txt_not_trusted": "未信任",
|
||||
"txt_note": "笔记",
|
||||
"txt_notes": "备注",
|
||||
"txt_replace_device_name_with_note": "为这台设备设置自定义名称,不会改变系统识别到的设备类型。",
|
||||
"txt_number": "数字",
|
||||
"txt_open": "打开",
|
||||
"txt_opera_browser": "Opera 浏览器",
|
||||
"txt_opera_extension": "Opera 扩展",
|
||||
"txt_or": "或",
|
||||
"txt_options": "选项",
|
||||
"txt_passport_number": "护照号",
|
||||
"txt_password": "密码",
|
||||
"txt_password_is_already_verified": "密码已验证",
|
||||
"txt_passwords_do_not_match": "两次输入的密码不一致",
|
||||
"txt_password_hint": "密码提示",
|
||||
"txt_password_hint_optional": "密码提示(可选)",
|
||||
"txt_password_hint_placeholder": "写一句只有你自己看得懂的线索",
|
||||
"txt_password_hint_register_placeholder": "这个提示可以在网页登录页直接显示。",
|
||||
"txt_password_hint_register_help": "这个提示可以在网页登录页直接显示。不要填写主密码、恢复代码,或任何能直接暴露密码的信息。",
|
||||
"txt_password_hint_login_help": "忘记主密码时,可以查看注册时保存的提示。",
|
||||
"txt_password_hint_login_note": "这里只会显示提示语,不会显示你的主密码本身。",
|
||||
"txt_show_password_hint": "查看密码提示",
|
||||
"txt_hide_password_hint": "隐藏密码提示",
|
||||
"txt_loading_password_hint": "正在加载提示...",
|
||||
"txt_password_hint_not_set": "这个邮箱没有可显示的密码提示。",
|
||||
"txt_password_hint_load_failed": "加载密码提示失败",
|
||||
"txt_password_hint_too_long": "密码提示最多只能输入 120 个字符",
|
||||
"txt_passkey": "通行密钥",
|
||||
"txt_passkeys": "通行密钥",
|
||||
"txt_passkey_created_at_value": "创建于 {value}",
|
||||
"txt_phone": "电话",
|
||||
"txt_please_input_email_and_password": "请输入邮箱和密码",
|
||||
"txt_please_input_master_password": "请输入主密码",
|
||||
"txt_please_input_totp_code": "请输入 TOTP 验证码",
|
||||
"txt_please_select_a_file": "请选择文件",
|
||||
"txt_postal_code": "邮政编码",
|
||||
"txt_prev": "上一页",
|
||||
"txt_private_key": "私钥",
|
||||
"txt_profile": "资料",
|
||||
"txt_profile_unavailable": "资料不可用",
|
||||
"txt_profile_updated": "资料已更新",
|
||||
"txt_public_key": "公钥",
|
||||
"txt_recover_2fa_failed": "恢复 2FA 失败",
|
||||
"txt_recover_two_step_login": "恢复两步登录",
|
||||
"txt_recovered_but_auto_login_failed_please_sign_in": "已恢复,但自动登录失败,请手动登录",
|
||||
"txt_recovery_code": "恢复代码",
|
||||
"txt_recovery_code_and_api_key": "恢复代码和 API 密钥",
|
||||
"txt_recovery_code_copied": "恢复代码已复制",
|
||||
"txt_recovery_code_is_empty": "恢复代码为空",
|
||||
"txt_recovery_code_loaded": "恢复代码已加载",
|
||||
"txt_api_key": "API 密钥",
|
||||
"txt_view_api_key": "查看 API 密钥",
|
||||
"txt_rotate_api_key": "轮换 API 密钥",
|
||||
"txt_api_key_copied": "API 密钥已复制",
|
||||
"txt_api_key_loaded": "API 密钥已加载",
|
||||
"txt_api_key_rotated": "API 密钥已轮换",
|
||||
"txt_rotate_api_key_confirm": "轮换 API 密钥?当前密钥将立即失效。",
|
||||
"txt_api_key_is_empty": "API 密钥为空",
|
||||
"txt_api_key_dialog_intro": "您的 API 密钥可用于在 Bitwarden CLI 中进行身份验证。",
|
||||
"txt_api_key_warning_body": "您的 API 密钥是一种替代身份验证机制。请严格保密。",
|
||||
"txt_oauth_client_credentials": "OAuth 2.0 客户端凭据",
|
||||
"txt_client_id": "client_id",
|
||||
"txt_client_secret": "client_secret",
|
||||
"txt_scope": "scope",
|
||||
"txt_grant_type": "grant_type",
|
||||
"txt_refresh": "刷新",
|
||||
"txt_refresh_in_seconds_s": "{seconds} 秒后刷新",
|
||||
"txt_regenerate": "重新生成",
|
||||
"txt_registration_succeeded_please_sign_in": "注册成功,请登录",
|
||||
"txt_remove": "移除",
|
||||
"txt_remove_device": "移除设备",
|
||||
"txt_remove_device_2": "移除设备",
|
||||
"txt_remove_all_devices": "移除所有设备",
|
||||
"txt_remove_all_devices_and_clear_all_2fa_trust": "确认移除所有设备并清除全部 2FA 信任吗?",
|
||||
"txt_remove_all_devices_and_sign_out_all_sessions": "确认移除所有设备、清除全部信任,并让所有设备重新登录吗?",
|
||||
"txt_remove_device_name_and_clear_its_2fa_trust": "确认移除设备“{name}”并清除其 2FA 信任吗?",
|
||||
"txt_remove_device_and_sign_out_name": "确认移除设备“{name}”,清除其信任,并让它重新登录吗?",
|
||||
"txt_reveal": "显示",
|
||||
"txt_restore": "恢复",
|
||||
"txt_revoke": "撤销",
|
||||
"txt_revoke_30_day_totp_trust_for_name": "确认撤销“{name}”的 30 天 TOTP 信任吗?",
|
||||
"txt_revoke_30_day_totp_trust_from_all_devices": "确认撤销所有设备的 30 天 TOTP 信任吗?",
|
||||
"txt_revoke_all_trusted": "撤销全部受信任设备",
|
||||
"txt_revoke_all_trusted_devices": "撤销所有设备信任",
|
||||
"txt_revoke_device_authorization": "撤销设备信任",
|
||||
"txt_revoke_device_trust_failed": "撤销设备信任失败",
|
||||
"txt_revoke_all_device_trust_failed": "撤销所有设备信任失败",
|
||||
"txt_revoke_trust": "撤销信任",
|
||||
"txt_untrust": "不信任",
|
||||
"txt_update_device_note_failed": "更新设备备注失败",
|
||||
"txt_role": "角色",
|
||||
"txt_save": "保存",
|
||||
"txt_save_profile": "保存资料",
|
||||
"txt_save_profile_failed": "保存资料失败",
|
||||
"txt_search_sends": "搜索发送...",
|
||||
"txt_search_your_secure_vault": "搜索你的密码库...",
|
||||
"txt_clear_search": "清空搜索",
|
||||
"txt_clear_search_esc": "清空搜索(Esc)",
|
||||
"txt_sort": "排序",
|
||||
"txt_sort_last_edited": "最近修改",
|
||||
"txt_sort_created": "最近创建",
|
||||
"txt_sort_name": "A-Z",
|
||||
"txt_secret_and_code_are_required": "密钥和代码不能为空",
|
||||
"txt_secret_copied": "密钥已复制",
|
||||
"txt_secure_note": "安全笔记",
|
||||
"txt_security_code": "安全码",
|
||||
"txt_security_code_cvv": "安全码 (CVV)",
|
||||
"txt_select_all": "全选",
|
||||
"txt_select_duplicate_items": "选择重复项",
|
||||
"txt_select_an_item": "请选择一个项目",
|
||||
"txt_send_created": "发送已创建",
|
||||
"txt_send_deleted": "发送已删除",
|
||||
"txt_send_details": "发送详情",
|
||||
"txt_send_file": "发送文件",
|
||||
"txt_send_unavailable": "发送不可用。",
|
||||
"txt_send_updated": "发送已更新",
|
||||
"txt_sign_out": "退出登录",
|
||||
"txt_ssh_key": "SSH 密钥",
|
||||
"txt_ssn": "社保号",
|
||||
"txt_state_province": "省 / 州",
|
||||
"txt_status": "状态",
|
||||
"txt_online": "在线",
|
||||
"txt_offline": "离线",
|
||||
"txt_submit": "提交",
|
||||
"txt_sync": "同步",
|
||||
"txt_sync_vault": "同步",
|
||||
"txt_switch_to_dark_mode": "切换到暗黑模式",
|
||||
"txt_switch_to_light_mode": "切换到明亮模式",
|
||||
"txt_dash": "-",
|
||||
"txt_text": "文本",
|
||||
"txt_text_2fa_recovered": "2FA 已恢复",
|
||||
"txt_text_2fa_recovered_new_recovery_code_code": "2FA 已恢复,新的恢复代码:{code}",
|
||||
"txt_text_3": "------",
|
||||
"txt_text_is_required": "文本不能为空",
|
||||
"txt_text_send": "文本发送",
|
||||
"txt_this_is_a_one_time_code_after_it_is_used_a_new_code_is_generated_automatically": "这是一次性恢复代码,使用后将自动生成新的恢复代码。",
|
||||
"txt_this_item_requires_master_password_every_time_before_viewing_details": "每次查看详情前均需输入主密码",
|
||||
"txt_this_link_is_missing_decryption_key": "此链接缺少解密密钥",
|
||||
"txt_this_send_is_password_protected": "此发送受密码保护",
|
||||
"txt_title": "称谓",
|
||||
"txt_totp": "TOTP",
|
||||
"txt_totp_code": "TOTP 验证码",
|
||||
"txt_totp_disabled": "TOTP 已禁用",
|
||||
"txt_totp_enabled": "TOTP 已启用",
|
||||
"txt_totp_is_enabled_for_this_account": "此账户已启用 TOTP。",
|
||||
"txt_total_items_count": "共 {count} 项",
|
||||
"txt_totp_secret": "TOTP 密钥",
|
||||
"txt_totp_verify_failed": "TOTP 验证失败",
|
||||
"txt_attachments": "附件",
|
||||
"txt_upload_attachments": "上传附件",
|
||||
"txt_new_attachments": "待上传附件",
|
||||
"txt_marked_for_removal_count": "保存后将删除 {count} 个附件",
|
||||
"txt_trash": "回收站",
|
||||
"txt_trust_this_device_for_30_days": "信任此设备 30 天",
|
||||
"txt_trusted_until": "信任至",
|
||||
"txt_two_step_verification": "两步验证",
|
||||
"txt_type": "类型",
|
||||
"txt_type_type": "类型 {type}",
|
||||
"txt_unban": "解封",
|
||||
"txt_unchecked": "未勾选",
|
||||
"txt_unknown_device": "未知设备",
|
||||
"txt_unlock": "解锁",
|
||||
"txt_unlocking": "正在解锁...",
|
||||
"txt_unlock_details": "解锁详情",
|
||||
"txt_unlock_failed": "解锁失败",
|
||||
"txt_unlock_failed_master_password_is_incorrect": "解锁失败,主密码不正确。",
|
||||
"txt_unlock_item": "解锁项目",
|
||||
"txt_unlock_send": "解锁发送",
|
||||
"txt_unlock_vault": "解锁密码库",
|
||||
"txt_unlocked": "已解锁",
|
||||
"txt_all_devices_removed": "已移除所有设备",
|
||||
"txt_remove_device_failed": "移除设备失败",
|
||||
"txt_remove_all_devices_failed": "移除所有设备失败",
|
||||
"txt_update_item_failed": "更新项目失败",
|
||||
"txt_update_send_failed": "更新发送失败",
|
||||
"txt_use_recovery_code": "使用恢复代码",
|
||||
"txt_use_your_one_time_recovery_code_to_disable_two_step_verification": "使用一次性恢复代码禁用两步验证。",
|
||||
"txt_user_deleted": "用户已删除",
|
||||
"txt_user_status_updated": "用户状态已更新",
|
||||
"txt_username": "用户名",
|
||||
"txt_uri_match_default_base_domain": "默认(基础域名)",
|
||||
"txt_uri_match_base_domain": "基础域名",
|
||||
"txt_uri_match_host": "主机",
|
||||
"txt_uri_match_exact": "精确",
|
||||
"txt_uri_match_never": "从不",
|
||||
"txt_uri_match_starts_with": "开始于",
|
||||
"txt_uri_match_regular_expression": "正则表达式",
|
||||
"txt_users": "用户",
|
||||
"txt_vault_synced": "密码库已同步",
|
||||
"txt_verification_code": "验证码",
|
||||
"txt_verify": "验证",
|
||||
"txt_warning": "警告",
|
||||
"txt_view_recovery_code": "查看恢复代码",
|
||||
"txt_web": "网页",
|
||||
"txt_website": "网站",
|
||||
"txt_websites": "网站",
|
||||
"txt_windows_desktop": "Windows 桌面端",
|
||||
"txt_yes": "是",
|
||||
"txt_auto_lock": "会话超时",
|
||||
"txt_auto_lock_description": "页面闲置后执行会话超时动作;关闭页面或浏览器后再次打开始终进入锁定页。",
|
||||
"txt_auto_lock_updated": "会话超时已更新",
|
||||
"txt_session_timeout": "会话超时",
|
||||
"txt_session_timeout_updated": "会话超时已更新",
|
||||
"txt_timeout_time": "超时时间",
|
||||
"txt_timeout_action": "超时动作",
|
||||
"txt_timeout_action_logout": "注销",
|
||||
"txt_timeout_action_lock": "锁定",
|
||||
"txt_in_planning": "构思中",
|
||||
"txt_security_preferences": "安全偏好",
|
||||
"txt_timeout_1_minute": "1 分钟",
|
||||
"txt_timeout_5_minutes": "5 分钟",
|
||||
"txt_timeout_15_minutes": "15 分钟",
|
||||
"txt_timeout_30_minutes": "30 分钟",
|
||||
"txt_timeout_never": "从不",
|
||||
"txt_lock_after_1_minute": "闲置 1 分钟后",
|
||||
"txt_lock_after_5_minutes": "闲置 5 分钟后",
|
||||
"txt_lock_after_15_minutes": "闲置 15 分钟后",
|
||||
"txt_lock_after_30_minutes": "闲置 30 分钟后",
|
||||
"txt_lock_after_never": "不因闲置锁定",
|
||||
"txt_import": "导入",
|
||||
"txt_export": "导出",
|
||||
"txt_format": "格式",
|
||||
"txt_source_file": "源文件",
|
||||
"txt_folder_handling": "文件夹处理",
|
||||
"txt_import_folder_mode_original": "保留导入文件中的原始路径",
|
||||
"txt_import_folder_mode_none": "不使用文件夹",
|
||||
"txt_import_folder_mode_target": "导入到指定文件夹",
|
||||
"txt_target_folder": "目标文件夹",
|
||||
"txt_select_folder_placeholder": "-- 选择文件夹 --",
|
||||
"txt_import_vault_data_hint": "将数据导入到当前账号。",
|
||||
"txt_export_vault_data_hint": "从当前账号导出数据。",
|
||||
"txt_import_export_title": "导入导出",
|
||||
"txt_encrypted_mode": "加密方式",
|
||||
"txt_account_verification": "账号验证",
|
||||
"txt_password_verification": "密码验证",
|
||||
"txt_file_password": "文件密码",
|
||||
"txt_zip_password_optional": "ZIP 密码(可选)",
|
||||
"txt_zip_password": "ZIP 密码",
|
||||
"txt_close": "关闭",
|
||||
"txt_total": "总计",
|
||||
"txt_import_success": "数据导入成功",
|
||||
"txt_import_success_number_of_items": "一共导入了 {count} 个项目。",
|
||||
"txt_import_attachment_summary": "附件已导入 {imported}/{total} 个。",
|
||||
"txt_import_failed_attachments_title": "以下 {count} 个附件未导入:",
|
||||
"txt_import_attachment_target_not_found": "没有找到对应的导入项目。",
|
||||
"txt_upload_attachment_failed": "附件上传失败。",
|
||||
"txt_import_file_password_required": "请输入文件密码。",
|
||||
"txt_import_invalid_zip_password": "ZIP 密码错误。",
|
||||
"txt_export_completed": "导出完成",
|
||||
"txt_export_failed": "导出失败",
|
||||
"txt_import_invalid_password_protected_file": "密码保护导出文件格式无效。",
|
||||
"txt_import_decrypt_failed": "导入文件解密失败。",
|
||||
"txt_import_empty_zip_archive": "ZIP 压缩包为空。",
|
||||
"txt_import_no_json_found_in_zip": "ZIP 内未找到可导入的 JSON 数据。",
|
||||
"txt_import_data_json_not_found": "ZIP 内未找到 data.json。",
|
||||
"txt_import_zip_password_required": "该 ZIP 需要密码。",
|
||||
"txt_import_invalid_json_file": "JSON 文件无效",
|
||||
"txt_import_failed": "导入失败",
|
||||
"txt_import_encrypted_file_title": "导入加密文件",
|
||||
"txt_import_encrypted_file_message": "该 Bitwarden 导出文件已加密,请输入文件密码继续。",
|
||||
"txt_import_encrypted_zip_title": "导入加密 ZIP",
|
||||
"txt_import_encrypted_zip_message": "该 ZIP 压缩包已加密,请输入 ZIP 密码继续。",
|
||||
"txt_new_type_header": "新建{type}",
|
||||
"txt_edit_type_header": "编辑{type}",
|
||||
"txt_delete_folder": "删除文件夹",
|
||||
"txt_delete_folder_message": "删除文件夹「{name}」?其中的项目将移至无文件夹。",
|
||||
"txt_delete_all_folders": "删除全部文件夹",
|
||||
"txt_delete_all_folders_message": "确认删除全部文件夹吗?其中的项目将移至无文件夹。",
|
||||
"txt_folder_not_found": "文件夹不存在",
|
||||
"txt_folder_deleted": "文件夹已删除",
|
||||
"txt_folder_updated": "文件夹已重命名",
|
||||
"txt_folders_deleted": "文件夹已删除",
|
||||
"txt_update_folder_failed": "重命名文件夹失败",
|
||||
"txt_delete_folder_failed": "删除文件夹失败",
|
||||
"txt_delete_all_folders_failed": "删除全部文件夹失败",
|
||||
"txt_other": "其他",
|
||||
"txt_vault_key_unavailable": "账户密钥不可用,请先解锁密码库后重试。",
|
||||
"txt_vault_not_ready": "密码库数据尚未就绪",
|
||||
"txt_unsupported_export_format": "不支持的导出格式",
|
||||
"txt_invalid_encrypted_export": "加密导出文件无效。",
|
||||
"txt_export_belongs_to_another_account": "此加密导出文件属于另一个账号。",
|
||||
"txt_invalid_argon2id_params": "导出文件中的 Argon2id 参数无效。",
|
||||
"txt_unsupported_kdf_type": "不支持的 KDF 类型:{type}",
|
||||
"txt_invalid_file_password": "文件密码错误。",
|
||||
"txt_failed_to_map_attachments": "无法将 {count} 个附件匹配到导入项目。",
|
||||
"txt_role_admin": "管理员",
|
||||
"txt_role_user": "用户",
|
||||
"txt_status_active": "正常",
|
||||
"txt_status_banned": "已封禁",
|
||||
"txt_status_inactive": "未激活"
|
||||
};
|
||||
|
||||
export default zhCN;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { render } from 'preact';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import App from './App';
|
||||
import { initI18n } from './lib/i18n';
|
||||
import './tailwind.css';
|
||||
import './styles.css';
|
||||
|
||||
@@ -14,9 +15,14 @@ const queryClient = new QueryClient({
|
||||
},
|
||||
});
|
||||
|
||||
async function bootstrap(): Promise<void> {
|
||||
await initI18n();
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>,
|
||||
document.getElementById('root')!
|
||||
);
|
||||
}
|
||||
|
||||
void bootstrap();
|
||||
|
||||
@@ -28,6 +28,18 @@ export default defineConfig({
|
||||
|
||||
const normalized = id.replace(/\\/g, '/');
|
||||
|
||||
if (normalized.includes('/src/lib/i18n/locales/en.ts')) {
|
||||
return 'i18n-en';
|
||||
}
|
||||
|
||||
if (normalized.includes('/src/lib/i18n/locales/zh-CN.ts')) {
|
||||
return 'i18n-zh-CN';
|
||||
}
|
||||
|
||||
if (normalized.includes('/src/lib/i18n.ts')) {
|
||||
return 'i18n-core';
|
||||
}
|
||||
|
||||
if (
|
||||
normalized.includes('/src/components/AuthViews.tsx') ||
|
||||
normalized.includes('/src/components/PublicSendPage.tsx') ||
|
||||
|
||||
Reference in New Issue
Block a user