221 lines
7.2 KiB
TypeScript
221 lines
7.2 KiB
TypeScript
|
|
import type { Request } from "express";
|
|||
|
|
import type { Lang } from "./ruleCatalogI18n";
|
|||
|
|
|
|||
|
|
import { normalizeLang } from "./ruleCatalogI18n";
|
|||
|
|
|
|||
|
|
export type { Lang } from "./ruleCatalogI18n";
|
|||
|
|
export { normalizeLang, SUPPORTED_LANGS } from "./ruleCatalogI18n";
|
|||
|
|
|
|||
|
|
type MessageKey =
|
|||
|
|
| "aiRulesDisabled"
|
|||
|
|
| "aiNoProvider"
|
|||
|
|
| "aiNoToken"
|
|||
|
|
| "aiUnknownError"
|
|||
|
|
| "invalidId"
|
|||
|
|
| "invalidInput"
|
|||
|
|
| "scanNotFound"
|
|||
|
|
| "ruleNotFound"
|
|||
|
|
| "promptNotFound"
|
|||
|
|
| "providerNotFound"
|
|||
|
|
| "rateLimited"
|
|||
|
|
| "zipMissing"
|
|||
|
|
| "fileMissing"
|
|||
|
|
| "textMissing"
|
|||
|
|
| "noAnalyzableFiles"
|
|||
|
|
| "skillUnreadable"
|
|||
|
|
| "analysisFailed"
|
|||
|
|
| "noDownloadableFiles"
|
|||
|
|
| "onlyPassedDownloadable"
|
|||
|
|
| "descriptionFailed"
|
|||
|
|
| "noApiTokenPlain"
|
|||
|
|
| "noApiTokenProvided"
|
|||
|
|
| "modelsLoadFailed"
|
|||
|
|
| "connSuccessReply"
|
|||
|
|
| "connSuccessModels"
|
|||
|
|
| "connSuccessNoModels"
|
|||
|
|
| "connReplyEmpty"
|
|||
|
|
| "connFailed";
|
|||
|
|
|
|||
|
|
const MESSAGES: Record<MessageKey, Record<Lang, string>> = {
|
|||
|
|
aiRulesDisabled: {
|
|||
|
|
de: "KI-Regeln sind im Regelwerk deaktiviert.",
|
|||
|
|
en: "AI rules are disabled in the rule set.",
|
|||
|
|
es: "Las reglas de IA están desactivadas en el conjunto de reglas.",
|
|||
|
|
},
|
|||
|
|
aiNoProvider: {
|
|||
|
|
de: "Kein aktiver KI-Provider konfiguriert. Bitte im Admin-Bereich einrichten.",
|
|||
|
|
en: "No active AI provider configured. Please set one up in the admin area.",
|
|||
|
|
es: "No hay ningún proveedor de IA activo configurado. Configúrelo en el área de administración.",
|
|||
|
|
},
|
|||
|
|
aiNoToken: {
|
|||
|
|
de: 'Für den Provider "{name}" ist kein API-Token hinterlegt.',
|
|||
|
|
en: 'No API token is stored for the provider "{name}".',
|
|||
|
|
es: 'No hay ningún token de API almacenado para el proveedor "{name}".',
|
|||
|
|
},
|
|||
|
|
aiUnknownError: {
|
|||
|
|
de: "Unbekannter KI-Fehler",
|
|||
|
|
en: "Unknown AI error",
|
|||
|
|
es: "Error de IA desconocido",
|
|||
|
|
},
|
|||
|
|
invalidId: {
|
|||
|
|
de: "Ungültige ID",
|
|||
|
|
en: "Invalid ID",
|
|||
|
|
es: "ID no válido",
|
|||
|
|
},
|
|||
|
|
invalidInput: {
|
|||
|
|
de: "Ungültige Eingabe",
|
|||
|
|
en: "Invalid input",
|
|||
|
|
es: "Entrada no válida",
|
|||
|
|
},
|
|||
|
|
scanNotFound: {
|
|||
|
|
de: "Scan nicht gefunden",
|
|||
|
|
en: "Scan not found",
|
|||
|
|
es: "Análisis no encontrado",
|
|||
|
|
},
|
|||
|
|
ruleNotFound: {
|
|||
|
|
de: "Regel nicht gefunden",
|
|||
|
|
en: "Rule not found",
|
|||
|
|
es: "Regla no encontrada",
|
|||
|
|
},
|
|||
|
|
promptNotFound: {
|
|||
|
|
de: "Prompt nicht gefunden",
|
|||
|
|
en: "Prompt not found",
|
|||
|
|
es: "Prompt no encontrado",
|
|||
|
|
},
|
|||
|
|
providerNotFound: {
|
|||
|
|
de: "Provider nicht gefunden",
|
|||
|
|
en: "Provider not found",
|
|||
|
|
es: "Proveedor no encontrado",
|
|||
|
|
},
|
|||
|
|
rateLimited: {
|
|||
|
|
de: "Zu viele Scans in kurzer Zeit. Bitte später erneut versuchen.",
|
|||
|
|
en: "Too many scans in a short time. Please try again later.",
|
|||
|
|
es: "Demasiados análisis en poco tiempo. Inténtelo de nuevo más tarde.",
|
|||
|
|
},
|
|||
|
|
zipMissing: {
|
|||
|
|
de: "ZIP-Inhalt fehlt.",
|
|||
|
|
en: "ZIP content is missing.",
|
|||
|
|
es: "Falta el contenido del ZIP.",
|
|||
|
|
},
|
|||
|
|
fileMissing: {
|
|||
|
|
de: "Dateiinhalt fehlt.",
|
|||
|
|
en: "File content is missing.",
|
|||
|
|
es: "Falta el contenido del archivo.",
|
|||
|
|
},
|
|||
|
|
textMissing: {
|
|||
|
|
de: "Text fehlt.",
|
|||
|
|
en: "Text is missing.",
|
|||
|
|
es: "Falta el texto.",
|
|||
|
|
},
|
|||
|
|
noAnalyzableFiles: {
|
|||
|
|
de: "Keine analysierbaren Dateien gefunden.",
|
|||
|
|
en: "No analyzable files found.",
|
|||
|
|
es: "No se encontraron archivos analizables.",
|
|||
|
|
},
|
|||
|
|
skillUnreadable: {
|
|||
|
|
de: "Das Skill konnte nicht gelesen werden. Bitte prüfen Sie das Format (gültiges ZIP / Textdatei).",
|
|||
|
|
en: "The skill could not be read. Please check the format (valid ZIP / text file).",
|
|||
|
|
es: "No se pudo leer la skill. Compruebe el formato (ZIP válido / archivo de texto).",
|
|||
|
|
},
|
|||
|
|
analysisFailed: {
|
|||
|
|
de: "Die Analyse ist fehlgeschlagen.",
|
|||
|
|
en: "The analysis failed.",
|
|||
|
|
es: "El análisis falló.",
|
|||
|
|
},
|
|||
|
|
noDownloadableFiles: {
|
|||
|
|
de: "Für dieses Skill sind keine herunterladbaren Dateien gespeichert.",
|
|||
|
|
en: "No downloadable files are stored for this skill.",
|
|||
|
|
es: "No hay archivos descargables almacenados para esta skill.",
|
|||
|
|
},
|
|||
|
|
onlyPassedDownloadable: {
|
|||
|
|
de: "Nur Skills mit dem Ergebnis „Bestanden“ können heruntergeladen werden.",
|
|||
|
|
en: "Only skills with a “Passed” result can be downloaded.",
|
|||
|
|
es: "Solo se pueden descargar las skills con resultado «Aprobado».",
|
|||
|
|
},
|
|||
|
|
descriptionFailed: {
|
|||
|
|
de: "Die Beschreibung konnte nicht erzeugt werden. Bitte Provider-Konfiguration und KI-Prompts prüfen.",
|
|||
|
|
en: "The description could not be generated. Please check the provider configuration and AI prompts.",
|
|||
|
|
es: "No se pudo generar la descripción. Compruebe la configuración del proveedor y los prompts de IA.",
|
|||
|
|
},
|
|||
|
|
noApiTokenPlain: {
|
|||
|
|
de: "Kein API-Token hinterlegt.",
|
|||
|
|
en: "No API token stored.",
|
|||
|
|
es: "No hay ningún token de API almacenado.",
|
|||
|
|
},
|
|||
|
|
noApiTokenProvided: {
|
|||
|
|
de: "Kein API-Token angegeben.",
|
|||
|
|
en: "No API token provided.",
|
|||
|
|
es: "No se proporcionó ningún token de API.",
|
|||
|
|
},
|
|||
|
|
modelsLoadFailed: {
|
|||
|
|
de: "Modelle konnten nicht geladen werden.",
|
|||
|
|
en: "Models could not be loaded.",
|
|||
|
|
es: "No se pudieron cargar los modelos.",
|
|||
|
|
},
|
|||
|
|
connSuccessReply: {
|
|||
|
|
de: "Verbindung erfolgreich. Antwort: {reply}",
|
|||
|
|
en: "Connection successful. Reply: {reply}",
|
|||
|
|
es: "Conexión correcta. Respuesta: {reply}",
|
|||
|
|
},
|
|||
|
|
connSuccessModels: {
|
|||
|
|
de: "Verbindung erfolgreich. {count} Modelle verfügbar.",
|
|||
|
|
en: "Connection successful. {count} models available.",
|
|||
|
|
es: "Conexión correcta. {count} modelos disponibles.",
|
|||
|
|
},
|
|||
|
|
connSuccessNoModels: {
|
|||
|
|
de: "Verbindung erfolgreich. Es wurden keine Modelle gefunden – bitte das Modell manuell eingeben.",
|
|||
|
|
en: "Connection successful. No models were found – please enter the model manually.",
|
|||
|
|
es: "Conexión correcta. No se encontraron modelos: introduzca el modelo manualmente.",
|
|||
|
|
},
|
|||
|
|
connReplyEmpty: {
|
|||
|
|
de: "(leer)",
|
|||
|
|
en: "(empty)",
|
|||
|
|
es: "(vacío)",
|
|||
|
|
},
|
|||
|
|
connFailed: {
|
|||
|
|
de: "Verbindung fehlgeschlagen.",
|
|||
|
|
en: "Connection failed.",
|
|||
|
|
es: "La conexión falló.",
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export function t(
|
|||
|
|
key: MessageKey,
|
|||
|
|
lang: Lang,
|
|||
|
|
vars?: Record<string, string>,
|
|||
|
|
): string {
|
|||
|
|
let msg = MESSAGES[key][lang];
|
|||
|
|
if (vars) {
|
|||
|
|
for (const [k, v] of Object.entries(vars)) {
|
|||
|
|
msg = msg.replace(`{${k}}`, v);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return msg;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Phrases injected into AI prompts so the model produces output in the
|
|||
|
|
// requested language. The directive overrides any language line baked into the
|
|||
|
|
// stored prompts (which historically said "Antworte auf Deutsch.").
|
|||
|
|
const LANGUAGE_DIRECTIVE: Record<Lang, string> = {
|
|||
|
|
de: "WICHTIG: Verfasse alle Ausgabetexte (Beschreibung, Findings, Empfehlungen) ausschließlich auf Deutsch.",
|
|||
|
|
en: "IMPORTANT: Write all output text (description, findings, remediation) exclusively in English.",
|
|||
|
|
es: "IMPORTANTE: Redacta todos los textos de salida (descripción, hallazgos, recomendaciones) exclusivamente en español.",
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export function languageDirective(lang: Lang): string {
|
|||
|
|
return LANGUAGE_DIRECTIVE[lang];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Resolve the language for a request's user-facing error messages. Prefers an
|
|||
|
|
// explicit `?lang=` query param, then the `Accept-Language` header (the web
|
|||
|
|
// client sends the active UI language), defaulting to German.
|
|||
|
|
export function reqLang(req: Request): Lang {
|
|||
|
|
const q = req.query?.lang;
|
|||
|
|
if (typeof q === "string" && q) return normalizeLang(q);
|
|||
|
|
const header = req.headers["accept-language"];
|
|||
|
|
if (typeof header === "string" && header) {
|
|||
|
|
return normalizeLang(header.split(",")[0].trim().slice(0, 2));
|
|||
|
|
}
|
|||
|
|
return "de";
|
|||
|
|
}
|