From f8e96e1484afe1bce4644b008a3f319c3f27b6ef Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Thu, 29 Oct 2026 14:54:44 +0400 Subject: [PATCH] i18n(it): address code review findings Localize a bootstrap shot title without a source pack only when the Nepal pack marker binds that title to the shot's id, so an operator shot renamed to a bootstrap title in the built-in scene stays as typed. Co-Authored-By: Claude Opus 5.5 --- src/scenes/bootstrapTitleProvenance.test.mjs | 20 +++++++++++++++ src/scenes/director.test.mjs | 4 +++ src/scenes/recipes.js | 27 ++++++++++++++------ src/scenes/sceneTitleDisplay.test.mjs | 16 ++++++++++-- 4 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 src/scenes/bootstrapTitleProvenance.test.mjs diff --git a/src/scenes/bootstrapTitleProvenance.test.mjs b/src/scenes/bootstrapTitleProvenance.test.mjs new file mode 100644 index 000000000..0d402dbbd --- /dev/null +++ b/src/scenes/bootstrapTitleProvenance.test.mjs @@ -0,0 +1,20 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { SCENE_RECIPES, displayShotTitle } from './recipes.js'; +import { setLocale } from '../i18n/index.js'; + +test('an operator-typed title in the built-in Nepal scene stays as typed', () => { + const scene = SCENE_RECIPES.find((r) => r.id === 'bhote-koshi-nepal-scene'); + assert.ok(scene, 'built-in bootstrap scene exists'); + for (const locale of ['ru', 'es', 'it', 'zh-CN', 'zh-Hant']) { + setLocale(locale); + const title = 'Global Incident Context'; + // A shot captured by the operator: no sourcePackId, renamed to this title. + const shown = displayShotTitle( + title, + { id: 'shot-user', title }, + { id: scene.id }, + ); + assert.equal(shown, title, `${locale}: got ${shown}`); + } +}); diff --git a/src/scenes/director.test.mjs b/src/scenes/director.test.mjs index fd8db6149..7b59507be 100644 --- a/src/scenes/director.test.mjs +++ b/src/scenes/director.test.mjs @@ -712,6 +712,10 @@ test('a legacy three-shot Nepal browser project bootstraps to the current 25-sho assert.deepEqual(scene.shots.slice(0, 3).map(({ camera }) => camera), originalCameras); assert.equal(scene.appliedShotPacks[0].id, 'bhote-koshi-nepal-evidence-pack'); assert.equal(scene.appliedShotPacks[0].version, 18); + // displayShotTitle trusts these bindings to localize the bootstrap shots. + for (const shot of scene.shots.slice(0, 8)) { + assert.equal(scene.appliedShotPacks[0].shotBindings[shot.title], shot.id); + } assert.equal(director._selectedSceneId, scene.id); assert.equal(director._selectedShotId, scene.shots[0].id); } finally { diff --git a/src/scenes/recipes.js b/src/scenes/recipes.js index 1a886c541..37eed431b 100644 --- a/src/scenes/recipes.js +++ b/src/scenes/recipes.js @@ -1061,33 +1061,44 @@ export function displaySceneTitle(scene) { * (`sourcePackId`), so a typed title that matches a built-in one stays as * typed. Without `shot`, the title alone decides. * @param {string} title - * @param {{sourcePackId?: string}} [shot] - * @param {{id?: string}} [scene] The shot's scene, for projects saved before - * shots recorded their source pack. + * @param {{id?: string, sourcePackId?: string}} [shot] + * @param {{id?: string, appliedShotPacks?: Array<{id?: string, shotBindings?: Object}>}} [scene] + * The shot's scene, for projects saved before shots recorded their source + * pack. * @returns {string} */ export function displayShotTitle(title, shot = undefined, scene = undefined) { const text = typeof title === 'string' ? title : ''; const fromRecipe = shot - ? Boolean(shot.sourcePackId) || recipeDeclaresShotTitle(scene?.id, text) + ? Boolean(shot.sourcePackId) || recipeDeclaresShotTitle(scene, shot, text) : true; if (fromRecipe && BUILTIN_SHOT_TITLES.has(text)) return t(text); const numbered = /^Shot ([1-9]\d*)$/.exec(text); return numbered ? t('Shot {n}', { n: numbered[1] }) : text; } -/** Whether the built-in recipe behind a scene declares a shot with this title. */ -function recipeDeclaresShotTitle(sceneId, title) { - const recipe = sceneId ? getSceneRecipeById(sceneId) : null; +/** Whether the built-in recipe behind a scene declares this shot's title. */ +function recipeDeclaresShotTitle(scene, shot, title) { + const recipe = scene?.id ? getSceneRecipeById(scene.id) : null; if (!recipe) return false; const declares = (path) => Array.isArray(path) && path.some((keyframe) => keyframe?.title === title); if (declares(recipe.cameraPath)) return true; // A bootstrap recipe seeds legacy "Shot N" keyframes; the upgrade replaces // them with the append recipe's bootstrap shots, which carry no source pack. + // The pack marker binds each of those titles to its shot id, so a shot the + // operator later gave the same title stays as typed. return SCENE_APPEND_RECIPES.some( (append) => append.legacySceneBootstrap?.targetSceneTitle === recipe.title && - declares(append.legacySceneBootstrap.cameraPath), + declares(append.legacySceneBootstrap.cameraPath) && + Boolean(shot?.id) && + (Array.isArray(scene.appliedShotPacks) + ? scene.appliedShotPacks + : [] + ).some( + (marker) => + marker?.id === append.id && marker.shotBindings?.[title] === shot.id, + ), ); } diff --git a/src/scenes/sceneTitleDisplay.test.mjs b/src/scenes/sceneTitleDisplay.test.mjs index 758800137..c476a2000 100644 --- a/src/scenes/sceneTitleDisplay.test.mjs +++ b/src/scenes/sceneTitleDisplay.test.mjs @@ -155,11 +155,23 @@ test('bootstrap shots of a built-in scene localize without a source pack', () => ); if (!scene) return; for (const { title } of bootstrap.cameraPath) { - // The upgrade copies these keyframes into the scene without sourcePackId. + // The upgrade copies these keyframes into the scene without sourcePackId + // and binds each title to its shot in the pack marker. + const upgraded = { + id: scene.id, + appliedShotPacks: [ + { id: append.id, version: 1, shotBindings: { [title]: 'shot-old' } }, + ], + }; assert.equal( - displayShotTitle(title, { id: 'shot-old', title }, { id: scene.id }), + displayShotTitle(title, { id: 'shot-old', title }, upgraded), displayShotTitle(title), ); + // Another shot the operator gave the same title stays as typed. + assert.equal( + displayShotTitle(title, { id: 'shot-user', title }, upgraded), + title, + ); // The same title in an operator's own scene stays as typed. assert.equal( displayShotTitle(title, { id: 'shot-user', title }, { id: 'scene-user' }),