fix: standardize Spanish user-facing copy

This commit is contained in:
2026-10-05 10:24:33 -03:00
parent dfc1f663c7
commit cdbff5cb0e
8 changed files with 204 additions and 201 deletions
+47 -47
View File
@@ -627,15 +627,15 @@ function saveTools() {
/** @type {Record<AppState, { caption: string; disabled: boolean }>} */
const STATE_VIEWS = {
idle: { caption: "Tap or say \"Hola Vocero\"", disabled: false },
connecting: { caption: "Connecting", disabled: true },
queued: { caption: "Finding you a spot…", disabled: true },
"your-turn": { caption: "You're up! 🎉", disabled: true },
idle: { caption: "Toca o di \"Hola Vocero\"", disabled: false },
connecting: { caption: "Conectando", disabled: true },
queued: { caption: "Buscando un lugar para ti…", disabled: true },
"your-turn": { caption: "¡Es tu turno! 🎉", disabled: true },
listening: { caption: "", disabled: false },
"user-speaking": { caption: "", disabled: false },
processing: { caption: "", disabled: false },
"ai-speaking": { caption: "", disabled: false },
error: { caption: "Tap to retry", disabled: false },
error: { caption: "Toca para reintentar", disabled: false },
};
/** @type {Record<AppState, string>} */
@@ -885,11 +885,11 @@ function pushCustomRagToLiveSessionIfAny() {
function syncRagHintFooter() {
if (!ragHint) return;
if (!customRagLoaded || !customRagText.trim()) {
ragHint.textContent = "Nessun RAG context caricato. Premi \"Invia nella sessione\" per applicare le modifiche.";
ragHint.textContent = "No hay contexto RAG cargado. Pulsa \"Enviar a la sesión\" para aplicar los cambios.";
return;
}
const lines = customRagText.split(/\r?\n/).length;
ragHint.textContent = `✅ RAG context caricato: ${customRagText.length} chars / ${lines} linee. Viene aggiunto a istruzioni LLM e user_context.`;
ragHint.textContent = `✅ Contexto RAG cargado: ${customRagText.length} caracteres / ${lines} líneas. Se añade a las instrucciones del modelo y a user_context.`;
}
// Dedupe cache for [INJECT-PROFILE] browser logs (avoid 20 logs per click).
let _lastInjectProfileLogKey = "";
@@ -1073,7 +1073,7 @@ function buildProfileRows(profile) {
"email", "emailAddress", "email_address", "eMail", "mail", "contactEmail",
"user_email", "customer_email", "loginEmail", "username_email",
]);
if (email) rows.push({ label: "Email", value: email, isEmail: true, href: `mailto:${email}` });
if (email) rows.push({ label: "Correo electrónico", value: email, isEmail: true, href: `mailto:${email}` });
// Phone (linkable tel)
const phone = firstProfileField(profile, [
@@ -1086,33 +1086,33 @@ function buildProfileRows(profile) {
const username = firstProfileField(profile, [
"username", "userName", "user_name", "login", "handle", "nickname", "screenName", "accountName",
]);
if (username) rows.push({ label: "Username", value: username, mono: true });
if (username) rows.push({ label: "Nombre de usuario", value: username, mono: true });
// Role / Plan
const role = firstProfileField(profile, [
"role", "roleName", "userRole", "plan", "planName", "tier", "subscription", "membership", "accessLevel",
]);
if (role) rows.push({ label: "Ruolo / Piano", value: role });
if (role) rows.push({ label: "Rol / Plan", value: role });
// Company / Organization
const company = firstProfileField(profile, [
"company", "companyName", "company_name", "organization", "org", "organizationName", "business",
]);
if (company) rows.push({ label: "Azienda", value: company });
if (company) rows.push({ label: "Empresa", value: company });
// User/Account ID (mono for monospace copy-paste)
const userId = firstProfileField(profile, [
"userId", "user_id", "uid", "accountId", "account_id", "sub", "customerId", "customer_id",
"id", "userPk", "profileId", "profile_id", "identityId",
]);
if (userId) rows.push({ label: "ID Utente", value: userId, mono: true });
if (userId) rows.push({ label: "ID de usuario", value: userId, mono: true });
// Locale / Country
const country = firstProfileField(profile, [
"country", "countryName", "country_code", "countryCode", "locale", "language", "lang",
"region", "timezone", "tz",
]);
if (country) rows.push({ label: "Nazione / Lingua", value: country });
if (country) rows.push({ label: "País / Idioma", value: country });
// Created at / signed up date — try common date fields.
const createdAtKeys = [
@@ -1130,7 +1130,7 @@ function buildProfileRows(profile) {
}
}
}
if (createdAt) rows.push({ label: "Iscritto il", value: createdAt, isDate: true });
if (createdAt) rows.push({ label: "Fecha de registro", value: createdAt, isDate: true });
// Last sign in / active date (last_login etc)
const lastActiveKeys = [
@@ -1146,7 +1146,7 @@ function buildProfileRows(profile) {
}
}
}
if (lastActive) rows.push({ label: "Ultimo accesso", value: lastActive, isDate: true });
if (lastActive) rows.push({ label: "Último acceso", value: lastActive, isDate: true });
return rows;
}
@@ -1160,16 +1160,16 @@ function populateUserProfileDialog(profile) {
const initials = initialsForName(name);
if (userProfileAvatar) {
userProfileAvatar.textContent = initials || "??";
userProfileAvatar.setAttribute("aria-label", name ? `Avatar: ${name}` : "Utente non impostato");
userProfileAvatar.setAttribute("aria-label", name ? `Avatar: ${name}` : "Usuario no identificado");
}
if (userProfileTitle) {
userProfileTitle.textContent = name || "Utente non impostato";
userProfileTitle.textContent = name || "Usuario no identificado";
}
if (userProfileSubtitle) {
const email = firstProfileField(profile, ["email", "emailAddress", "email_address", "eMail", "mail"]);
const phone = firstProfileField(profile, ["phone", "phoneNumber", "phone_number", "mobile", "telephone", "tel"]);
const parts = [email, phone].filter(Boolean);
userProfileSubtitle.textContent = parts.length ? parts.join(" · ") : (name ? "Nessuna informazione di contatto disponibile." : "Connettersi a Vocero per vedere i dati utente.");
userProfileSubtitle.textContent = parts.length ? parts.join(" · ") : (name ? "No hay información de contacto disponible." : "Conéctate a Vocero para ver los datos del usuario.");
}
if (userProfileMetadata) {
userProfileMetadata.innerHTML = "";
@@ -1239,8 +1239,8 @@ function syncVoceroHeaderUser() {
// Show avatar with the 2-letter initials. Full name is reachable via hover
// tooltip (title) and is used by screen readers via aria-label.
identUserName.textContent = initials;
identUserName.title = `Utente: ${name}`;
identUserName.setAttribute("aria-label", `Utente: ${name}`);
identUserName.title = `Usuario: ${name}`;
identUserName.setAttribute("aria-label", `Usuario: ${name}`);
} else {
identUserName.textContent = "";
identUserName.removeAttribute("title");
@@ -1301,7 +1301,7 @@ function syncVoceroHeaderUser() {
badge1.textContent = `${count} programados`;
const badge2 = document.createElement("span");
badge2.className = "reports-summary-badge";
badge2.textContent = `Bloque LLM ${totalLen.toLocaleString()} chars`;
badge2.textContent = `Bloque LLM ${totalLen.toLocaleString()} caracteres`;
titleRow.appendChild(titleText);
titleRow.appendChild(badge1);
titleRow.appendChild(badge2);
@@ -1907,7 +1907,7 @@ function _kickIdleTimer(reason = "activity") {
idleCloseTimer = 0;
// Friendly caption flash right before teardown moves us back to the idle
// copy (so the user understands *why* the call ended).
setCaption("Chiuso per inattività", "muted");
setCaption("Sesión cerrada por inactividad", "muted");
void (async () => {
try {
await teardown();
@@ -1917,7 +1917,7 @@ function _kickIdleTimer(reason = "activity") {
// teardown() calls setState("idle") which restores the normal idle
// caption. Leave a visible breadcrumb on the subcaption briefly so the
// user can tell what happened instead of wondering why the orb reset.
circleSubcaption.textContent = "Sessione chiusa dopo 30 secondi di silenzio. Tocca l'orb o ripeti «Hola Vocero» per iniziare di nuovo.";
circleSubcaption.textContent = "Sesión cerrada tras 30 segundos de silencio. Toca el círculo o repite «Hola Vocero» para empezar de nuevo.";
circleSubcaption.hidden = false;
window.setTimeout(() => { circleSubcaption.hidden = true; }, 8000);
})();
@@ -1968,7 +1968,7 @@ function setState(next) {
// Warm reassurance under the terse position, only while waiting in line.
if (next === "queued") {
circleSubcaption.textContent =
"Sorry, we overhugged! 🤗 Every slot is busy, so we saved you a spot. Hang tight, you're moving up.";
"¡Nos pasamos de abrazos! 🤗 Todos los lugares están ocupados, pero guardamos uno para ti en la cola. Ya estás avanzando.";
circleSubcaption.hidden = false;
} else {
circleSubcaption.hidden = true;
@@ -2003,8 +2003,8 @@ function updateRestartAvailability() {
currentState === "connecting" || currentState === "queued" || currentState === "your-turn";
restartHint.hidden = false;
restartHint.textContent = LIVE_STATES.has(currentState)
? "Reconnects now with the settings above."
: "Starts a conversation with the settings above.";
? "Reconecta ahora con la configuración indicada."
: "Inicia una conversación con la configuración indicada.";
}
/**
@@ -2031,7 +2031,7 @@ function openSettings() {
inputInstructions.readOnly = !!pinnedPrompt;
primaryPromptMeta.hidden = !pinnedPrompt;
primaryPromptMeta.textContent = pinnedPrompt
? "Primary prompt pinned by this deployment."
? "Instrucciones principales fijadas por este despliegue."
: "";
syncGateUi();
updateRestartAvailability();
@@ -2118,7 +2118,7 @@ function setGateThreshold(db) {
settings.noiseGate = Math.min(GATE_MAX_DB, Math.max(GATE_OFF_DB, Math.round(db)));
const off = settings.noiseGate <= GATE_OFF_DB;
inputNoiseGate.value = String(settings.noiseGate);
gateValue.textContent = off ? "Off" : `${settings.noiseGate} dB`;
gateValue.textContent = off ? "Desactivada" : `${settings.noiseGate} dB`;
renderGateHandle();
localStorage.setItem(STORAGE_KEYS.noiseGate, String(settings.noiseGate));
if (client && LIVE_STATES.has(currentState)) {
@@ -2130,7 +2130,7 @@ function setGateThreshold(db) {
function syncGateUi() {
inputNoiseGate.value = String(settings.noiseGate);
const off = settings.noiseGate <= GATE_OFF_DB;
gateValue.textContent = off ? "Off" : `${settings.noiseGate} dB`;
gateValue.textContent = off ? "Desactivada" : `${settings.noiseGate} dB`;
renderGateHandle();
}
@@ -2340,8 +2340,8 @@ searchKeyInput.addEventListener("input", () => {
pushToolsToSession();
}
toolWebHint.textContent = userSearchKey
? "Using your key — stored in this browser only."
: "No server key configured. Add your own Serper key to enable web search.";
? "Se usa tu clave, guardada solo en este navegador."
: "No hay una clave del servidor configurada. Añade tu clave de Serper para activar la búsqueda web.";
});
async function runTool(name, argsJson, callId) {
@@ -2936,7 +2936,7 @@ function connectionTarget() {
}
const directUrl = buildDirectWsUrl(pinnedUrl || settings.directUrl);
if (!directUrl) {
throw new Error("Enter a speech-to-speech server URL in Settings.");
throw new Error("Introduce la URL del servidor de voz en Configuración.");
}
return { directUrl };
}
@@ -3022,16 +3022,16 @@ function syncConnectionUi() {
inputLbUrl.value = pinnedUrl;
inputLbUrl.readOnly = true;
connHint.classList.remove("error");
connHint.textContent = "Speech-to-speech server URL pinned by this deployment.";
connHint.textContent = "URL del servidor de voz fijada por este despliegue.";
} else if (allowDirect) {
// Direct mode: the user sets their own s2s server URL.
connField.hidden = false;
inputLbUrl.value = settings.directUrl;
inputLbUrl.readOnly = false;
inputLbUrl.placeholder = "http://localhost:port";
inputLbUrl.placeholder = "http://localhost:8080";
connHint.classList.remove("error");
connHint.textContent =
"URL of your speech-to-speech server, e.g. http://localhost:8080 (the app adds /v1/realtime).";
"URL de tu servidor de voz, por ejemplo http://localhost:8080 (la aplicación añade /v1/realtime).";
} else {
// LB mode: the load balancer URL is deployment-owned — hide it entirely so
// its address is never exposed in Settings.
@@ -3049,7 +3049,7 @@ function missingServerUrl() {
function promptServerUrl() {
if (settingsModal.open) syncConnectionUi();
else openSettings();
connHint.textContent = "Set the speech-to-speech server URL to start.";
connHint.textContent = "Configura la URL del servidor de voz para empezar.";
connHint.classList.add("error");
inputLbUrl.focus();
}
@@ -3127,8 +3127,8 @@ async function handleStartError(err) {
setState("error");
setCaption(
err.code === "join-expired"
? "Your spot expired. Tap to rejoin."
: "That took a while. Tap to rejoin.",
? "Tu lugar ha caducado. Toca para volver a la cola."
: "La espera se ha alargado. Toca para volver a la cola.",
"error",
);
return;
@@ -3144,8 +3144,8 @@ micBtn.addEventListener("click", () => {
}
client.setMuted(micMuted);
micBtn.classList.toggle("muted", micMuted);
micBtn.setAttribute("aria-label", micMuted ? "Unmute" : "Mute");
micBtn.title = micMuted ? "Unmute" : "Mute";
micBtn.setAttribute("aria-label", micMuted ? "Activar micrófono" : "Silenciar micrófono");
micBtn.title = micMuted ? "Activar micrófono" : "Silenciar micrófono";
});
stopBtn.addEventListener("click", async () => {
@@ -3178,7 +3178,7 @@ async function primeMicPermission() {
for (const track of s.getTracks()) track.stop();
} catch (err) {
throw new Error(
`Microphone access denied${err instanceof Error ? `: ${err.message}` : ""}`,
`Acceso al micrófono denegado${err instanceof Error ? `: ${err.message}` : ""}`,
);
}
}
@@ -3193,7 +3193,7 @@ async function acquireMicStream() {
/** @param {number} position Update the queued caption ("You're #N in line"). */
function onQueuePosition(position) {
const n = Number(position) || 0;
setCaption(n > 0 ? `You're #${n} in line` : "Finding you a spot…", "muted");
setCaption(n > 0 ? `Estás en el puesto ${n} de la cola` : "Buscando un lugar para ti…", "muted");
}
// ── "Your turn" join countdown ──────────────────────────────────────────────
@@ -3206,14 +3206,14 @@ function startJoinCountdown(sec) {
stopJoinCountdown();
let left = Math.max(0, Math.floor(sec));
const paint = () => {
joinQueueBtn.textContent = left > 0 ? `Join now (${left}s)` : "Join now";
joinQueueBtn.textContent = left > 0 ? `Entrar ahora (${left}s)` : "Entrar ahora";
};
paint();
joinCountdownTimer = window.setInterval(() => {
left -= 1;
if (left <= 0) {
stopJoinCountdown();
joinQueueBtn.textContent = "Join now";
joinQueueBtn.textContent = "Entrar ahora";
return;
}
paint();
@@ -3241,7 +3241,7 @@ async function doStart(audioContext = null) {
chat.clear();
chat.reset();
setState("connecting");
setCaption("Asking for mic…", "muted");
setCaption("Solicitando acceso al micrófono…", "muted");
// Create + resume the AudioContext SYNCHRONOUSLY, still inside the gesture.
// iOS Safari only starts an AudioContext from a user gesture; if we waited
@@ -3644,7 +3644,7 @@ async function tryTalkFirstDirect() {
"color:#2563eb;font-weight:700;background:#dbeafe;padding:3px 6px;border-radius:4px;"
);
try {
setCaption("Preparing your session…", "muted");
setCaption("Preparando tu sesión…", "muted");
// Let _ensureAudioContext create the context. If it stays suspended, the
// client resumes it on the first audio or the next user click.
await doStart();
@@ -3691,7 +3691,7 @@ async function startTalkFromGesture() {
// This user gesture permits AudioContext and getUserMedia to start.
console.info("[start-session] 🎤 start doStart() from unlocker gesture (TALK mode)");
const audioContext = createResumedAudioContext();
setCaption("Preparing your session…", "muted");
setCaption("Preparando tu sesión…", "muted");
await doStart(audioContext);
} catch (err) {
console.error("[start-session] ⛔ doStart() from unlocker failed", err);