polish(#3220): esc() download label + scrub orphaned media_open i18n keys

Opus iter2 SHOULD-FIX (both non-blocking, applied for cleanliness):
- esc() the media_download title/aria-label so a future translator's quote
  can't break out of the attribute (defense-in-depth).
- Remove the now-dead media_open key from all 11 locales + the ui.js fallback
  dict (the Open button was dropped in the clean-image redesign).
This commit is contained in:
nesquena-hermes 2026-06-01 23:03:48 +00:00
parent 8ce02caf11
commit 031a3ce2e9
2 changed files with 3 additions and 13 deletions

View file

@ -1297,7 +1297,6 @@ const LOCALES = {
media_audio_label: 'Audio',
media_svg_label: 'Diagram',
media_video_label: 'Video',
media_open: 'Open',
media_download: 'Download',
csv_loading: 'Loading CSV',
csv_too_large: 'CSV file too large for inline rendering',
@ -2616,7 +2615,6 @@ const LOCALES = {
media_audio_label: 'Audio',
media_svg_label: 'Diagramma',
media_video_label: 'Video',
media_open: 'Apri',
media_download: 'Scarica',
csv_loading: 'Caricamento CSV',
csv_too_large: 'File CSV troppo grande per il rendering inline',
@ -3940,7 +3938,6 @@ const LOCALES = {
media_audio_label: '音声',
media_svg_label: '図',
media_video_label: '動画',
media_open: '開く',
media_download: 'ダウンロード',
csv_loading: 'CSV を読み込み中',
csv_too_large: 'CSV ファイルが大きすぎてインライン表示できません',
@ -5136,7 +5133,6 @@ const LOCALES = {
media_audio_label: 'Аудио',
media_svg_label: 'Диаграмма',
media_video_label: 'Видео',
media_open: 'Открыть',
media_download: 'Скачать',
csv_loading: 'Загрузка CSV',
csv_too_large: 'CSV-файл слишком большой для встроенного отображения',
@ -6380,7 +6376,6 @@ const LOCALES = {
media_audio_label: 'Audio',
media_svg_label: 'Diagrama',
media_video_label: 'Vídeo',
media_open: 'Abrir',
media_download: 'Descargar',
csv_loading: 'Cargando CSV',
csv_too_large: 'Archivo CSV demasiado grande para mostrar en línea',
@ -7638,7 +7633,6 @@ const LOCALES = {
media_audio_label: 'Audio',
media_svg_label: 'Diagramm',
media_video_label: 'Video',
media_open: 'Öffnen',
media_download: 'Herunterladen',
csv_loading: 'CSV wird geladen',
csv_too_large: 'CSV-Datei zu groß für Inline-Anzeige',
@ -8901,7 +8895,6 @@ const LOCALES = {
media_audio_label: '音频',
media_svg_label: '图表',
media_video_label: '视频',
media_open: '打开',
media_download: '下载',
csv_loading: '加载 CSV',
csv_too_large: 'CSV 文件过大,无法内联渲染',
@ -10217,7 +10210,6 @@ const LOCALES = {
media_audio_label: '音訊',
media_svg_label: '圖表',
media_video_label: '影片',
media_open: '開啟',
media_download: '下載',
csv_loading: '載入 CSV',
csv_too_large: 'CSV 檔案過大,無法內嵌渲染',
@ -12673,7 +12665,6 @@ const LOCALES = {
media_audio_label: '오디오',
media_svg_label: '다이어그램',
media_video_label: '비디오',
media_open: '열기',
media_download: '다운로드',
csv_loading: 'CSV 로딩 중',
csv_too_large: 'CSV 파일이 너무 커서 인라인 렌더링할 수 없습니다',
@ -13895,7 +13886,6 @@ const LOCALES = {
media_audio_label: 'Audio',
media_svg_label: 'Diagramme',
media_video_label: 'Vidéo',
media_open: 'Ouvrir',
media_download: 'Télécharger',
csv_loading: 'Chargement du fichier CSV',
csv_too_large: 'Fichier CSV trop volumineux pour le rendu en ligne',
@ -15245,7 +15235,6 @@ const LOCALES = {
media_audio_label: 'Ses',
media_svg_label: 'Diyagram',
media_video_label: 'Video',
media_open: 'Aç',
media_download: 'İndir',
csv_loading: 'CSV yükleniyor',
csv_too_large: 'CSV dosyası satır içi oluşturma için çok büyük',

View file

@ -3507,14 +3507,15 @@ function renderMd(raw){
const localArtifactCard=(src,name)=>{
const safeSrc=esc(src);
const safeName=esc(name||'image');
const tt=(typeof t==='function')?t:(key=>({media_open:'Open',media_download:'Download'}[key]||key));
const tt=(typeof t==='function')?t:(key=>({media_download:'Download'}[key]||key));
// Clean inline image (keeps the existing .msg-media-img lightbox-on-click
// behavior) with a hover/focus-revealed Download action overlaid top-right,
// matching the ChatGPT/Claude/Gemini pattern. The image stays the hero —
// no permanent card chrome. Download is the one affordance the lightbox
// (zoom-on-click) doesn't already provide.
const dlLabel=esc(tt('media_download'));
const dlSvg='<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>';
return `<span class="msg-artifact-image"><img class="msg-media-img" src="${safeSrc}" alt="${safeName}" loading="lazy"><a class="msg-artifact-download" href="${safeSrc}" download="${safeName}" title="${tt('media_download')}" aria-label="${tt('media_download')}" onclick="event.stopPropagation()">${dlSvg}</a></span>`;
return `<span class="msg-artifact-image"><img class="msg-media-img" src="${safeSrc}" alt="${safeName}" loading="lazy"><a class="msg-artifact-download" href="${safeSrc}" download="${safeName}" title="${dlLabel}" aria-label="${dlLabel}" onclick="event.stopPropagation()">${dlSvg}</a></span>`;
};
if(/^file:\/\//i.test(ref)){
try{