i18n: fix review findings and polish ru/es strings

Fixes the 51 P0–P2 findings from the full review of the i18n branch and
the issues found while checking every new string on screen in ru and es.

- Translate errors and statuses that still reached ru/es in English:
  feed and tile errors, provider statuses, the ALPR chip, the traffic
  mode chip, voice plan step labels, fire feed values, receiver bands,
  scene save reasons and the Cesium attribution link.
- Show preset city names in the UI language (src/ui/placeNames.js);
  data and everything sent to the model stay English.
- Share layer names between the layer panel, HUD and voice card
  (src/ui/layerNames.js).
- Keep long ru/es strings whole: voice card error lines, analyst
  titles, the voice tier status, the Nepal evidence button, the ALPR
  retry chip. English layout is unchanged.
- Ask before a language switch drops unsaved work.
- Catalog fixes: glossary terms (пуск, пресет), gender agreement and
  consistent "peticiones" in es, fewer calques in both languages.
- Fix English text that was itself wrong (e.g. "Exit cockpit to fly to
  a weather", raw tool ids in voice plan steps).

Catalogs: es/ru 2535 messages each, complete.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-15 23:49:52 +04:00
1 parent 8029747b7e
commit 00704ddc89
212 files changed
+9871 -909

No files matched your search

+126 -20
View File
@@ -54,6 +54,74 @@ export function collectKeyUpdates(fields) {
return updates;
}
/**
* Why /api/setup/keys refused a save, by the `code` the server sends with its
* English `error` (src/keySetupCore.mjs, server/standalone/key-setup.js). The
* English here is the server's own sentence, so the default locale reads the
* same as before.
*/
const SAVE_REFUSALS = Object.freeze({
proxied: msg('Provider Settings does not answer proxied requests'),
sharing: msg('Provider Settings is disabled while sharing is enabled'),
'not-loopback': msg(
'Provider Settings answers only the machine running the server',
),
'not-local-host': msg('Provider Settings answers only local hostnames'),
'origin-required': msg('Provider Settings requires an exact local Origin'),
'origin-unrecognized': msg('Unrecognized Origin refused'),
'cross-origin': msg('Cross-origin requests are refused'),
'content-type': msg('Content-Type must be application/json'),
method: msg('Method not allowed'),
'too-large': msg('Request too large'),
'invalid-json': msg('Invalid JSON'),
'not-object': msg('Body must be a JSON object of {ENV_VAR: value}'),
'no-keys': msg('No keys provided'),
'too-many': msg('At most {limit} keys per save'),
'unknown-key': msg('Unknown key: {name}'),
'not-string': msg('{name} must be a string'),
empty: msg('{name} is empty'),
'too-long': msg('{name} is longer than any real key ({limit} max)'),
'not-printable': msg(
'{name} may only contain printable characters with no spaces',
),
'invalid-char': msg(
'{name} contains a character that is not valid in a key (#, quotes, $, \\, or backtick)',
),
external: msg(
'{name} is configured outside Provider Settings and can only be changed where it was set',
),
'harden-failed': msg(
'The key was not saved: could not restrict the credential file to your account; nothing was saved',
),
'store-unreadable': msg(
'The key was not saved: the existing configuration could not be read, so nothing was changed',
),
'write-failed': msg('Could not write the {store} store'),
});
/**
* The status line for a refused save — pure, exported for tests. A known
* `code` reads in the active language; anything else shows the server's text.
* @param {{code?: string, params?: object, error?: string}} payload
* @param {number} status HTTP status of the response.
* @returns {string}
*/
export function saveRefusalText(payload, status) {
const pattern = Object.hasOwn(SAVE_REFUSALS, payload?.code)
? SAVE_REFUSALS[payload.code]
: null;
if (pattern) {
const values = {};
for (const name of ['name', 'limit', 'store']) {
const value = payload.params?.[name];
if (typeof value === 'string' || typeof value === 'number')
values[name] = value;
}
return t(pattern, values);
}
return payload?.error || t('Save failed ({status}).', { status });
}
function sleepWithSignal(ms, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
@@ -72,6 +140,36 @@ function sleepWithSignal(ms, signal) {
});
}
/**
* An OAuth check error. A server-sent reason is kept as-is and later wrapped
* in «OAuth check failed: …»; when there is none, the error carries our own
* already-translated sentence and is flagged so the toast shows it unwrapped
* (the wrapper would only repeat it).
* @param {unknown} serverError
* @param {string} fallback Already-translated fallback sentence.
* @returns {Error}
*/
function oauthCheckError(serverError, fallback) {
if (serverError) return new Error(String(serverError));
const error = new Error(fallback);
error.oauthFallback = true;
return error;
}
/**
* Status text for a failed OAuth check — pure, exported for tests. A fallback
* sentence of our own already says what failed and is shown alone; any other
* reason (server text, network error) gets the «OAuth check failed» wrapper.
* @param {unknown} error
* @returns {string}
*/
export function oauthCheckFailureText(error) {
if (error?.oauthFallback && error.message) return error.message;
return t('OAuth check failed: {detail}', {
detail: error?.message || error,
});
}
export async function waitForChatGptOAuth({
fetchImpl,
signal,
@@ -90,8 +188,9 @@ export async function waitForChatGptOAuth({
if (signal?.aborted) return false;
if (response.ok && payload.available) return true;
if (!response.ok || payload.loginFailed) {
throw new Error(
payload.error || 'Could not check ChatGPT sign-in. Try again.',
throw oauthCheckError(
payload.error,
t('Could not check ChatGPT sign-in. Try again.'),
);
}
if (now() >= deadline) break;
@@ -415,11 +514,7 @@ export async function initKeySetup({
const payload = await response.json().catch(() => ({}));
if (disposed) return;
if (!response.ok || !payload.ok) {
// payload.error is a server-authored message (API value) — shown as-is.
say(
payload.error ||
t('Save failed ({status}).', { status: response.status }),
);
say(saveRefusalText(payload, response.status));
return;
}
for (const input of root.querySelectorAll('input[data-env-var]'))
@@ -496,12 +591,12 @@ export async function initKeySetup({
if (current === 'oauth') {
writeStoredCloudVoiceAuthMode('api-key');
syncCloudVoiceAuth();
say('Cloud voice will use OPENAI_API_KEY on the next session.');
say(t('Cloud voice will use OPENAI_API_KEY on the next session.'));
return;
}
oauthBusy = true;
syncCloudVoiceAuth();
say('Checking local ChatGPT OAuth sign-in…');
say(t('Checking local ChatGPT OAuth sign-in…'));
try {
const response = await doFetch('/api/realtime/oauth-status', {
cache: 'no-store',
@@ -510,22 +605,25 @@ export async function initKeySetup({
const payload = await response.json().catch(() => ({}));
if (lifetime.signal.aborted) return;
if (!response.ok)
throw new Error(
payload.error || 'Could not check ChatGPT sign-in.',
throw oauthCheckError(
payload.error,
t('Could not check ChatGPT sign-in.'),
);
if (payload.available) {
writeStoredCloudVoiceAuthMode('oauth');
syncCloudVoiceAuth();
say(
'ChatGPT OAuth selected for cloud voice. Your API key stays saved and available.',
t(
'ChatGPT OAuth selected for cloud voice. Your API key stays saved and available.',
),
);
return;
}
say(
payload.code === 'CODEX_OAUTH_REAUTH_REQUIRED'
? 'ChatGPT sign-in expired. Opening sign-in again…'
: 'Opening ChatGPT sign-in in your browser…',
? t('ChatGPT sign-in expired. Opening sign-in again…')
: t('Opening ChatGPT sign-in in your browser…'),
);
const loginResponse = await doFetch('/api/realtime/oauth-login', {
method: 'POST',
@@ -538,7 +636,7 @@ export async function initKeySetup({
say(
loginPayload.error ||
payload.error ||
'Could not start ChatGPT sign-in on this machine.',
t('Could not start ChatGPT sign-in on this machine.'),
);
return;
}
@@ -546,13 +644,17 @@ export async function initKeySetup({
writeStoredCloudVoiceAuthMode('oauth');
syncCloudVoiceAuth();
say(
'ChatGPT OAuth selected for cloud voice. Your API key stays saved and available.',
t(
'ChatGPT OAuth selected for cloud voice. Your API key stays saved and available.',
),
);
return;
}
say(
'Finish ChatGPT sign-in in the browser. Waiting for it to complete…',
t(
'Finish ChatGPT sign-in in the browser. Waiting for it to complete…',
),
);
const available = await waitForChatGptOAuth({
fetchImpl: doFetch,
@@ -561,18 +663,22 @@ export async function initKeySetup({
if (lifetime.signal.aborted) return;
if (!available) {
say(
'ChatGPT sign-in timed out. Click USE CHATGPT OAUTH to try again.',
t('ChatGPT sign-in timed out. Click {button} to try again.', {
button: t('USE CHATGPT OAUTH'),
}),
);
return;
}
writeStoredCloudVoiceAuthMode('oauth');
syncCloudVoiceAuth();
say(
'ChatGPT sign-in complete. OAuth will be used for the next cloud voice session.',
t(
'ChatGPT sign-in complete. OAuth will be used for the next cloud voice session.',
),
);
} catch (error) {
if (lifetime.signal.aborted) return;
say(`OAuth check failed: ${error?.message || error}`);
say(oauthCheckFailureText(error));
} finally {
oauthBusy = false;
if (!disposed) syncCloudVoiceAuth();