Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions frontend/scripts/i18n-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,58 @@ const report = msg => {
console.error(` ✗ ${msg}`)
}

// Plural keys (key_one, key_other, …) expand per-locale: Japanese has only
// `other`, English/German/Spanish have `one`+`other`. So the set of concrete
// keys legitimately differs between locales — compare by plural BASE plus each
// locale's own CLDR categories, not by raw key equality.
const PLURAL_SUFFIX = /^(.*)_(zero|one|two|few|many|other)$/
const splitPlural = key => {
const m = key.match(PLURAL_SUFFIX)
return m ? { base: m[1], cat: m[2] } : { base: key, cat: null }
}
const categoriesFor = locale => new Set(new Intl.PluralRules(locale, { type: 'cardinal' }).resolvedOptions().pluralCategories)

for (const ns of namespaces) {
const source = flatten(load(SOURCE, ns))
const sourceKeys = new Set(Object.keys(source))

for (const [key, value] of Object.entries(source)) {
if (typeof value === 'string' && value.trim() === '') report(`[${SOURCE}/${ns}] empty English value: "${key}"`)
}

// Split English keys into plain keys and plural bases (a base is "plural" if
// English carries any plural form of it).
const sourcePlain = new Set()
const sourcePluralBases = new Set()
for (const key of Object.keys(source)) {
const { base, cat } = splitPlural(key)
if (cat) sourcePluralBases.add(base)
else sourcePlain.add(key)
}

for (const locale of locales) {
if (locale === SOURCE) continue
const target = flatten(load(locale, ns))
const targetKeys = new Set(Object.keys(target))
for (const key of sourceKeys) if (!targetKeys.has(key)) report(`[${locale}/${ns}] missing key: "${key}"`)
for (const key of targetKeys) if (!sourceKeys.has(key)) report(`[${locale}/${ns}] dead key (not in ${SOURCE}): "${key}"`)
const cats = categoriesFor(locale)

// Plain keys must match one-for-one.
for (const key of sourcePlain) if (!targetKeys.has(key)) report(`[${locale}/${ns}] missing key: "${key}"`)

// Each English plural base must have exactly this locale's plural categories.
for (const base of sourcePluralBases)
for (const cat of cats)
if (!targetKeys.has(`${base}_${cat}`)) report(`[${locale}/${ns}] missing plural form: "${base}_${cat}"`)

// Dead keys: present here but not accounted for in English.
for (const key of targetKeys) {
const { base, cat } = splitPlural(key)
if (cat) {
if (!sourcePluralBases.has(base)) report(`[${locale}/${ns}] dead key (not in ${SOURCE}): "${key}"`)
else if (!cats.has(cat)) report(`[${locale}/${ns}] unexpected plural form for ${locale}: "${key}"`)
} else if (!sourcePlain.has(key)) {
report(`[${locale}/${ns}] dead key (not in ${SOURCE}): "${key}"`)
}
}
}
}

Expand Down
100 changes: 99 additions & 1 deletion frontend/src/i18n/locales/de/notices.json
Original file line number Diff line number Diff line change
@@ -1 +1,99 @@
{}
{
"access": {
"noDevice": "Sie haben keinen Zugriff auf dieses Gerät. ({{id}})",
"noService": "Sie haben keinen Zugriff auf diesen Dienst. ({{id}})"
},
"auth": {
"emailModified": "E-Mail-Adresse erfolgreich geändert.",
"invalidFormat": "Ungültiges Format.",
"loginFailed": "Anmeldung fehlgeschlagen.",
"passwordChanged": "Passwort erfolgreich geändert."
},
"connection": {
"surveyFailed": "Die Übermittlung der Verbindungsumfrage ist fehlgeschlagen. Bitte wenden Sie sich an den Support."
},
"device": {
"cannotDeleteOnline": "Ein Online-Gerät kann nicht gelöscht werden. Heben Sie die Auswahl von „{{name}}“ auf und versuchen Sie es erneut.",
"cannotDeleteShared": "Ein freigegebenes Gerät kann nicht gelöscht werden. Heben Sie die Auswahl von „{{name}}“ auf oder verlassen Sie die Freigabe und versuchen Sie es erneut.",
"cannotTransferShared": "Ein freigegebenes Gerät kann nicht übertragen werden. Heben Sie die Auswahl von „{{name}}“ auf oder verlassen Sie die Freigabe und versuchen Sie es erneut.",
"deleted": "„{{name}}“ wurde erfolgreich gelöscht.",
"deletedCount_one": "{{count}} Gerät wurde erfolgreich gelöscht.",
"deletedCount_other": "{{count}} Geräte wurden erfolgreich gelöscht.",
"idNotFound": "Eine Geräte-ID konnte nicht gefunden werden. Heben Sie die Auswahl von {{id}} auf und versuchen Sie es erneut.",
"noPermissionDelete": "Sie haben keine Berechtigung, ein Gerät zu löschen. Heben Sie die Auswahl von „{{name}}“ auf und versuchen Sie es erneut.",
"noPermissionTransfer": "Sie haben keine Berechtigung, ein Gerät zu übertragen. Heben Sie die Auswahl von „{{name}}“ auf und versuchen Sie es erneut.",
"notFound": "Ihr Gerät ({{code}}) konnte nicht gefunden werden.",
"registered": "„{{name}}“ wurde erfolgreich registriert!",
"removed": "„{{name}}“ wurde erfolgreich entfernt.",
"restored": "Gerät erfolgreich wiederhergestellt!",
"transferred": "„{{name}}“ wurde erfolgreich an {{email}} übertragen.",
"transferredCount_one": "{{count}} Gerät wurde erfolgreich an {{email}} übertragen.",
"transferredCount_other": "{{count}} Geräte wurden erfolgreich an {{email}} übertragen.",
"unregistered": "Geräteregistrierung erfolgreich aufgehoben!"
},
"feedback": {
"sendError": "Beim Senden der Nachricht ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut."
},
"file": {
"deleteError": "Fehler beim Löschen der Datei",
"updateError": "Fehler beim Aktualisieren der Datei"
},
"job": {
"deleteError": "Fehler beim Löschen des Jobs",
"loadLogsError": "Protokolle konnten nicht geladen werden: {{message}}",
"noDeviceLogs": "Für dieses Gerät sind noch keine Protokolle verfügbar.",
"noLogs": "Es sind noch keine Protokolle verfügbar.",
"noRunLogs": "Für diesen Durchlauf sind noch keine Protokolle verfügbar."
},
"mfa": {
"enabled": "Zwei-Faktor-Authentifizierung erfolgreich aktiviert.",
"enableError": "Fehler beim Aktivieren der Zwei-Faktor-Authentifizierung: {{error}}",
"invalidTotp": "Ungültiger TOTP-Code. ({{error}})",
"phoneVerificationError": "Fehler bei der Telefonverifizierung: {{error}}",
"updatePhoneError": "Fehler beim Aktualisieren der Telefonnummer: {{error}}",
"verificationSent": "Bestätigung gesendet."
},
"network": {
"addFailed": "Hinzufügen des Netzwerks fehlgeschlagen. Bitte wenden Sie sich an den Support.",
"removeConnectionFailed": "Die Netzwerkverbindung ({{serviceId}}) konnte nicht von {{network}} entfernt werden. Bitte wenden Sie sich an den Support."
},
"organization": {
"created": "Ihre Organisation wurde erstellt.",
"customerRemoveError": "Beim Entfernen eines Kunden ist ein Problem aufgetreten. Bitte wenden Sie sich an den Support, falls das Problem weiterhin besteht",
"left": "Sie haben die Organisation erfolgreich verlassen.",
"removed": "Ihre Organisation wurde entfernt.",
"removedNamed": "„{{name}}“ wurde erfolgreich entfernt.",
"resellerReportError": "Die URL für den Reseller-Bericht konnte nicht abgerufen werden",
"updateFailed": "Aktualisierung von {{type}} fehlgeschlagen, bitte überprüfen Sie Ihre Formulardaten."
},
"plan": {
"cancelFailed": "Kündigung des Abonnements fehlgeschlagen, bitte wenden Sie sich an den Support.",
"checkoutFailed": "Bezahlvorgang fehlgeschlagen, bitte wenden Sie sich an den Support.",
"missingPrice": "Für die ausgewählte Preisstufe fehlt ein Preis",
"purchaseError": "Fehler beim Kauf der Lizenz",
"subscriptionUpdatedOther": "Das {{plan}}-Abonnement von {{email}} wurde aktualisiert.",
"subscriptionUpdatedYours": "Ihr {{plan}}-Abonnement wurde aktualisiert.",
"updateFailed": "Aktualisierung des Abonnements fehlgeschlagen, bitte wenden Sie sich an den Support."
},
"service": {
"linkRemoveError": "Beim Entfernen des Dienstlinks ist ein Fehler aufgetreten.",
"removed": "Dienst wurde erfolgreich entfernt.",
"updateError": "Dienst konnte nicht aktualisiert werden."
},
"share": {
"removed": "{{email}} wurde erfolgreich entfernt."
},
"tag": {
"added_one": "{{tag}} wurde zu {{count}} Gerät hinzugefügt.",
"added_other": "{{tag}} wurde zu {{count}} Geräten hinzugefügt.",
"merged": "Tag mit vorhandenem Tag ‚{{name}}‘ zusammengeführt.",
"renameError": "Ihr Tag ({{name}}) konnte nicht umbenannt werden."
},
"user": {
"languageChanged": "Sprache geändert zu {{language}}",
"leaveResellerError": "Verlassen des Resellers fehlgeschlagen"
},
"version": {
"unsupported": "Diese Version von Desktop wird nicht mehr unterstützt. Sie sollte in Kürze automatisch aktualisiert werden."
}
}
100 changes: 99 additions & 1 deletion frontend/src/i18n/locales/en/notices.json
Original file line number Diff line number Diff line change
@@ -1 +1,99 @@
{}
{
"access": {
"noDevice": "You don't have access to that device. ({{id}})",
"noService": "You don't have access to that service. ({{id}})"
},
"auth": {
"emailModified": "Email modified successfully.",
"invalidFormat": "Invalid format.",
"loginFailed": "Login failed.",
"passwordChanged": "Password changed successfully."
},
"connection": {
"surveyFailed": "Connection survey submission failed. Please contact support."
},
"device": {
"cannotDeleteOnline": "You cannot delete an online device. Deselect \"{{name}}\" and try again.",
"cannotDeleteShared": "You cannot delete a shared device. Deselect or leave \"{{name}}\" and try again.",
"cannotTransferShared": "You cannot transfer a shared device. Deselect or leave \"{{name}}\" and try again.",
"deleted": "\"{{name}}\" was successfully deleted.",
"deletedCount_one": "{{count}} device was successfully deleted.",
"deletedCount_other": "{{count}} devices were successfully deleted.",
"idNotFound": "A device id could not be found. Deselect {{id}} and try again.",
"noPermissionDelete": "You do not have permission to delete a device. Deselect \"{{name}}\" and try again.",
"noPermissionTransfer": "You do not have permission to transfer a device. Deselect \"{{name}}\" and try again.",
"notFound": "Your device ({{code}}) could not be found.",
"registered": "'{{name}}' was successfully registered!",
"removed": "\"{{name}}\" was successfully removed.",
"restored": "Device restored successfully!",
"transferred": "\"{{name}}\" was successfully transferred to {{email}}.",
"transferredCount_one": "{{count}} device was successfully transferred to {{email}}.",
"transferredCount_other": "{{count}} devices were successfully transferred to {{email}}.",
"unregistered": "Device unregistered successfully!"
},
"feedback": {
"sendError": "Sending message encountered an error. Please try again."
},
"file": {
"deleteError": "Error deleting file",
"updateError": "Error updating file"
},
"job": {
"deleteError": "Error deleting job",
"loadLogsError": "Couldn’t load logs: {{message}}",
"noDeviceLogs": "No logs available for this device yet.",
"noLogs": "No logs available yet.",
"noRunLogs": "No logs are available for this run yet."
},
"mfa": {
"enabled": "Two-factor authentication enabled successfully.",
"enableError": "Two-factor authentication enabled error: {{error}}",
"invalidTotp": "Invalid TOTP Code. ({{error}})",
"phoneVerificationError": "Phone verification error: {{error}}",
"updatePhoneError": "Update phone error: {{error}}",
"verificationSent": "Verification sent."
},
"network": {
"addFailed": "Adding network failed. Please contact support.",
"removeConnectionFailed": "Failed to remove network connection ({{serviceId}}) from {{network}}. Please contact support."
},
"organization": {
"created": "Your organization has been created.",
"customerRemoveError": "There was a problem removing a customer. Please contact support if the issue persists",
"left": "You have successfully left the organization.",
"removed": "Your organization has been removed.",
"removedNamed": "Successfully removed “{{name}}”.",
"resellerReportError": "Failed to get reseller report URL",
"updateFailed": "{{type}} update failed, please validate your form data."
},
"plan": {
"cancelFailed": "Subscription cancellation failed, please contact support.",
"checkoutFailed": "Checkout failed, please contact support.",
"missingPrice": "Plan selection missing price",
"purchaseError": "Error purchasing license",
"subscriptionUpdatedOther": "{{email}}'s {{plan}} subscription updated.",
"subscriptionUpdatedYours": "Your {{plan}} subscription updated.",
"updateFailed": "Subscription update failed, please contact support."
},
"service": {
"linkRemoveError": "An error occurred when trying to remove the service link.",
"removed": "Service was successfully removed.",
"updateError": "Could not update service."
},
"share": {
"removed": "{{email}} successfully removed."
},
"tag": {
"added_one": "{{tag}} added to {{count}} device.",
"added_other": "{{tag}} added to {{count}} devices.",
"merged": "Tag merged into existing tag ‘{{name}}.’",
"renameError": "Your tag ({{name}}) could not be renamed."
},
"user": {
"languageChanged": "Language changed to {{language}}",
"leaveResellerError": "Failed to leave reseller"
},
"version": {
"unsupported": "This version of Desktop is no longer supported. It should auto update shortly."
}
}
103 changes: 102 additions & 1 deletion frontend/src/i18n/locales/es/notices.json
Original file line number Diff line number Diff line change
@@ -1 +1,102 @@
{}
{
"access": {
"noDevice": "No tienes acceso a ese dispositivo. ({{id}})",
"noService": "No tienes acceso a ese servicio. ({{id}})"
},
"auth": {
"emailModified": "Correo electrónico modificado correctamente.",
"invalidFormat": "Formato no válido.",
"loginFailed": "Error al iniciar sesión.",
"passwordChanged": "Contraseña cambiada correctamente."
},
"connection": {
"surveyFailed": "No se pudo enviar la encuesta de conexión. Ponte en contacto con soporte."
},
"device": {
"cannotDeleteOnline": "No puedes eliminar un dispositivo en línea. Deselecciona “{{name}}” e inténtalo de nuevo.",
"cannotDeleteShared": "No puedes eliminar un dispositivo compartido. Deselecciona o abandona “{{name}}” e inténtalo de nuevo.",
"cannotTransferShared": "No puedes transferir un dispositivo compartido. Deselecciona o abandona “{{name}}” e inténtalo de nuevo.",
"deleted": "“{{name}}” se eliminó correctamente.",
"deletedCount_one": "{{count}} dispositivo se eliminó correctamente.",
"deletedCount_many": "{{count}} dispositivos se eliminaron correctamente.",
"deletedCount_other": "{{count}} dispositivos se eliminaron correctamente.",
"idNotFound": "No se pudo encontrar el ID del dispositivo. Deselecciona {{id}} e inténtalo de nuevo.",
"noPermissionDelete": "No tienes permiso para eliminar un dispositivo. Deselecciona “{{name}}” e inténtalo de nuevo.",
"noPermissionTransfer": "No tienes permiso para transferir un dispositivo. Deselecciona “{{name}}” e inténtalo de nuevo.",
"notFound": "No se pudo encontrar tu dispositivo ({{code}}).",
"registered": "¡“{{name}}” se registró correctamente!",
"removed": "“{{name}}” se quitó correctamente.",
"restored": "¡Dispositivo restaurado correctamente!",
"transferred": "“{{name}}” se transfirió correctamente a {{email}}.",
"transferredCount_one": "{{count}} dispositivo se transfirió correctamente a {{email}}.",
"transferredCount_many": "{{count}} dispositivos se transfirieron correctamente a {{email}}.",
"transferredCount_other": "{{count}} dispositivos se transfirieron correctamente a {{email}}.",
"unregistered": "¡Registro del dispositivo anulado correctamente!"
},
"feedback": {
"sendError": "Se produjo un error al enviar el mensaje. Inténtalo de nuevo."
},
"file": {
"deleteError": "Error al eliminar el archivo",
"updateError": "Error al actualizar el archivo"
},
"job": {
"deleteError": "Error al eliminar el trabajo",
"loadLogsError": "No se pudieron cargar los registros: {{message}}",
"noDeviceLogs": "Aún no hay registros disponibles para este dispositivo.",
"noLogs": "Aún no hay registros disponibles.",
"noRunLogs": "Aún no hay registros disponibles para esta ejecución."
},
"mfa": {
"enabled": "Autenticación de dos factores activada correctamente.",
"enableError": "Error al activar la autenticación de dos factores: {{error}}",
"invalidTotp": "Código TOTP no válido. ({{error}})",
"phoneVerificationError": "Error de verificación telefónica: {{error}}",
"updatePhoneError": "Error al actualizar el teléfono: {{error}}",
"verificationSent": "Verificación enviada."
},
"network": {
"addFailed": "No se pudo agregar la red. Ponte en contacto con soporte.",
"removeConnectionFailed": "No se pudo quitar la conexión de red ({{serviceId}}) de {{network}}. Ponte en contacto con soporte."
},
"organization": {
"created": "Tu organización se ha creado.",
"customerRemoveError": "Se produjo un problema al eliminar un cliente. Ponte en contacto con soporte si el problema persiste",
"left": "Has abandonado la organización correctamente.",
"removed": "Tu organización se ha eliminado.",
"removedNamed": "“{{name}}” se eliminó correctamente.",
"resellerReportError": "No se pudo obtener la URL del informe de revendedor",
"updateFailed": "Error al actualizar {{type}}; valida los datos de tu formulario."
},
"plan": {
"cancelFailed": "No se pudo cancelar la suscripción; ponte en contacto con soporte.",
"checkoutFailed": "Error en el pago; ponte en contacto con soporte.",
"missingPrice": "Falta el precio en la selección del plan",
"purchaseError": "Error al comprar la licencia",
"subscriptionUpdatedOther": "Se actualizó la suscripción {{plan}} de {{email}}.",
"subscriptionUpdatedYours": "Se actualizó tu suscripción {{plan}}.",
"updateFailed": "No se pudo actualizar la suscripción; ponte en contacto con soporte."
},
"service": {
"linkRemoveError": "Se produjo un error al intentar quitar el enlace del servicio.",
"removed": "El servicio se quitó correctamente.",
"updateError": "No se pudo actualizar el servicio."
},
"share": {
"removed": "{{email}} se eliminó correctamente."
},
"tag": {
"added_one": "{{tag}} se agregó a {{count}} dispositivo.",
"added_many": "{{tag}} se agregó a {{count}} dispositivos.",
"added_other": "{{tag}} se agregó a {{count}} dispositivos.",
"merged": "La etiqueta se combinó con la etiqueta existente ‘{{name}}.’",
"renameError": "No se pudo cambiar el nombre de tu etiqueta ({{name}})."
},
"user": {
"languageChanged": "Idioma cambiado a {{language}}",
"leaveResellerError": "No se pudo abandonar el revendedor"
},
"version": {
"unsupported": "Esta versión de Desktop ya no es compatible. Debería actualizarse automáticamente en breve."
}
}
Loading
Loading