fix: standardize Spanish user-facing copy
This commit is contained in:
+47
-47
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user