Add dynamic .json translations for content script (#1006)

This commit is contained in:
Leendert de Borst
2025-07-15 10:31:14 +02:00
committed by Leendert de Borst
parent 0c2de27f1a
commit accc76d8a2
11 changed files with 344 additions and 215 deletions

View File

@@ -1,8 +1,8 @@
import { type Browser } from '@wxt-dev/browser';
import { sendMessage } from 'webext-bridge/background';
import { t } from '@/utils/contentTranslations';
import { PasswordGenerator } from '@/utils/dist/shared/password-generator';
import { tc } from '@/utils/i18n/StandaloneI18n';
import { browser } from "#imports";
@@ -21,7 +21,7 @@ export async function setupContextMenus() : Promise<void> {
browser.contextMenus.create({
id: "aliasvault-activate-form",
parentId: "aliasvault-root",
title: await t('autofillWithAliasVault'),
title: await tc('autofillWithAliasVault'),
contexts: ["editable"],
});
@@ -37,7 +37,7 @@ export async function setupContextMenus() : Promise<void> {
browser.contextMenus.create({
id: "aliasvault-generate-password",
parentId: "aliasvault-root",
title: await t('generateRandomPassword'),
title: await tc('generateRandomPassword'),
contexts: ["all"]
});
@@ -83,7 +83,7 @@ export function handleContextMenuClick(info: Browser.contextMenus.OnClickData, t
*/
async function copyPasswordToClipboard(generatedPassword: string) : Promise<void> {
navigator.clipboard.writeText(generatedPassword).then(async () => {
showToast(await t('passwordCopiedToClipboard'));
showToast(await tc('passwordCopiedToClipboard'));
});
/**

View File

@@ -9,6 +9,7 @@ import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/Boo
import { defineContentScript } from '#imports';
import { createShadowRootUi } from '#imports';
import { tc } from '@/utils/i18n/StandaloneI18n';
export default defineContentScript({
matches: ['<all_urls>'],
@@ -159,13 +160,13 @@ export default defineContentScript({
if (authStatus.hasPendingMigrations) {
// Show upgrade required popup
createUpgradeRequiredPopup(inputElement, container, 'Vault upgrade required.');
await createUpgradeRequiredPopup(inputElement, container, await tc('vaultUpgradeRequired'));
return;
}
if (authStatus.error) {
// Show upgrade required popup for version-related errors
createUpgradeRequiredPopup(inputElement, container, authStatus.error);
await createUpgradeRequiredPopup(inputElement, container, authStatus.error);
return;
}

View File

@@ -4,11 +4,11 @@ import { filterCredentials } from '@/entrypoints/contentScript/Filter';
import { fillCredential } from '@/entrypoints/contentScript/Form';
import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY, LAST_CUSTOM_EMAIL_KEY, LAST_CUSTOM_USERNAME_KEY } from '@/utils/Constants';
import { t } from '@/utils/contentTranslations';
import { CreateIdentityGenerator } from '@/utils/dist/shared/identity-generator';
import type { Credential } from '@/utils/dist/shared/models/vault';
import { CreatePasswordGenerator, PasswordGenerator } from '@/utils/dist/shared/password-generator';
import { FormDetector } from '@/utils/formDetector/FormDetector';
import { tc } from '@/utils/i18n/StandaloneI18n';
import { SqliteClient } from '@/utils/SqliteClient';
import { CredentialsResponse } from '@/utils/types/messaging/CredentialsResponse';
import { IdentitySettingsResponse } from '@/utils/types/messaging/IdentitySettingsResponse';
@@ -47,8 +47,7 @@ export function openAutofillPopup(input: HTMLInputElement, container: HTMLElemen
if (response.success) {
await createAutofillPopup(input, response.credentials, container);
} else {
const vaultLockedText = await t('vaultLocked');
createVaultLockedPopup(input, container, vaultLockedText);
await createVaultLockedPopup(input, container);
}
})();
}
@@ -158,13 +157,13 @@ export function removeExistingPopup(container: HTMLElement) : void {
*/
export async function createAutofillPopup(input: HTMLInputElement, credentials: Credential[] | undefined, rootContainer: HTMLElement) : Promise<void> {
// Get all translations first
const newText = await t('new');
const searchPlaceholder = await t('searchVault');
const hideFor1HourText = await t('hideFor1Hour');
const hidePermanentlyText = await t('hidePermanently');
const noMatchesText = await t('noMatchesFound');
const creatingText = await t('creatingNewAlias');
const failedText = await t('failedToCreateIdentity');
const newText = await tc('new');
const searchPlaceholder = await tc('searchVault');
const hideFor1HourText = await tc('hideFor1Hour');
const hidePermanentlyText = await tc('hidePermanently');
const noMatchesText = await tc('noMatchesFound');
const creatingText = await tc('creatingNewAlias');
const failedText = await tc('failedToCreateIdentity');
// Disable browser's native autocomplete to avoid conflicts with AliasVault's autocomplete.
input.setAttribute('autocomplete', 'false');
@@ -449,7 +448,7 @@ export async function createAutofillPopup(input: HTMLInputElement, credentials:
/**
* Create vault locked popup.
*/
export function createVaultLockedPopup(input: HTMLInputElement, rootContainer: HTMLElement, vaultLockedText?: string): void {
export async function createVaultLockedPopup(input: HTMLInputElement, rootContainer: HTMLElement): Promise<void> {
/**
* Handle unlock click.
*/
@@ -472,7 +471,7 @@ export function createVaultLockedPopup(input: HTMLInputElement, rootContainer: H
// Add message
const messageElement = document.createElement('div');
messageElement.className = 'av-vault-locked-message';
messageElement.textContent = vaultLockedText || 'AliasVault is locked.';
messageElement.textContent = await tc('vaultLocked');
container.appendChild(messageElement);
// Add unlock button with SVG icon
@@ -772,10 +771,10 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
<circle cx="16" cy="16" r="1"/>
</svg>
`;
const randomIdentitySubtext = await t('randomIdentityDescription');
const randomIdentityTitle = await t('createRandomAlias');
const randomIdentityTitleDropdown = await t('randomAlias');
const randomIdentitySubtextDropdown = 'Random identity with random email';
const randomIdentitySubtext = await tc('randomIdentityDescription');
const randomIdentityTitle = await tc('createRandomAlias');
const randomIdentityTitleDropdown = await tc('randomAlias');
const randomIdentitySubtextDropdown = await tc('randomIdentityDescriptionDropdown');
const manualUsernamePasswordIcon = `
<svg class="av-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
@@ -783,22 +782,24 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
<path d="M5.5 20a6.5 6.5 0 0 1 13 0"/>
</svg>
`;
const manualUsernamePasswordSubtext = await t('manualCredentialDescription');
const manualUsernamePasswordTitle = await t('createUsernamePassword');
const manualUsernamePasswordTitleDropdown = await t('usernamePassword');
const manualUsernamePasswordSubtextDropdown = 'Manual username and password';
const manualUsernamePasswordSubtext = await tc('manualCredentialDescription');
const manualUsernamePasswordTitle = await tc('createUsernamePassword');
const manualUsernamePasswordTitleDropdown = await tc('usernamePassword');
const manualUsernamePasswordSubtextDropdown = await tc('manualCredentialDescriptionDropdown');
// Get all translated strings first
const serviceNameText = await t('serviceName');
const enterServiceNameText = await t('enterServiceName');
const cancelText = await t('cancel');
const createAndSaveAliasText = await t('createAndSaveAlias');
const emailText = await t('email');
const enterEmailAddressText = await t('enterEmailAddress');
const usernameText = await t('username');
const enterUsernameText = await t('enterUsername');
const generatedPasswordText = await t('generatedPassword');
const createAndSaveCredentialText = await t('createAndSaveCredential');
const serviceNameText = await tc('serviceName');
const enterServiceNameText = await tc('enterServiceName');
const cancelText = await tc('cancel');
const createAndSaveAliasText = await tc('createAndSaveAlias');
const emailText = await tc('email');
const enterEmailAddressText = await tc('enterEmailAddress');
const usernameText = await tc('username');
const enterUsernameText = await tc('enterUsername');
const generatedPasswordText = await tc('generatedPassword');
const generateNewPasswordText = await tc('generateNewPassword');
const togglePasswordVisibilityText = await tc('togglePasswordVisibility');
const createAndSaveCredentialText = await tc('createAndSaveCredential');
// Create the main content
popup.innerHTML = `
@@ -893,13 +894,13 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
class="av-create-popup-input"
data-is-generated="true"
>
<button id="toggle-password-visibility" class="av-create-popup-visibility-btn" title="Toggle password visibility">
<button id="toggle-password-visibility" class="av-create-popup-visibility-btn" title="${togglePasswordVisibilityText}">
<svg class="av-icon" viewBox="0 0 24 24">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
<button id="regenerate-password" class="av-create-popup-regenerate-btn" title="Generate new password">
<button id="regenerate-password" class="av-create-popup-regenerate-btn" title="${generateNewPasswordText}">
<svg class="av-icon" viewBox="0 0 24 24">
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
<path d="M3 3v5h5"></path>
@@ -1138,7 +1139,7 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
/**
* Handle custom save button click.
*/
const handleCustomSave = () : void => {
const handleCustomSave = async () : Promise<void> => {
const serviceName = inputServiceName.value.trim();
if (serviceName) {
const email = customEmail.value.trim();
@@ -1162,14 +1163,14 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
if (!emailLabel.querySelector('.av-create-popup-error-text')) {
const emailError = document.createElement('span');
emailError.className = 'av-create-popup-error-text';
emailError.textContent = 'Enter email and/or username';
emailError.textContent = await tc('enterEmailAndOrUsername');
emailLabel.appendChild(emailError);
}
if (!usernameLabel.querySelector('.av-create-popup-error-text')) {
const usernameError = document.createElement('span');
usernameError.className = 'av-create-popup-error-text';
usernameError.textContent = 'Enter email and/or username';
usernameError.textContent = await tc('enterEmailAndOrUsername');
usernameLabel.appendChild(usernameError);
}
@@ -1496,7 +1497,7 @@ function addReliableClickHandler(element: HTMLElement, handler: (e: Event) => vo
/**
* Create upgrade required popup.
*/
export function createUpgradeRequiredPopup(input: HTMLInputElement, rootContainer: HTMLElement, errorMessage: string): void {
export async function createUpgradeRequiredPopup(input: HTMLInputElement, rootContainer: HTMLElement, errorMessage: string): Promise<void> {
/**
* Handle upgrade click.
*/
@@ -1524,7 +1525,7 @@ export function createUpgradeRequiredPopup(input: HTMLInputElement, rootContaine
// Add upgrade button with SVG icon
const button = document.createElement('button');
button.title = 'Open AliasVault to upgrade';
button.title = await tc('openAliasVaultToUpgrade');
button.className = 'av-upgrade-required-button';
button.innerHTML = `
<svg class="av-icon-upgrade" viewBox="0 0 24 24">
@@ -1539,7 +1540,7 @@ export function createUpgradeRequiredPopup(input: HTMLInputElement, rootContaine
// Add close button as a separate element positioned to the right
const closeButton = document.createElement('button');
closeButton.className = 'av-button av-button-close av-upgrade-required-close';
closeButton.title = 'Dismiss popup';
closeButton.title = await tc('dismissPopup');
closeButton.innerHTML = `
<svg class="av-icon" viewBox="0 0 24 24">
<line x1="18" y1="6" x2="6" y2="18"></line>

View File

@@ -182,7 +182,7 @@ const Unlock: React.FC = () => {
</Button>
<div className="text-sm font-medium text-gray-500 dark:text-gray-200 mt-6">
Switch accounts? <button onClick={handleLogout} className="text-primary-700 hover:underline dark:text-primary-500">{t('logout')}</button>
{t('switchAccounts')} <button onClick={handleLogout} className="text-primary-700 hover:underline dark:text-primary-500">{t('logout')}</button>
</div>
</form>
</div>

View File

@@ -23,6 +23,7 @@
"sessionExpired": "Your session has expired. Please log in again.",
"unlockSuccess": "Vault unlocked successfully!",
"connectingTo": "Connecting to",
"switchAccounts": "Switch accounts?",
"errors": {
"invalidCode": "Please enter a valid 6-digit authentication code.",
"serverError": "Could not reach AliasVault server. Please try again later or contact support if the problem persists.",

View File

@@ -42,7 +42,21 @@
"NoVaultFound": "Your account does not have a vault yet. Please complete the tutorial in the AliasVault web client before using the browser extension.",
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help."
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
"unknownError": "An unknown error occurred",
"failedToStoreVault": "Failed to store vault",
"vaultNotAvailable": "Vault not available",
"failedToGetVault": "Failed to get vault",
"vaultIsLocked": "Vault is locked",
"failedToGetCredentials": "Failed to get credentials",
"failedToCreateIdentity": "Failed to create identity",
"failedToGetDefaultEmailDomain": "Failed to get default email domain",
"failedToGetDefaultIdentitySettings": "Failed to get default identity settings",
"failedToGetPasswordSettings": "Failed to get password settings",
"failedToUploadVault": "Failed to upload vault",
"noDerivedKeyAvailable": "No derived key available for encryption",
"failedToUploadVaultToServer": "Failed to upload new vault to server",
"noVaultOrDerivedKeyFound": "No vault or derived key found"
},
"apiErrors": {
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",

View File

@@ -0,0 +1,39 @@
{
"new": "New",
"cancel": "Cancel",
"search": "Search",
"vaultLocked": "AliasVault is locked.",
"creatingNewAlias": "Creating new alias...",
"noMatchesFound": "No matches found",
"searchVault": "Search vault...",
"serviceName": "Service name",
"email": "Email",
"username": "Username",
"generatedPassword": "Generated Password",
"enterServiceName": "Enter service name",
"enterEmailAddress": "Enter email address",
"enterUsername": "Enter username",
"hideFor1Hour": "Hide for 1 hour (current site)",
"hidePermanently": "Hide permanently (current site)",
"createRandomAlias": "Create random alias",
"createUsernamePassword": "Create username/password",
"randomAlias": "Random alias",
"usernamePassword": "Username/password",
"createAndSaveAlias": "Create and save alias",
"createAndSaveCredential": "Create and save credential",
"randomIdentityDescription": "Generate a random identity with a random email address accessible in AliasVault.",
"randomIdentityDescriptionDropdown": "Random identity with random email",
"manualCredentialDescription": "Specify your own email address and username.",
"manualCredentialDescriptionDropdown": "Manual username and password",
"failedToCreateIdentity": "Failed to create identity. Please try again.",
"enterEmailAndOrUsername": "Enter email and/or username",
"autofillWithAliasVault": "Autofill with AliasVault",
"generateRandomPassword": "Generate random password (copy to clipboard)",
"generateNewPassword": "Generate new password",
"togglePasswordVisibility": "Toggle password visibility",
"passwordCopiedToClipboard": "Password copied to clipboard",
"enterEmailAndOrUsernameError": "Enter email and/or username",
"openAliasVaultToUpgrade": "Open AliasVault to upgrade",
"vaultUpgradeRequired": "Vault upgrade required.",
"dismissPopup": "Dismiss popup"
}

View File

@@ -42,7 +42,21 @@
"NoVaultFound": "Uw account heeft nog geen vault. Voltooi eerst de tutorial in de AliasVault webclient voordat u de browserextensie gebruikt.",
"serverNotAvailable": "De AliasVault server is niet beschikbaar. Probeer het later opnieuw of neem contact op met de ondersteuning als het probleem aanhoudt.",
"clientVersionNotSupported": "Deze versie van de AliasVault browserextensie wordt niet meer ondersteund door de server. Update uw browserextensie naar de nieuwste versie.",
"serverVersionNotSupported": "De AliasVault server moet worden bijgewerkt naar een nieuwere versie om deze browserextensie te kunnen gebruiken. Neem contact op met de ondersteuning als u hulp nodig hebt."
"serverVersionNotSupported": "De AliasVault server moet worden bijgewerkt naar een nieuwere versie om deze browserextensie te kunnen gebruiken. Neem contact op met de ondersteuning als u hulp nodig hebt.",
"unknownError": "Er is een onbekende fout opgetreden",
"failedToStoreVault": "Vault opslaan mislukt",
"vaultNotAvailable": "Vault niet beschikbaar",
"failedToGetVault": "Vault ophalen mislukt",
"vaultIsLocked": "Vault is vergrendeld",
"failedToGetCredentials": "Inloggegevens ophalen mislukt",
"failedToCreateIdentity": "Identiteit aanmaken mislukt",
"failedToGetDefaultEmailDomain": "Standaard e-maildomein ophalen mislukt",
"failedToGetDefaultIdentitySettings": "Standaard identiteitsinstellingen ophalen mislukt",
"failedToGetPasswordSettings": "Wachtwoordinstellingen ophalen mislukt",
"failedToUploadVault": "Vault uploaden mislukt",
"noDerivedKeyAvailable": "Geen afgeleide sleutel beschikbaar voor versleuteling",
"failedToUploadVaultToServer": "Nieuwe vault uploaden naar server mislukt",
"noVaultOrDerivedKeyFound": "Geen vault of afgeleide sleutel gevonden"
},
"apiErrors": {
"UNKNOWN_ERROR": "Er is een onbekende fout opgetreden. Probeer het opnieuw.",

View File

@@ -0,0 +1,31 @@
{
"new": "Nieuw",
"cancel": "Annuleren",
"search": "Zoeken",
"vaultLocked": "AliasVault is vergrendeld.",
"creatingNewAlias": "Nieuwe alias aanmaken...",
"noMatchesFound": "Geen resultaten gevonden",
"searchVault": "Kluis doorzoeken...",
"serviceName": "Servicenaam",
"email": "E-mail",
"username": "Gebruikersnaam",
"generatedPassword": "Gegenereerd wachtwoord",
"enterServiceName": "Voer servicenaam in",
"enterEmailAddress": "Voer e-mailadres in",
"enterUsername": "Voer gebruikersnaam in",
"hideFor1Hour": "Verberg voor 1 uur (huidige site)",
"hidePermanently": "Permanent verbergen (huidige site)",
"createRandomAlias": "Willekeurige alias aanmaken",
"createUsernamePassword": "Gebruikersnaam/wachtwoord aanmaken",
"randomAlias": "Willekeurige alias",
"usernamePassword": "Gebruikersnaam/wachtwoord",
"createAndSaveAlias": "Alias aanmaken en opslaan",
"createAndSaveCredential": "Inloggegevens aanmaken en opslaan",
"randomIdentityDescription": "Genereer een willekeurige identiteit met een willekeurig e-mailadres toegankelijk in AliasVault.",
"manualCredentialDescription": "Specificeer je eigen e-mailadres en gebruikersnaam.",
"failedToCreateIdentity": "Identiteit aanmaken mislukt. Probeer opnieuw.",
"enterEmailAndOrUsername": "Voer e-mail en/of gebruikersnaam in",
"autofillWithAliasVault": "Autofill met AliasVault",
"generateRandomPassword": "Willekeurig wachtwoord genereren (kopiëren naar klembord)",
"passwordCopiedToClipboard": "Wachtwoord gekopieerd naar klembord"
}

View File

@@ -1,168 +0,0 @@
import { storage } from '#imports';
/**
* Translation keys for content scripts
*/
export interface IContentTranslations {
// Common
new: string;
cancel: string;
search: string;
// Status messages
vaultLocked: string;
creatingNewAlias: string;
noMatchesFound: string;
// Form labels and placeholders
searchVault: string;
serviceName: string;
email: string;
username: string;
generatedPassword: string;
enterServiceName: string;
enterEmailAddress: string;
enterUsername: string;
// Context menu
hideFor1Hour: string;
hidePermanently: string;
// Create alias popup
createRandomAlias: string;
createUsernamePassword: string;
randomAlias: string;
usernamePassword: string;
createAndSaveAlias: string;
createAndSaveCredential: string;
randomIdentityDescription: string;
manualCredentialDescription: string;
// Error messages
failedToCreateIdentity: string;
enterEmailAndOrUsername: string;
// Context menu
autofillWithAliasVault: string;
generateRandomPassword: string;
passwordCopiedToClipboard: string;
}
/**
* English translations
*/
const enTranslations: IContentTranslations = {
new: 'New',
cancel: 'Cancel',
search: 'Search',
vaultLocked: 'AliasVault is locked.',
creatingNewAlias: 'Creating new alias...',
noMatchesFound: 'No matches found',
searchVault: 'Search vault...',
serviceName: 'Service name',
email: 'Email',
username: 'Username',
generatedPassword: 'Generated Password',
enterServiceName: 'Enter service name',
enterEmailAddress: 'Enter email address',
enterUsername: 'Enter username',
hideFor1Hour: 'Hide for 1 hour (current site)',
hidePermanently: 'Hide permanently (current site)',
createRandomAlias: 'Create random alias',
createUsernamePassword: 'Create username/password',
randomAlias: 'Random alias',
usernamePassword: 'Username/password',
createAndSaveAlias: 'Create and save alias',
createAndSaveCredential: 'Create and save credential',
randomIdentityDescription: 'Generate a random identity with a random email address accessible in AliasVault.',
manualCredentialDescription: 'Specify your own email address and username.',
failedToCreateIdentity: 'Failed to create identity. Please try again.',
enterEmailAndOrUsername: 'Enter email and/or username',
autofillWithAliasVault: 'Autofill with AliasVault',
generateRandomPassword: 'Generate random password (copy to clipboard)',
passwordCopiedToClipboard: 'Password copied to clipboard'
};
/**
* Dutch translations
*/
const nlTranslations: IContentTranslations = {
new: 'Nieuw',
cancel: 'Annuleren',
search: 'Zoeken',
vaultLocked: 'AliasVault is vergrendeld.',
creatingNewAlias: 'Nieuwe alias aanmaken...',
noMatchesFound: 'Geen resultaten gevonden',
searchVault: 'Kluis doorzoeken...',
serviceName: 'Servicenaam',
email: 'E-mail',
username: 'Gebruikersnaam',
generatedPassword: 'Gegenereerd wachtwoord',
enterServiceName: 'Voer servicenaam in',
enterEmailAddress: 'Voer e-mailadres in',
enterUsername: 'Voer gebruikersnaam in',
hideFor1Hour: 'Verberg voor 1 uur (huidige site)',
hidePermanently: 'Permanent verbergen (huidige site)',
createRandomAlias: 'Willekeurige alias aanmaken',
createUsernamePassword: 'Gebruikersnaam/wachtwoord aanmaken',
randomAlias: 'Willekeurige alias',
usernamePassword: 'Gebruikersnaam/wachtwoord',
createAndSaveAlias: 'Alias aanmaken en opslaan',
createAndSaveCredential: 'Inloggegevens aanmaken en opslaan',
randomIdentityDescription: 'Genereer een willekeurige identiteit met een willekeurig e-mailadres toegankelijk in AliasVault.',
manualCredentialDescription: 'Specificeer je eigen e-mailadres en gebruikersnaam.',
failedToCreateIdentity: 'Identiteit aanmaken mislukt. Probeer opnieuw.',
enterEmailAndOrUsername: 'Voer e-mail en/of gebruikersnaam in',
autofillWithAliasVault: 'Autofill met AliasVault',
generateRandomPassword: 'Willekeurig wachtwoord genereren (kopiëren naar klembord)',
passwordCopiedToClipboard: 'Wachtwoord gekopieerd naar klembord'
};
/**
* All available translations
*/
const translations = {
en: enTranslations,
nl: nlTranslations
};
/**
* Get current language from storage
*/
export async function getCurrentLanguage(): Promise<string> {
try {
// Use extension storage API exclusively (reliable across all contexts)
const langFromStorage = await storage.getItem('local:language') as string;
if (langFromStorage && ['en', 'nl'].includes(langFromStorage)) {
return langFromStorage;
}
// If no language is set in storage, detect browser language and save it
const browserLang = navigator.language.split('-')[0];
const detectedLanguage = ['en', 'nl'].includes(browserLang) ? browserLang : 'en';
// Save the detected language to storage for future use
await storage.setItem('local:language', detectedLanguage);
return detectedLanguage;
} catch (error) {
console.error('Failed to get current language:', error);
return 'en';
}
}
/**
* Get translation for a key
*/
export async function t(key: keyof IContentTranslations): Promise<string> {
const lang = await getCurrentLanguage();
return translations[lang as keyof typeof translations]?.[key] || translations.en[key];
}
/**
* Get all translations for current language
*/
export async function getTranslations(): Promise<IContentTranslations> {
const lang = await getCurrentLanguage();
return translations[lang as keyof typeof translations] || translations.en;
}

View File

@@ -0,0 +1,196 @@
import { storage } from '#imports';
/**
* Type for content translations
*/
export type ContentTranslations = {
[key: string]: string;
};
/**
* Cache for loaded translations to avoid repeated file reads
*/
const translationCache = new Map<string, ContentTranslations>();
/**
* Get current language from storage
*/
export async function getCurrentLanguage(): Promise<string> {
try {
// Use extension storage API exclusively (reliable across all contexts)
const langFromStorage = await storage.getItem('local:language') as string;
if (langFromStorage && ['en', 'nl', 'de', 'es', 'fr', 'uk'].includes(langFromStorage)) {
return langFromStorage;
}
// If no language is set in storage, detect browser language and save it
const browserLang = navigator.language.split('-')[0];
const detectedLanguage = ['en', 'nl', 'de', 'es', 'fr', 'uk'].includes(browserLang) ? browserLang : 'en';
// Save the detected language to storage for future use
await storage.setItem('local:language', detectedLanguage);
return detectedLanguage;
} catch (error) {
console.error('Failed to get current language:', error);
return 'en';
}
}
/**
* Load translations for a specific namespace and language
*/
async function loadTranslations(namespace: string, language: string): Promise<ContentTranslations> {
const cacheKey = `${namespace}:${language}`;
// Check cache first
if (translationCache.has(cacheKey)) {
return translationCache.get(cacheKey)!;
}
try {
// Dynamically import the translation file
const translations = await import(`../../locales/${language}/${namespace}.json`);
const translationData = translations.default || translations;
// Cache the translations
translationCache.set(cacheKey, translationData);
return translationData;
} catch (error) {
console.warn(`Failed to load translations for ${namespace}:${language}`, error);
// Fallback to English if available
if (language !== 'en') {
try {
const fallbackTranslations = await import(`../../locales/en/${namespace}.json`);
const fallbackData = fallbackTranslations.default || fallbackTranslations;
translationCache.set(cacheKey, fallbackData);
return fallbackData;
} catch (fallbackError) {
console.error('Failed to load English fallback translations', fallbackError);
}
}
// Return empty object as last resort
return {};
}
}
/**
* Translation function for non-React contexts
*
* @param key - Translation key (supports nested keys like 'errors.networkError')
* @param namespace - Translation namespace (default: 'content')
* @param fallback - Fallback text if translation is not found
* @returns Promise<string> - Translated text
*/
export async function t(
key: string,
namespace: string = 'content',
fallback?: string
): Promise<string> {
try {
const language = await getCurrentLanguage();
const translations = await loadTranslations(namespace, language);
// Support nested keys like 'errors.networkError'
const value = getNestedValue(translations, key);
if (value && typeof value === 'string') {
return value;
}
// If translation not found and we're not using English, try English fallback
if (language !== 'en') {
const englishTranslations = await loadTranslations(namespace, 'en');
const englishValue = getNestedValue(englishTranslations, key);
if (englishValue && typeof englishValue === 'string') {
return englishValue;
}
}
// Return fallback or key if no translation found
return fallback || key;
} catch (error) {
console.error('Translation error:', error);
return fallback || key;
}
}
/**
* Get nested value from object using dot notation
*/
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
return path.split('.').reduce((current, key) => {
return current && current[key] !== undefined ? current[key] : undefined;
}, obj);
}
/**
* Translation function specifically for content scripts
* This is a convenience wrapper around the main t() function
*/
export async function tc(key: string, fallback?: string): Promise<string> {
return t(key, 'content', fallback);
}
/**
* Translation function for errors namespace
*/
export async function te(key: string, fallback?: string): Promise<string> {
return t(key, 'errors', fallback);
}
/**
* Translation function for common namespace
*/
export async function tcommon(key: string, fallback?: string): Promise<string> {
return t(key, 'common', fallback);
}
/**
* Clear translation cache (useful for language changes)
*/
export function clearTranslationCache(): void {
translationCache.clear();
}
/**
* Pre-load common translations for use in synchronous contexts
*/
export async function preloadTranslationsForSync(
namespaces: string[] = ['common', 'errors']
): Promise<Record<string, ContentTranslations>> {
const languages = ['en', 'nl'];
const translations: Record<string, ContentTranslations> = {};
for (const lang of languages) {
const langTranslations: ContentTranslations = {};
for (const namespace of namespaces) {
try {
const nsTranslations = await loadTranslations(namespace, lang);
// Flatten the namespace structure for easier access
Object.keys(nsTranslations).forEach(key => {
if (namespace === 'common' && key === 'errors') {
// Handle nested errors object
const errors = nsTranslations[key] as Record<string, string>;
Object.keys(errors).forEach(errorKey => {
langTranslations[`errors.${errorKey}`] = errors[errorKey];
});
} else {
langTranslations[key] = nsTranslations[key];
}
});
} catch (error) {
console.warn(`Failed to preload ${namespace} translations for ${lang}:`, error);
}
}
translations[lang] = langTranslations;
}
return translations;
}