feat(editor): translate FileExplorer + FileTabs to 9 locales (Editor block 2)

FileExplorer (sidebar)
- Workspace header label and the new-workspace / save-project icon
  buttons now read from t('editor.fileExplorer.*').
- Per-board section: collapse / expand toggle, status dot tooltip
  (Running / Compiled / Idle), per-board "new file" button, and the
  composite "<board name> — click to edit" hover title (the board
  name itself stays untranslated — it's a product noun like
  "Arduino Uno").
- File rows: hover title with optional "(unsaved)" suffix,
  unsaved-dot tooltip, and the right-click context menu's Rename /
  Delete commands.
- Empty-state placeholder when no boards are on the canvas.
- The window.confirm() shown before deleting a file now reads from
  t() too, so non-English users see the prompt in their language.

FileTabs (open-tabs strip above the editor)
- Per-tab close button title and the unsaved-changes dot tooltip.
- Inline confirm dialog when closing a modified file: prompt copy,
  "Close anyway" and "Cancel" buttons.

Hand-translated for all 9 locales. Hotkey hints (Ctrl+S, Strg+S)
localised per German convention; other locales keep "Ctrl+S" as the
universally-recognised label.
This commit is contained in:
David Montero Crespo 2026-05-09 03:08:51 -03:00
parent 11012ec0e1
commit 7f0ac2a74c
11 changed files with 291 additions and 18 deletions

View File

@ -1,4 +1,5 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useEditorStore } from '../../store/useEditorStore';
import { useSimulatorStore } from '../../store/useSimulatorStore';
import type { BoardKind } from '../../types/board';
@ -151,6 +152,7 @@ interface FileExplorerProps {
}
export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewClick }) => {
const { t } = useTranslation();
const {
fileGroups,
activeFileId,
@ -244,7 +246,7 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
setContextMenu(null);
const files = fileGroups[groupId] ?? [];
if (files.length <= 1) return;
if (!window.confirm('Delete this file?')) return;
if (!window.confirm(t('editor.fileExplorer.confirmDelete'))) return;
deleteFile(fileId);
};
@ -270,18 +272,18 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
return (
<div className="file-explorer">
<div className="file-explorer-header">
<span className="file-explorer-title">WORKSPACE</span>
<span className="file-explorer-title">{t('editor.fileExplorer.workspace')}</span>
<div className="file-explorer-header-actions">
<button
className="file-explorer-new-btn"
title="New workspace (clears boards, components, wires and files)"
title={t('editor.fileExplorer.newWorkspace')}
onClick={onNewClick}
>
<IcoNewWorkspace />
</button>
<button
className="file-explorer-save-btn"
title="Save project (Ctrl+S)"
title={t('editor.fileExplorer.saveProject')}
onClick={onSaveClick}
>
<IcoSave />
@ -313,7 +315,7 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
switchToBoard(board.id, groupId);
if (!isOpen) toggleCollapse(board.id);
}}
title={`${BOARD_KIND_LABELS[board.boardKind]}click to edit`}
title={`${BOARD_KIND_LABELS[board.boardKind]}${t('editor.fileExplorer.clickToEdit')}`}
>
<button
className="fe-collapse-btn"
@ -321,7 +323,7 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
e.stopPropagation();
toggleCollapse(board.id);
}}
title={isOpen ? 'Collapse' : 'Expand'}
title={isOpen ? t('editor.fileExplorer.collapse') : t('editor.fileExplorer.expand')}
>
<IcoChevron open={isOpen} />
</button>
@ -335,13 +337,19 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
<span
className="fe-status-dot"
style={{ background: statusColor }}
title={board.running ? 'Running' : board.compiledProgram ? 'Compiled' : 'Idle'}
title={
board.running
? t('editor.fileExplorer.status.running')
: board.compiledProgram
? t('editor.fileExplorer.status.compiled')
: t('editor.fileExplorer.status.idle')
}
/>
{/* New file button — visible on hover */}
<button
className="fe-board-new-btn"
title="New file in this board"
title={t('editor.fileExplorer.newFileInBoard')}
onClick={(e) => {
e.stopPropagation();
startCreateFile(board.id, groupId);
@ -366,7 +374,7 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
switchToBoard(board.id, groupId);
startRename(file.id, groupId);
}}
title={`${file.name}${file.modified ? ' (unsaved)' : ''}`}
title={`${file.name}${file.modified ? ` (${t('editor.fileExplorer.unsavedSuffix')})` : ''}`}
>
<span className="file-explorer-icon">
<FileIcon name={file.name} />
@ -390,7 +398,7 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
)}
{file.modified && (
<span className="file-explorer-dot" title="Unsaved changes" />
<span className="file-explorer-dot" title={t('editor.fileExplorer.unsavedChanges')} />
)}
</div>
);
@ -429,7 +437,7 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
{/* Fallback: no boards yet */}
{boards.length === 0 && (
<div style={{ color: '#666', fontSize: 11, padding: '12px 12px', lineHeight: 1.5 }}>
Add a board to the canvas to start editing code.
{t('editor.fileExplorer.emptyState')}
</div>
)}
</div>
@ -441,14 +449,14 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
onClick={(e) => e.stopPropagation()}
>
<button onClick={() => startRename(contextMenu.fileId, contextMenu.boardGroupId)}>
Rename
{t('editor.fileExplorer.contextMenu.rename')}
</button>
<button
className="ctx-delete"
onClick={() => handleDelete(contextMenu.fileId, contextMenu.boardGroupId)}
disabled={(fileGroups[contextMenu.boardGroupId] ?? []).length <= 1}
>
Delete
{t('editor.fileExplorer.contextMenu.delete')}
</button>
</div>
)}

View File

@ -1,8 +1,10 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useEditorStore } from '../../store/useEditorStore';
import './FileTabs.css';
export const FileTabs: React.FC = () => {
const { t } = useTranslation();
const { files, openFileIds, activeFileId, setActiveFile, closeFile } = useEditorStore();
const [confirmCloseId, setConfirmCloseId] = useState<string | null>(null);
@ -34,12 +36,14 @@ export const FileTabs: React.FC = () => {
onClick={() => setActiveFile(file.id)}
title={file.name}
>
{file.modified && <span className="file-tab-modified" title="Unsaved changes" />}
{file.modified && (
<span className="file-tab-modified" title={t('editor.fileTabs.unsavedChanges')} />
)}
<span className="file-tab-name">{file.name}</span>
<button
className="file-tab-close"
onClick={(e) => handleCloseClick(e, file.id, file.modified)}
title="Close"
title={t('editor.fileTabs.close')}
>
×
</button>
@ -50,12 +54,12 @@ export const FileTabs: React.FC = () => {
{confirmCloseId && (
<div className="ftabs-overlay" onClick={() => setConfirmCloseId(null)}>
<div className="ftabs-confirm-box" onClick={(e) => e.stopPropagation()}>
<p>Close file with unsaved changes?</p>
<p>{t('editor.fileTabs.confirmClose')}</p>
<div className="ftabs-confirm-actions">
<button className="ftabs-btn-close" onClick={confirmClose}>
Close anyway
{t('editor.fileTabs.closeAnyway')}
</button>
<button onClick={() => setConfirmCloseId(null)}>Cancel</button>
<button onClick={() => setConfirmCloseId(null)}>{t('editor.fileTabs.cancel')}</button>
</div>
</div>
</div>

View File

@ -108,6 +108,35 @@
"cta": "Bibliotheksmanager",
"dismiss": "Schließen"
}
},
"fileExplorer": {
"workspace": "ARBEITSBEREICH",
"newWorkspace": "Neuer Arbeitsbereich (entfernt Boards, Bauteile, Drähte und Dateien)",
"saveProject": "Projekt speichern (Strg+S)",
"clickToEdit": "zum Bearbeiten klicken",
"collapse": "Einklappen",
"expand": "Aufklappen",
"newFileInBoard": "Neue Datei auf diesem Board",
"unsavedChanges": "Nicht gespeicherte Änderungen",
"unsavedSuffix": "nicht gespeichert",
"emptyState": "Füge ein Board zur Leinwand hinzu, um Code zu schreiben.",
"confirmDelete": "Diese Datei löschen?",
"status": {
"running": "Läuft",
"compiled": "Kompiliert",
"idle": "Inaktiv"
},
"contextMenu": {
"rename": "Umbenennen",
"delete": "Löschen"
}
},
"fileTabs": {
"unsavedChanges": "Nicht gespeicherte Änderungen",
"close": "Schließen",
"confirmClose": "Datei mit nicht gespeicherten Änderungen schließen?",
"closeAnyway": "Trotzdem schließen",
"cancel": "Abbrechen"
}
}
}

View File

@ -108,6 +108,35 @@
"cta": "Library Manager",
"dismiss": "Dismiss"
}
},
"fileExplorer": {
"workspace": "WORKSPACE",
"newWorkspace": "New workspace (clears boards, components, wires and files)",
"saveProject": "Save project (Ctrl+S)",
"clickToEdit": "click to edit",
"collapse": "Collapse",
"expand": "Expand",
"newFileInBoard": "New file in this board",
"unsavedChanges": "Unsaved changes",
"unsavedSuffix": "unsaved",
"emptyState": "Add a board to the canvas to start editing code.",
"confirmDelete": "Delete this file?",
"status": {
"running": "Running",
"compiled": "Compiled",
"idle": "Idle"
},
"contextMenu": {
"rename": "Rename",
"delete": "Delete"
}
},
"fileTabs": {
"unsavedChanges": "Unsaved changes",
"close": "Close",
"confirmClose": "Close file with unsaved changes?",
"closeAnyway": "Close anyway",
"cancel": "Cancel"
}
}
}

View File

@ -108,6 +108,35 @@
"cta": "Gestor de librerías",
"dismiss": "Descartar"
}
},
"fileExplorer": {
"workspace": "ESPACIO DE TRABAJO",
"newWorkspace": "Nuevo espacio de trabajo (borra placas, componentes, cables y archivos)",
"saveProject": "Guardar proyecto (Ctrl+S)",
"clickToEdit": "haz clic para editar",
"collapse": "Plegar",
"expand": "Expandir",
"newFileInBoard": "Nuevo archivo en esta placa",
"unsavedChanges": "Cambios sin guardar",
"unsavedSuffix": "sin guardar",
"emptyState": "Añade una placa al lienzo para empezar a editar código.",
"confirmDelete": "¿Eliminar este archivo?",
"status": {
"running": "En ejecución",
"compiled": "Compilado",
"idle": "Inactivo"
},
"contextMenu": {
"rename": "Renombrar",
"delete": "Eliminar"
}
},
"fileTabs": {
"unsavedChanges": "Cambios sin guardar",
"close": "Cerrar",
"confirmClose": "¿Cerrar archivo con cambios sin guardar?",
"closeAnyway": "Cerrar igualmente",
"cancel": "Cancelar"
}
}
}

View File

@ -108,6 +108,35 @@
"cta": "Gestionnaire de bibliothèques",
"dismiss": "Fermer"
}
},
"fileExplorer": {
"workspace": "ESPACE DE TRAVAIL",
"newWorkspace": "Nouvel espace de travail (efface cartes, composants, fils et fichiers)",
"saveProject": "Enregistrer le projet (Ctrl+S)",
"clickToEdit": "cliquer pour modifier",
"collapse": "Réduire",
"expand": "Développer",
"newFileInBoard": "Nouveau fichier sur cette carte",
"unsavedChanges": "Modifications non enregistrées",
"unsavedSuffix": "non enregistré",
"emptyState": "Ajoutez une carte au canevas pour commencer à éditer du code.",
"confirmDelete": "Supprimer ce fichier ?",
"status": {
"running": "En cours",
"compiled": "Compilé",
"idle": "Inactif"
},
"contextMenu": {
"rename": "Renommer",
"delete": "Supprimer"
}
},
"fileTabs": {
"unsavedChanges": "Modifications non enregistrées",
"close": "Fermer",
"confirmClose": "Fermer le fichier avec des modifications non enregistrées ?",
"closeAnyway": "Fermer quand même",
"cancel": "Annuler"
}
}
}

View File

@ -108,6 +108,35 @@
"cta": "Gestore Librerie",
"dismiss": "Ignora"
}
},
"fileExplorer": {
"workspace": "WORKSPACE",
"newWorkspace": "Nuovo workspace (cancella schede, componenti, fili e file)",
"saveProject": "Salva progetto (Ctrl+S)",
"clickToEdit": "clic per modificare",
"collapse": "Comprimi",
"expand": "Espandi",
"newFileInBoard": "Nuovo file su questa scheda",
"unsavedChanges": "Modifiche non salvate",
"unsavedSuffix": "non salvato",
"emptyState": "Aggiungi una scheda sul canvas per iniziare a scrivere codice.",
"confirmDelete": "Eliminare questo file?",
"status": {
"running": "In esecuzione",
"compiled": "Compilato",
"idle": "Inattivo"
},
"contextMenu": {
"rename": "Rinomina",
"delete": "Elimina"
}
},
"fileTabs": {
"unsavedChanges": "Modifiche non salvate",
"close": "Chiudi",
"confirmClose": "Chiudere il file con modifiche non salvate?",
"closeAnyway": "Chiudi comunque",
"cancel": "Annulla"
}
}
}

View File

@ -108,6 +108,35 @@
"cta": "ライブラリマネージャー",
"dismiss": "閉じる"
}
},
"fileExplorer": {
"workspace": "ワークスペース",
"newWorkspace": "新しいワークスペース(ボード、コンポーネント、配線、ファイルをすべて削除)",
"saveProject": "プロジェクトを保存 (Ctrl+S)",
"clickToEdit": "クリックして編集",
"collapse": "折りたたむ",
"expand": "展開",
"newFileInBoard": "このボードに新規ファイル",
"unsavedChanges": "未保存の変更",
"unsavedSuffix": "未保存",
"emptyState": "キャンバスにボードを追加してコードの編集を開始しましょう。",
"confirmDelete": "このファイルを削除しますか?",
"status": {
"running": "実行中",
"compiled": "コンパイル済み",
"idle": "アイドル"
},
"contextMenu": {
"rename": "名前を変更",
"delete": "削除"
}
},
"fileTabs": {
"unsavedChanges": "未保存の変更",
"close": "閉じる",
"confirmClose": "未保存の変更があるファイルを閉じますか?",
"closeAnyway": "それでも閉じる",
"cancel": "キャンセル"
}
}
}

View File

@ -108,6 +108,35 @@
"cta": "Gerenciador de Bibliotecas",
"dismiss": "Dispensar"
}
},
"fileExplorer": {
"workspace": "ESPAÇO DE TRABALHO",
"newWorkspace": "Novo espaço de trabalho (limpa placas, componentes, fios e arquivos)",
"saveProject": "Salvar projeto (Ctrl+S)",
"clickToEdit": "clique para editar",
"collapse": "Recolher",
"expand": "Expandir",
"newFileInBoard": "Novo arquivo nesta placa",
"unsavedChanges": "Alterações não salvas",
"unsavedSuffix": "não salvo",
"emptyState": "Adicione uma placa à tela para começar a editar código.",
"confirmDelete": "Excluir este arquivo?",
"status": {
"running": "Em execução",
"compiled": "Compilado",
"idle": "Inativo"
},
"contextMenu": {
"rename": "Renomear",
"delete": "Excluir"
}
},
"fileTabs": {
"unsavedChanges": "Alterações não salvas",
"close": "Fechar",
"confirmClose": "Fechar arquivo com alterações não salvas?",
"closeAnyway": "Fechar mesmo assim",
"cancel": "Cancelar"
}
}
}

View File

@ -108,6 +108,35 @@
"cta": "Менеджер библиотек",
"dismiss": "Закрыть"
}
},
"fileExplorer": {
"workspace": "РАБОЧАЯ ОБЛАСТЬ",
"newWorkspace": "Новая рабочая область (удалит платы, компоненты, провода и файлы)",
"saveProject": "Сохранить проект (Ctrl+S)",
"clickToEdit": "нажмите, чтобы редактировать",
"collapse": "Свернуть",
"expand": "Развернуть",
"newFileInBoard": "Новый файл на этой плате",
"unsavedChanges": "Несохранённые изменения",
"unsavedSuffix": "не сохранено",
"emptyState": "Добавьте плату на холст, чтобы начать писать код.",
"confirmDelete": "Удалить этот файл?",
"status": {
"running": "Работает",
"compiled": "Скомпилировано",
"idle": "Простой"
},
"contextMenu": {
"rename": "Переименовать",
"delete": "Удалить"
}
},
"fileTabs": {
"unsavedChanges": "Несохранённые изменения",
"close": "Закрыть",
"confirmClose": "Закрыть файл с несохранёнными изменениями?",
"closeAnyway": "Всё равно закрыть",
"cancel": "Отмена"
}
}
}

View File

@ -108,6 +108,35 @@
"cta": "库管理器",
"dismiss": "关闭"
}
},
"fileExplorer": {
"workspace": "工作区",
"newWorkspace": "新建工作区(清空开发板、元件、连线和文件)",
"saveProject": "保存项目 (Ctrl+S)",
"clickToEdit": "点击编辑",
"collapse": "折叠",
"expand": "展开",
"newFileInBoard": "在此开发板上新建文件",
"unsavedChanges": "未保存的更改",
"unsavedSuffix": "未保存",
"emptyState": "请将开发板添加到画布上以开始编写代码。",
"confirmDelete": "删除此文件?",
"status": {
"running": "运行中",
"compiled": "已编译",
"idle": "空闲"
},
"contextMenu": {
"rename": "重命名",
"delete": "删除"
}
},
"fileTabs": {
"unsavedChanges": "未保存的更改",
"close": "关闭",
"confirmClose": "确定关闭包含未保存更改的文件吗?",
"closeAnyway": "仍然关闭",
"cancel": "取消"
}
}
}