diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json deleted file mode 100644 index fb911e00..00000000 --- a/public/locales/en/translation.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "mechanism_delete": "Delete Mechanism", - "mechanism_rename": "Rename Mechanism", - "mechanism_copy": "Copy Mechanism", - "opmode_delete": "Delete OpMode", - "opmode_rename": "Rename OpMode", - "opmode_copy": "Copy OpMode", - "project_delete": "Delete Project", - "project_rename": "Rename Project", - "project_copy": "Copy Project", - "fail_list_projects": "Failed to load the list of projects.", - "mechanism": "Mechanism", - "opmode": "OpMode", - "class_rule_description": "No spaces are allowed in the name. Each word in the name should start with a capital letter.", - "example_mechanism": "For example: GamePieceShooter", - "example_opmode": "For example: AutoParkAndShoot", - "example_project": "For example: WackyWheelerRobot", - "addTabDialog": { - "title": "Add Tab", - "newItemPlaceholder": "Add Module", - "search": "Search..." - - } -} diff --git a/src/App.tsx b/src/App.tsx index 9ce3bf2c..200149bd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,6 +47,7 @@ import * as ChangeFramework from './blocks/utils/change_framework' import { mutatorOpenListener } from './blocks/mrc_param_container' import { TOOLBOX_UPDATE_EVENT } from './blocks/mrc_mechanism_component_holder'; import { antdThemeFromString } from './reactComponents/ThemeModal'; +import { useTranslation } from 'react-i18next'; /** Storage key for shown toolbox categories. */ const SHOWN_TOOLBOX_CATEGORIES_KEY = 'shownPythonToolboxCategories'; @@ -86,6 +87,8 @@ const LAYOUT_BACKGROUND_COLOR = '#0F0'; * project management, and user interface layout. */ const App: React.FC = (): React.JSX.Element => { + const { t, i18n } = useTranslation(); + const [alertErrorMessage, setAlertErrorMessage] = React.useState(''); const [storage, setStorage] = React.useState(null); const [currentModule, setCurrentModule] = React.useState(null); @@ -101,7 +104,6 @@ const App: React.FC = (): React.JSX.Element => { const [rightCollapsed, setRightCollapsed] = React.useState(false); const [theme, setTheme] = React.useState('dark'); - const blocksEditor = React.useRef(null); const generatorContext = React.useRef(null); const blocklyComponent = React.useRef(null); @@ -232,7 +234,7 @@ const App: React.FC = (): React.JSX.Element => { const tabs: Tabs.TabItem[] = [ { key: projectData.robot.modulePath, - title: 'Robot', + title: t('ROBOT'), type: TabType.ROBOT, }, ]; @@ -261,7 +263,7 @@ const App: React.FC = (): React.JSX.Element => { if (blocksEditor.current && currentModule) { blocksEditor.current.updateToolbox(shownPythonToolboxCategories); } - }, [currentModule, shownPythonToolboxCategories]); + }, [currentModule, shownPythonToolboxCategories, i18n.language]); // Add event listener for toolbox updates React.useEffect(() => { @@ -290,7 +292,31 @@ const App: React.FC = (): React.JSX.Element => { if (blocksEditor.current) { blocksEditor.current.loadModuleBlocks(currentModule); } - }, [currentModule]); + }, [currentModule]); + + const setupWorkspace = (newWorkspace: Blockly.WorkspaceSvg) => { + if (!blocklyComponent.current || !storage) { + return; + } + // Recreate workspace when Blockly component is ready + ChangeFramework.setup(newWorkspace); + newWorkspace.addChangeListener(mutatorOpenListener); + newWorkspace.addChangeListener(handleBlocksChanged); + generatorContext.current = createGeneratorContext(); + + if (currentModule) { + generatorContext.current.setModule(currentModule); + } + + blocksEditor.current = new editor.Editor(newWorkspace, generatorContext.current, storage); + + // Set the current module in the editor after creating it + if (currentModule) { + blocksEditor.current.loadModuleBlocks(currentModule); + } + + blocksEditor.current.updateToolbox(shownPythonToolboxCategories); + }; // Initialize Blockly workspace and editor when component and storage are ready React.useEffect(() => { @@ -300,17 +326,8 @@ const App: React.FC = (): React.JSX.Element => { const blocklyWorkspace = blocklyComponent.current.getBlocklyWorkspace(); if (blocklyWorkspace) { - ChangeFramework.setup(blocklyWorkspace); - blocklyWorkspace.addChangeListener(mutatorOpenListener); - blocklyWorkspace.addChangeListener(handleBlocksChanged); + setupWorkspace(blocklyWorkspace); } - - generatorContext.current = createGeneratorContext(); - if (currentModule) { - generatorContext.current.setModule(currentModule); - } - - blocksEditor.current = new editor.Editor(blocklyWorkspace, generatorContext.current, storage); }, [blocklyComponent, storage]); // Generate code when module or regeneration trigger changes @@ -342,6 +359,31 @@ const App: React.FC = (): React.JSX.Element => { } }, [project]); + // Handle language changes with automatic saving + React.useEffect(() => { + const handleLanguageChange = async () => { + // Save current blocks before language change + if (currentModule && areBlocksModified()) { + try { + await saveBlocks(); + } catch (e) { + console.error('Failed to save blocks before language change:', e); + } + } + + // Update toolbox after language change + if (blocksEditor.current) { + blocksEditor.current.updateToolbox(shownPythonToolboxCategories); + } + }; + + i18n.on('languageChanged', handleLanguageChange); + + return () => { + i18n.off('languageChanged', handleLanguageChange); + }; + }, [currentModule, shownPythonToolboxCategories, i18n]); + const { Sider, Content } = Antd.Layout; return ( @@ -392,6 +434,7 @@ const App: React.FC = (): React.JSX.Element => { diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index 08876d1c..4de2fab0 100644 --- a/src/blocks/mrc_call_python_function.ts +++ b/src/blocks/mrc_call_python_function.ts @@ -421,12 +421,14 @@ const CALL_PYTHON_FUNCTION = { returnType: this.mrcReturnType, args: [], }; - this.mrcArgs.forEach((arg) => { - extraState.args.push({ - 'name': arg.name, - 'type': arg.type, + if (this.mrcArgs){ + this.mrcArgs.forEach((arg) => { + extraState.args.push({ + 'name': arg.name, + 'type': arg.type, + }); }); - }); + } if (this.mrcTooltip) { extraState.tooltip = this.mrcTooltip; } diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index a8a8cfd9..67e0002e 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -93,6 +93,7 @@ const CLASS_METHOD_DEF = { this.mrcParameters = []; this.setPreviousStatement(false); this.setNextStatement(false); + this.updateBlock_(); }, /** * Returns the state of this block as a JSON serializable object. diff --git a/src/blocks/mrc_component.ts b/src/blocks/mrc_component.ts index 492f8eae..5812ccea 100644 --- a/src/blocks/mrc_component.ts +++ b/src/blocks/mrc_component.ts @@ -69,7 +69,7 @@ const COMPONENT = { this.setStyle(MRC_STYLE_COMPONENTS); this.appendDummyInput() .appendField(new Blockly.FieldTextInput(''), FIELD_NAME) - .appendField('of type') + .appendField(Blockly.Msg.OF_TYPE) .appendField(createFieldNonEditableText(''), FIELD_TYPE); this.setPreviousStatement(true, OUTPUT_NAME); this.setNextStatement(true, OUTPUT_NAME); @@ -82,12 +82,14 @@ const COMPONENT = { const extraState: ComponentExtraState = { }; extraState.params = []; - this.mrcArgs.forEach((arg) => { - extraState.params!.push({ - 'name': arg.name, - 'type': arg.type, + if (this.mrcArgs){ + this.mrcArgs.forEach((arg) => { + extraState.params!.push({ + 'name': arg.name, + 'type': arg.type, + }); }); - }); + } if (this.mrcImportModule) { extraState.importModule = this.mrcImportModule; } diff --git a/src/blocks/mrc_event_handler.ts b/src/blocks/mrc_event_handler.ts index b6cdc9cc..14174e90 100644 --- a/src/blocks/mrc_event_handler.ts +++ b/src/blocks/mrc_event_handler.ts @@ -65,11 +65,11 @@ const EVENT_HANDLER = { */ init(this: EventHandlerBlock): void { this.appendDummyInput('TITLE') - .appendField('When') + .appendField(Blockly.Msg.WHEN) .appendField(createFieldNonEditableText('sender'), 'SENDER') .appendField(createFieldNonEditableText('eventName'), 'EVENT_NAME'); this.appendDummyInput('PARAMS') - .appendField('with'); + .appendField(Blockly.Msg.WITH); this.setOutput(false); this.setStyle(MRC_STYLE_EVENT_HANDLER); this.appendStatementInput('STACK').appendField(''); diff --git a/src/blocks/mrc_get_parameter.ts b/src/blocks/mrc_get_parameter.ts index a345117f..d489424d 100644 --- a/src/blocks/mrc_get_parameter.ts +++ b/src/blocks/mrc_get_parameter.ts @@ -49,7 +49,7 @@ const GET_PARAMETER_BLOCK = { init: function(this: GetParameterBlock): void { this.setStyle(MRC_STYLE_VARIABLES); this.appendDummyInput() - .appendField('parameter') + .appendField(Blockly.Msg.PARAMETER) .appendField(createFieldNonEditableText('parameter'), 'PARAMETER_NAME'); this.setOutput(true, this.parameterType); @@ -89,7 +89,7 @@ const GET_PARAMETER_BLOCK = { } // If we end up here it shouldn't be allowed block.unplug(true); - blockBlock.setWarningText('Parameters can only go in their method\'s block.'); + blockBlock.setWarningText(Blockly.Msg.PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK); } }, }; diff --git a/src/blocks/mrc_mechanism.ts b/src/blocks/mrc_mechanism.ts index 5d0c4387..340a0de6 100644 --- a/src/blocks/mrc_mechanism.ts +++ b/src/blocks/mrc_mechanism.ts @@ -55,7 +55,7 @@ const MECHANISM = { this.setStyle(MRC_STYLE_MECHANISMS); this.appendDummyInput() .appendField(new Blockly.FieldTextInput('my_mech'), 'NAME') - .appendField('of type') + .appendField(Blockly.Msg.OF_TYPE) .appendField(createFieldNonEditableText(''), 'TYPE'); this.setPreviousStatement(true, OUTPUT_NAME); this.setNextStatement(true, OUTPUT_NAME); diff --git a/src/blocks/mrc_mechanism_component_holder.ts b/src/blocks/mrc_mechanism_component_holder.ts index dfa7e817..612d79e4 100644 --- a/src/blocks/mrc_mechanism_component_holder.ts +++ b/src/blocks/mrc_mechanism_component_holder.ts @@ -72,9 +72,9 @@ const MECHANISM_COMPONENT_HOLDER = { */ init: function (this: MechanismComponentHolderBlock): void { this.setInputsInline(false); - this.appendStatementInput('MECHANISMS').setCheck(MECHANISM_OUTPUT).appendField('Mechanisms'); - this.appendStatementInput('COMPONENTS').setCheck(COMPONENT_OUTPUT).appendField('Components'); - this.appendStatementInput('EVENTS').setCheck(EVENT_OUTPUT).appendField('Events'); + this.appendStatementInput('MECHANISMS').setCheck(MECHANISM_OUTPUT).appendField(Blockly.Msg.MECHANISMS); + this.appendStatementInput('COMPONENTS').setCheck(COMPONENT_OUTPUT).appendField(Blockly.Msg.COMPONENTS); + this.appendStatementInput('EVENTS').setCheck(EVENT_OUTPUT).appendField(Blockly.Msg.EVENTS); this.setOutput(false); diff --git a/src/blocks/mrc_misc_evaluate_but_ignore_result.ts b/src/blocks/mrc_misc_evaluate_but_ignore_result.ts index 7c987864..345ba959 100644 --- a/src/blocks/mrc_misc_evaluate_but_ignore_result.ts +++ b/src/blocks/mrc_misc_evaluate_but_ignore_result.ts @@ -31,14 +31,13 @@ export const setup = function() { Blockly.Blocks[BLOCK_NAME] = { init: function() { this.appendValueInput('VALUE') - .appendField('evaluate but ignore result') + .appendField(Blockly.Msg.EVALUATE_BUT_IGNORE_RESULT) .setAlign(Blockly.inputs.Align.RIGHT); this.setPreviousStatement(true); this.setNextStatement(true); this.setStyle(MRC_STYLE_MISC); this.setTooltip( - 'Executes the connected block and ignores the result. ' + - 'Allows you to call a function and ignore the return value.'); + Blockly.Msg.EVALUATE_BUT_IGNORE_RESULT_TOOLTIP); }, }; }; diff --git a/src/blocks/mrc_opmode_details.ts b/src/blocks/mrc_opmode_details.ts index d1e7ba85..8b8301e2 100644 --- a/src/blocks/mrc_opmode_details.ts +++ b/src/blocks/mrc_opmode_details.ts @@ -61,23 +61,24 @@ const OPMODE_DETAILS = { init: function (this: OpmodeDetailsBlock): void { this.setStyle(MRC_STYLE_CLASS_BLOCKS); this.appendDummyInput() - .appendField('Type') - .appendField(createFieldDropdown(['Auto', 'Teleop', 'Test']), 'TYPE') + .appendField(Blockly.Msg.TYPE) + // These aren't Blockly.Msg because they need to match the Python generator's expected values. + .appendField(createFieldDropdown(["Auto", "Teleop", "Test"]), 'TYPE') .appendField(' ') - .appendField('Enabled') + .appendField(Blockly.Msg.ENABLED) .appendField(new Blockly.FieldCheckbox(true), 'ENABLED'); this.appendDummyInput() - .appendField('Display Name') + .appendField(Blockly.Msg.DISPLAY_NAME) .appendField(new Blockly.FieldTextInput(''), 'NAME') this.appendDummyInput() - .appendField('Display Group') + .appendField(Blockly.Msg.DISPLAY_GROUP) .appendField(new Blockly.FieldTextInput(''), 'GROUP'); - this.getField('TYPE')?.setTooltip('What sort of OpMode this is'); - this.getField('ENABLED')?.setTooltip('Whether the OpMode is shown on Driver Station'); - this.getField('NAME')?.setTooltip('The name shown on the Driver Station. If blank will use the class name.'); - this.getField('GROUP')?.setTooltip('An optional group to group OpModes on Driver Station'); + this.getField('TYPE')?.setTooltip(Blockly.Msg.OPMODE_TYPE_TOOLTIP); + this.getField('ENABLED')?.setTooltip(Blockly.Msg.OPMODE_ENABLED_TOOLTIP); + this.getField('NAME')?.setTooltip(Blockly.Msg.OPMODE_NAME_TOOLTIP); + this.getField('GROUP')?.setTooltip(Blockly.Msg.OPMODE_GROUP_TOOLTIP); }, } diff --git a/src/blocks/tokens.ts b/src/blocks/tokens.ts new file mode 100644 index 00000000..ee399a04 --- /dev/null +++ b/src/blocks/tokens.ts @@ -0,0 +1,46 @@ +import * as Blockly from 'blockly/core'; +import { M } from 'vitest/dist/chunks/reporters.d.BFLkQcL6.js'; + +export const customTokens = (t: (key: string) => string): typeof Blockly.Msg => { + return { + ADD_COMMENT: t('BLOCKLY.ADD_COMMENT'), + REMOVE_COMMENT: t('BLOCKLY.REMOVE_COMMENT'), + DUPLICATE_COMMENT: t('BLOCKLY.DUPLICATE_COMMENT'), + OF_TYPE: t('BLOCKLY.OF_TYPE'), + WITH: t('BLOCKLY.WITH'), + WHEN: t('BLOCKLY.WHEN'), + PARAMETER: t('BLOCKLY.PARAMETER'), + PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK: t('BLOCKLY.PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK'), + MECHANISMS: t('BLOCKLY.MECHANISMS'), + COMPONENTS: t('BLOCKLY.COMPONENTS'), + EVENTS: t('BLOCKLY.EVENTS'), + EVALUATE_BUT_IGNORE_RESULT: t('BLOCKLY.EVALUATE_BUT_IGNORE_RESULT'), + EVALUATE_BUT_IGNORE_RESULT_TOOLTIP: t('BLOCKLY.EVALUATE_BUT_IGNORE_RESULT_TOOLTIP'), + AUTO: t('BLOCKLY.AUTO'), + TELEOP: t('BLOCKLY.TELEOP'), + TEST: t('BLOCKLY.TEST'), + TYPE: t('BLOCKLY.TYPE'), + ENABLED: t('BLOCKLY.ENABLED'), + DISPLAY_NAME: t('BLOCKLY.DISPLAY_NAME'), + DISPLAY_GROUP: t('BLOCKLY.DISPLAY_GROUP'), + OPMODE_TYPE_TOOLTIP: t('BLOCKLY.TOOLTIP.OPMODE_TYPE_TOOLTIP'), + OPMODE_ENABLED_TOOLTIP: t('BLOCKLY.TOOLTIP.OPMODE_ENABLED_TOOLTIP'), + OPMODE_NAME_TOOLTIP: t('BLOCKLY.TOOLTIP.OPMODE_NAME_TOOLTIP'), + OPMODE_GROUP_TOOLTIP: t('BLOCKLY.TOOLTIP.OPMODE_GROUP_TOOLTIP'), + MRC_CATEGORY_HARDWARE: t('BLOCKLY.CATEGORY.HARDWARE'), + MRC_CATEGORY_ROBOT: t('BLOCKLY.CATEGORY.ROBOT'), + MRC_CATEGORY_COMPONENTS: t('BLOCKLY.CATEGORY.COMPONENTS'), + MRC_CATEGORY_MECHANISMS: t('BLOCKLY.CATEGORY.MECHANISMS'), + MRC_CATEGORY_LOGIC: t('BLOCKLY.CATEGORY.LOGIC'), + MRC_CATEGORY_LOOPS: t('BLOCKLY.CATEGORY.LOOPS'), + MRC_CATEGORY_LISTS: t('BLOCKLY.CATEGORY.LISTS'), + MRC_CATEGORY_MATH: t('BLOCKLY.CATEGORY.MATH'), + MRC_CATEGORY_TEXT: t('BLOCKLY.CATEGORY.TEXT'), + MRC_CATEGORY_MISC: t('BLOCKLY.CATEGORY.MISC'), + MRC_CATEGORY_VARIABLES: t('BLOCKLY.CATEGORY.VARIABLES'), + MRC_CATEGORY_METHODS: t('BLOCKLY.CATEGORY.METHODS'), + MRC_CATEGORY_EVENTS: t('BLOCKLY.CATEGORY.EVENTS'), + MRC_CATEGORY_ADD_MECHANISM: t('BLOCKLY.CATEGORY.ADD_MECHANISM'), + MRC_CATEGORY_ADD_COMPONENT: t('BLOCKLY.CATEGORY.ADD_COMPONENT'), + }; +}; \ No newline at end of file diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json new file mode 100644 index 00000000..462330d2 --- /dev/null +++ b/src/i18n/locales/en/translation.json @@ -0,0 +1,79 @@ +{ + "mechanism_delete": "Delete Mechanism", + "mechanism_rename": "Rename Mechanism", + "mechanism_copy": "Copy Mechanism", + "opmode_delete": "Delete OpMode", + "opmode_rename": "Rename OpMode", + "opmode_copy": "Copy OpMode", + "project_delete": "Delete Project", + "project_rename": "Rename Project", + "project_copy": "Copy Project", + "fail_list_projects": "Failed to load the list of projects.", + "mechanism": "Mechanism", + "opmode": "OpMode", + "class_rule_description": "No spaces are allowed in the name. Each word in the name should start with a capital letter.", + "example_mechanism": "For example: GamePieceShooter", + "example_opmode": "For example: AutoParkAndShoot", + "example_project": "For example: WackyWheelerRobot", + "addTabDialog": { + "title": "Add Tab", + "newItemPlaceholder": "Add Module", + "search": "Search..." + }, + "PROJECT": "Project", + "SAVE": "Save", + "DEPLOY": "Deploy", + "MANAGE": "Manage", + "EXPLORER": "Explorer", + "ROBOT": "Robot", + "SETTINGS": "Settings", + "WPI_TOOLBOX": "WPI Toolbox", + "THEME": "Theme", + "LANGUAGE": "Language", + "ENGLISH": "English", + "SPANISH": "Spanish", + "HELP": "Help", + "ABOUT": "About", + "BLOCKS": "Blocks", + "CODE": "Code", + "COPY": "Copy", + "BLOCKLY":{ + "OF_TYPE": "of type", + "WITH": "with", + "WHEN": "when", + "PARAMETER": "parameter", + "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "Parameters can only go in their method's block", + "MECHANISMS": "Mechanisms", + "COMPONENTS": "Components", + "EVENTS": "Events", + "EVALUATE_BUT_IGNORE_RESULT": "evaluate but ignore result", + "TYPE": "Type", + "ENABLED": "Enabled", + "DISPLAY_NAME": "Display Name", + "DISPLAY_GROUP": "Display Group", + "TOOLTIP":{ + "EVALUATE_BUT_IGNORE_RESULT": "Executes the connected block and ignores the result. Allows you to call a function and ignore the return value.", + "OPMODE_TYPE": "What sort of OpMode this is", + "OPMODE_ENABLED": "Whether the OpMode is shown on Driver Station", + "OPMODE_NAME": "The name shown on the Driver Station. If blank will use the class name.", + "OPMODE_GROUP": "An optional group to group OpModes on Driver Station" + }, + "CATEGORY":{ + "LISTS": "Lists", + "HARDWARE": "Hardware", + "COMPONENTS": "Components", + "ROBOT": "Robot", + "MECHANISMS": "Mechanisms", + "LOGIC": "Logic", + "LOOPS": "Loops", + "MATH": "Math", + "TEXT": "Text", + "MISC": "Miscellaneous", + "VARIABLES": "Variables", + "METHODS": "Methods", + "EVENTS": "Events", + "ADD_MECHANISM": "+ Mechanism", + "ADD_COMPONENT": "+ Component" + } + } +} diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json new file mode 100644 index 00000000..0f23c12d --- /dev/null +++ b/src/i18n/locales/es/translation.json @@ -0,0 +1,80 @@ +{ + "__TODO__": "These translations were done by Copilot. (Claude Sonnet 4). They need to be verified by a native Spansih speaker.", + "mechanism_delete": "Eliminar Mecanismo", + "mechanism_rename": "Renombrar Mecanismo", + "mechanism_copy": "Copiar Mecanismo", + "opmode_delete": "Eliminar OpMode", + "opmode_rename": "Renombrar OpMode", + "opmode_copy": "Copiar OpMode", + "project_delete": "Eliminar Proyecto", + "project_rename": "Renombrar Proyecto", + "project_copy": "Copiar Proyecto", + "fail_list_projects": "Error al cargar la lista de proyectos.", + "mechanism": "Mecanismo", + "opmode": "OpMode", + "class_rule_description": "No se permiten espacios en el nombre. Cada palabra en el nombre debe comenzar con una letra mayúscula.", + "example_mechanism": "Por ejemplo: DisparadorDePiezas", + "example_opmode": "Por ejemplo: AutoEstacionarYDisparar", + "example_project": "Por ejemplo: RobotRuedasLocas", + "PROJECT": "Proyecto", + "SAVE": "Guardar", + "DEPLOY": "Desplegar", + "MANAGE": "Gestionar", + "EXPLORER": "Explorador", + "ROBOT": "Robot", + "SETTINGS": "Configuración", + "WPI_TOOLBOX": "Caja de Herramientas WPI", + "THEME": "Tema", + "LANGUAGE": "Idioma", + "ENGLISH": "Inglés", + "SPANISH": "Español", + "HELP": "Ayuda", + "ABOUT": "Acerca de", + "BLOCKS": "Bloques", + "CODE": "Código", + "COPY": "Copiar", + "addTabDialog": { + "title": "Agregar Pestaña", + "newItemPlaceholder": "Agregar Módulo", + "search": "Buscar..." + }, + "BLOCKLY": { + "OF_TYPE": "de tipo", + "WITH": "con", + "WHEN": "cuando", + "PARAMETER": "parámetro", + "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "Los parámetros solo pueden ir en el bloque de su método", + "MECHANISMS": "Mecanismos", + "COMPONENTS": "Componentes", + "EVENTS": "Eventos", + "EVALUATE_BUT_IGNORE_RESULT": "evaluar pero ignorar resultado", + "TYPE": "Tipo", + "ENABLED": "Habilitado", + "DISPLAY_NAME": "Nombre a Mostrar", + "DISPLAY_GROUP": "Grupo a Mostrar", + "TOOLTIP": { + "EVALUATE_BUT_IGNORE_RESULT": "Ejecuta el bloque conectado e ignora el resultado. Te permite llamar una función e ignorar el valor de retorno.", + "OPMODE_TYPE": "Qué tipo de OpMode es este", + "OPMODE_ENABLED": "Si el OpMode se muestra en la Estación del Conductor", + "OPMODE_NAME": "El nombre mostrado en la Estación del Conductor. Si está en blanco usará el nombre de la clase.", + "OPMODE_GROUP": "Un grupo opcional para agrupar OpModes en la Estación del Conductor" + }, + "CATEGORY": { + "LISTS": "Listas", + "HARDWARE": "Hardware", + "COMPONENTS": "Componentes", + "ROBOT": "Robot", + "MECHANISMS": "Mecanismos", + "LOGIC": "Lógica", + "LOOPS": "Bucles", + "MATH": "Matemáticas", + "TEXT": "Texto", + "MISC": "Varios", + "VARIABLES": "Variables", + "METHODS": "Métodos", + "EVENTS": "Eventos", + "ADD_MECHANISM": "+ Mecanismo", + "ADD_COMPONENT": "+ Componente" + } + } +} \ No newline at end of file diff --git a/src/reactComponents/BlocklyComponent.tsx b/src/reactComponents/BlocklyComponent.tsx index 0edd3fbb..d1a29ed0 100644 --- a/src/reactComponents/BlocklyComponent.tsx +++ b/src/reactComponents/BlocklyComponent.tsx @@ -20,11 +20,16 @@ */ import * as React from 'react'; import * as Blockly from 'blockly/core'; -import * as locale from 'blockly/msg/en'; +import * as En from 'blockly/msg/en'; +import * as Es from 'blockly/msg/es'; +import { customTokens } from '../blocks/tokens'; + import { themes } from '../themes/mrc_themes'; import {pluginInfo as HardwareConnectionsPluginInfo} from '../blocks/utils/connection_checker'; import 'blockly/blocks'; // Includes standard blocks like controls_if, logic_compare, etc. +import { useTranslation } from 'react-i18next'; + /** Interface for methods exposed by the BlocklyComponent. */ export interface BlocklyComponentType { @@ -34,6 +39,7 @@ export interface BlocklyComponentType { /** Interface for props passed to the BlocklyComponent. */ export interface BlocklyComponentProps { theme: string; + onWorkspaceRecreated: (workspace: Blockly.WorkspaceSvg) => void; } /** Grid spacing for the Blockly workspace. */ @@ -74,12 +80,15 @@ const WORKSPACE_STYLE: React.CSSProperties = { * cleanup, and resize handling. */ const BlocklyComponent = React.forwardRef( - ({ theme }, ref): React.JSX.Element => { + (props, ref): React.JSX.Element => { const blocklyDiv = React.useRef(null); const workspaceRef = React.useRef(null); + const { t, i18n } = useTranslation(); + + const getBlocklyTheme = (): Blockly.Theme => { - const blocklyTheme = 'mrc_theme_' + theme.replace(/-/g, '_'); + const blocklyTheme = 'mrc_theme_' + props.theme.replace(/-/g, '_'); // Find the theme by key const themeObj = themes.find(t => t.name === blocklyTheme); if (!themeObj) { @@ -126,6 +135,38 @@ const BlocklyComponent = React.forwardRef { + if (!workspaceRef.current) { + return; + } + // Save workspace state + const workspaceXml = Blockly.Xml.workspaceToDom(workspaceRef.current); + + // Set new locale + switch (i18n.language) { + case 'es': + Blockly.setLocale(Es as any); + break; + case 'en': + Blockly.setLocale(En as any); + break; + default: + Blockly.setLocale(En as any); + break; + } + // Apply custom tokens + Blockly.setLocale(customTokens(t)); + + // Clear the workspace + workspaceRef.current.clear(); + + // Force complete toolbox rebuild by calling onWorkspaceRecreated AFTER locale is set + if (props.onWorkspaceRecreated) { + props.onWorkspaceRecreated(workspaceRef.current); + } + }; + /** Initializes the Blockly workspace. */ const initializeWorkspace = (): void => { if (!blocklyDiv.current) { @@ -133,8 +174,19 @@ const BlocklyComponent = React.forwardRef { + updateBlocklyLocale(); + }, [i18n.language]); // Handle workspace resize React.useEffect(() => { diff --git a/src/reactComponents/CodeDisplay.tsx b/src/reactComponents/CodeDisplay.tsx index 4a647a89..563620d3 100644 --- a/src/reactComponents/CodeDisplay.tsx +++ b/src/reactComponents/CodeDisplay.tsx @@ -25,6 +25,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { dracula, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'; import type { MessageInstance } from 'antd/es/message/interface'; +import { useTranslation } from 'react-i18next'; /** Function type for setting string values. */ type StringFunction = (input: string) => void; @@ -64,6 +65,7 @@ export default function CodeDisplay(props: CodeDisplayProps): React.JSX.Element } const { token } = Antd.theme.useToken(); + const { t } = useTranslation(); const syntaxStyle = syntaxHighligherFromTheme(props.theme); /** Handles copying the generated code to clipboard. */ @@ -90,8 +92,8 @@ export default function CodeDisplay(props: CodeDisplayProps): React.JSX.Element /** Renders the header section with title and copy button. */ const renderHeader = (): React.JSX.Element => ( - Code - + {t("CODE")} + } size="small" diff --git a/src/reactComponents/Header.tsx b/src/reactComponents/Header.tsx index 3a34f17d..4cbd83a0 100644 --- a/src/reactComponents/Header.tsx +++ b/src/reactComponents/Header.tsx @@ -21,6 +21,7 @@ import * as Antd from 'antd'; import * as commonStorage from '../storage/common_storage'; import * as React from 'react'; +import { useTranslation } from 'react-i18next'; /** Function type for setting string values. */ type StringFunction = (input: string) => void; @@ -50,6 +51,7 @@ const TEXT_PADDING_LEFT = 20; */ export default function Header(props: HeaderProps): React.JSX.Element { const { token } = Antd.theme.useToken(); + const { t } = useTranslation(); const isDarkTheme = token.colorBgLayout === '#000000'; @@ -95,7 +97,7 @@ export default function Header(props: HeaderProps): React.JSX.Element { fontWeight: TITLE_FONT_WEIGHT, }} > - Blocks + {t("BLOCKS")} - Project: {getProjectName()} + {t("PROJECT")}: {getProjectName()} {renderErrorAlert()} diff --git a/src/reactComponents/Menu.tsx b/src/reactComponents/Menu.tsx index e14f6a21..fd2b34e5 100644 --- a/src/reactComponents/Menu.tsx +++ b/src/reactComponents/Menu.tsx @@ -35,6 +35,8 @@ import { QuestionCircleOutlined, InfoCircleOutlined, BgColorsOutlined, + GlobalOutlined, + CheckOutlined, } from '@ant-design/icons'; import FileManageModal from './FileManageModal'; import ProjectManageModal from './ProjectManageModal'; @@ -92,7 +94,7 @@ function getDivider(): MenuItem { /** * Generates menu items for a given project. */ -function getMenuItems(t: (key: string) => string, project: commonStorage.Project): MenuItem[] { +function getMenuItems(t: (key: string) => string, project: commonStorage.Project, currentLanguage: string): MenuItem[] { const mechanisms: MenuItem[] = []; const opmodes: MenuItem[] = []; @@ -123,24 +125,35 @@ function getMenuItems(t: (key: string) => string, project: commonStorage.Project opmodes.push(getItem('Manage...', 'manageOpmodes')); return [ - getItem(t('Project'), 'project', , [ - getItem(t('Save'), 'save', ), - getItem(t('Deploy'), 'deploy', undefined, undefined, true), + getItem(t('PROJECT'), 'project', , [ + getItem(t('SAVE'), 'save', ), + getItem(t('DEPLOY'), 'deploy', undefined, undefined, true), getDivider(), - getItem(t('Manage') + '...', 'manageProjects'), + getItem(t('MANAGE') + '...', 'manageProjects'), ]), - getItem(t('Explorer'), 'explorer', , [ - getItem(t('Robot'), project.robot.modulePath, ), - getItem(t('Mechanisms'), 'mechanisms', , mechanisms), - getItem(t('OpModes'), 'opmodes', , opmodes), + getItem(t('EXPLORER'), 'explorer', , [ + getItem(t('ROBOT'), project.robot.modulePath, ), + getItem(t('MECHANISMS'), 'mechanisms', , mechanisms), + getItem(t('OPMODES'), 'opmodes', , opmodes), ]), - getItem(t('Settings'), 'settings', , [ - getItem(t('WPI toolbox'), 'wpi_toolbox'), - getItem(t('Theme') + '...', 'theme', ) + getItem(t('SETTINGS'), 'settings', , [ + getItem(t('WPI_TOOLBOX'), 'wpi_toolbox'), + getItem(t('THEME') + '...', 'theme', ), + getItem(t('LANGUAGE'), 'language', , [ + getItem( + t('ENGLISH'), + 'setlang:en', + currentLanguage === 'en' ? : undefined + ), + getItem( + t('SPANISH'), + 'setlang:es', + currentLanguage === 'es' ? : undefined + ), + ]), ]), - getItem(t('Help'), 'help', , [ - getItem(t('About') + '...', 'about', -), + getItem(t('HELP'), 'help', , [ + getItem(t('ABOUT') + '...', 'about', ), ]), ]; } @@ -150,7 +163,7 @@ function getMenuItems(t: (key: string) => string, project: commonStorage.Project * Provides access to mechanisms, opmodes, and project management functionality. */ export function Component(props: MenuProps): React.JSX.Element { - const {t} = I18Next.useTranslation(); + const {t, i18n} = I18Next.useTranslation(); const [projects, setProjects] = React.useState([]); const [menuItems, setMenuItems] = React.useState([]); @@ -264,8 +277,12 @@ export function Component(props: MenuProps): React.JSX.Element { props.openWPIToolboxSettings(); } else if (key === 'theme') { setThemeModalOpen(true); + } else if (key.startsWith('setlang:')) { + const lang = key.split(':')[1]; + i18n.changeLanguage(lang); } else { // TODO: Handle other menu actions + console.log(`Selected key that wasn't module: ${key}`); } }; @@ -294,14 +311,14 @@ export function Component(props: MenuProps): React.JSX.Element { fetchMostRecentProject(); }, [projects]); - // Update menu items and save project when project changes + // Update menu items and save project when project or language changes React.useEffect(() => { if (props.project) { setMostRecentProject(); - setMenuItems(getMenuItems(t, props.project)); + setMenuItems(getMenuItems(t, props.project, i18n.language)); setNoProjects(false); } - }, [props.project]); + }, [props.project, i18n.language]); return ( <> diff --git a/src/toolbox/event_category.ts b/src/toolbox/event_category.ts index 4fd9f30e..2e2ecc9d 100644 --- a/src/toolbox/event_category.ts +++ b/src/toolbox/event_category.ts @@ -29,12 +29,12 @@ import { RETURN_TYPE_NONE, FunctionKind } from '../blocks/mrc_call_python_functi const CUSTOM_CATEGORY_EVENTS = 'EVENTS'; -export const category = { +export const getCategory = () => ({ kind: 'category', categorystyle: MRC_CATEGORY_STYLE_METHODS, - name: 'Events', + name: Blockly.Msg['MRC_CATEGORY_EVENTS'], custom: CUSTOM_CATEGORY_EVENTS, -}; +}); export class EventsCategory { private currentModule: commonStorage.Module | null = null; diff --git a/src/toolbox/hardware_category.ts b/src/toolbox/hardware_category.ts index a7c73b91..b3d9f5ad 100644 --- a/src/toolbox/hardware_category.ts +++ b/src/toolbox/hardware_category.ts @@ -30,7 +30,7 @@ export function getHardwareCategory(currentModule: commonStorage.Module) { if (currentModule.moduleType === commonStorage.MODULE_TYPE_ROBOT) { return { kind: 'category', - name: 'Hardware', + name: Blockly.Msg['MRC_CATEGORY_HARDWARE'], contents: [ getRobotMechanismsBlocks(currentModule), getComponentsBlocks(currentModule, false), @@ -43,7 +43,7 @@ export function getHardwareCategory(currentModule: commonStorage.Module) { if (currentModule.moduleType === commonStorage.MODULE_TYPE_OPMODE) { return { kind: 'category', - name: 'Robot', + name: Blockly.Msg['MRC_CATEGORY_ROBOT'], contents: [ getRobotMechanismsBlocks(currentModule), getRobotComponentsBlocks(currentModule), @@ -67,7 +67,7 @@ function getRobotMechanismsBlocks(currentModule: commonStorage.Module) { if (currentModule.moduleType === commonStorage.MODULE_TYPE_ROBOT) { contents.push({ kind: 'category', - name: '+ Mechanism', + name: Blockly.Msg['MRC_CATEGORY_ADD_MECHANISM'], contents: getAllPossibleMechanisms(), }); } @@ -203,7 +203,7 @@ function getRobotMechanismsBlocks(currentModule: commonStorage.Module) { return { kind: 'category', - name: 'Mechanisms', + name: Blockly.Msg['MRC_CATEGORY_MECHANISMS'], contents, }; } @@ -233,7 +233,7 @@ function getRobotComponentsBlocks(currentModule: commonStorage.Module) { return { kind: 'category', - name: 'Components', + name: Blockly.Msg['MRC_CATEGORY_COMPONENTS'], contents, }; } @@ -246,7 +246,7 @@ function getRobotMethodsBlocks(currentModule: commonStorage.Module) { return { kind: 'category', - name: 'Methods', + name: Blockly.Msg['MRC_CATEGORY_METHODS'], contents, }; } @@ -260,7 +260,7 @@ function getComponentsBlocks(currentModule: commonStorage.Module, hideParams : b // Add the "+ Component" category contents.push({ kind: 'category', - name: '+ Component', + name: Blockly.Msg['MRC_CATEGORY_ADD_COMPONENT'], contents: Component.getAllPossibleComponents(hideParams) }); @@ -283,7 +283,7 @@ function getComponentsBlocks(currentModule: commonStorage.Module, hideParams : b return { kind: 'category', - name: 'Components', + name: Blockly.Msg['MRC_CATEGORY_COMPONENTS'], contents, }; } diff --git a/src/toolbox/lists_category.ts b/src/toolbox/lists_category.ts index dd4ac45e..fc5ed26b 100644 --- a/src/toolbox/lists_category.ts +++ b/src/toolbox/lists_category.ts @@ -1,6 +1,8 @@ -export const category = { +import * as Blockly from 'blockly/core'; + +export const getCategory = () => ({ kind: 'category', - name: 'Lists', + name: Blockly.Msg['MRC_CATEGORY_LISTS'], categorystyle: 'list_category', contents: [ { @@ -151,4 +153,4 @@ export const category = { type: 'lists_reverse', }, ], - } \ No newline at end of file + }); \ No newline at end of file diff --git a/src/toolbox/logic_category.ts b/src/toolbox/logic_category.ts index 500ccb00..34996df6 100644 --- a/src/toolbox/logic_category.ts +++ b/src/toolbox/logic_category.ts @@ -1,7 +1,9 @@ -export const category = +import * as Blockly from 'blockly/core'; + +export const getCategory = () => ( { kind: 'category', - name: 'Logic', + name: Blockly.Msg['MRC_CATEGORY_LOGIC'], categorystyle: 'logic_category', contents: [ { @@ -48,4 +50,4 @@ export const category = type: 'logic_ternary', }, ], -} \ No newline at end of file +}); \ No newline at end of file diff --git a/src/toolbox/loop_category.ts b/src/toolbox/loop_category.ts index 967a954c..62d50ab8 100644 --- a/src/toolbox/loop_category.ts +++ b/src/toolbox/loop_category.ts @@ -1,7 +1,8 @@ -export const category = -{ +import * as Blockly from 'blockly/core'; + +export const getCategory = () => ({ kind: 'category', - name: 'Loops', + name: Blockly.Msg['MRC_CATEGORY_LOOPS'], categorystyle: 'loop_category', contents: [ { @@ -61,4 +62,4 @@ export const category = type: 'controls_flow_statements', }, ], - } \ No newline at end of file + }); \ No newline at end of file diff --git a/src/toolbox/math_category.ts b/src/toolbox/math_category.ts index 277b0cd3..edcc356e 100644 --- a/src/toolbox/math_category.ts +++ b/src/toolbox/math_category.ts @@ -1,7 +1,7 @@ -export const category = -{ +import * as Blockly from 'blockly/core'; +export const getCategory = () => ({ kind: 'category', - name: 'Math', + name: Blockly.Msg['MRC_CATEGORY_MATH'], categorystyle: 'math_category', contents: [ { @@ -351,4 +351,4 @@ export const category = type: 'math_random_float', }, ], -} \ No newline at end of file +}); \ No newline at end of file diff --git a/src/toolbox/methods_category.ts b/src/toolbox/methods_category.ts index 4e72acba..4a2f035e 100644 --- a/src/toolbox/methods_category.ts +++ b/src/toolbox/methods_category.ts @@ -32,12 +32,12 @@ import { ClassMethodDefBlock } from '../blocks/mrc_class_method_def' const CUSTOM_CATEGORY_METHODS = 'METHODS'; -export const category = { +export const getCategory = () => ({ kind: 'category', categorystyle: MRC_CATEGORY_STYLE_METHODS, - name: 'Methods', + name: Blockly.Msg['MRC_CATEGORY_METHODS'], custom: CUSTOM_CATEGORY_METHODS, -}; +}); export class MethodsCategory { private currentModule: commonStorage.Module | null = null; diff --git a/src/toolbox/misc_category.ts b/src/toolbox/misc_category.ts index 758321bb..f7762e82 100644 --- a/src/toolbox/misc_category.ts +++ b/src/toolbox/misc_category.ts @@ -1,7 +1,7 @@ -export const category = -{ +import * as Blockly from 'blockly/core'; +export const getCategory = () => ({ kind: 'category', - name: 'Miscellaneous', + name: Blockly.Msg['MRC_CATEGORY_MISC'], contents: [ { kind: 'block', @@ -15,4 +15,4 @@ export const category = type: 'mrc_misc_evaluate_but_ignore_result', }, ], -} \ No newline at end of file +}); \ No newline at end of file diff --git a/src/toolbox/text_category.ts b/src/toolbox/text_category.ts index 0c58992c..4d4e9637 100644 --- a/src/toolbox/text_category.ts +++ b/src/toolbox/text_category.ts @@ -1,7 +1,7 @@ -export const category = -{ +import * as Blockly from 'blockly/core'; +export const getCategory = () => ({ kind: 'category', - name: 'Text', + name: Blockly.Msg['MRC_CATEGORY_TEXT'], categorystyle: 'text_category', contents: [ { @@ -201,4 +201,4 @@ export const category = }, }, ], -} \ No newline at end of file +}); \ No newline at end of file diff --git a/src/toolbox/toolbox.ts b/src/toolbox/toolbox.ts index 2aec4211..632c77c1 100644 --- a/src/toolbox/toolbox.ts +++ b/src/toolbox/toolbox.ts @@ -2,7 +2,7 @@ import * as Blockly from 'blockly/core'; import * as commonStorage from '../storage/common_storage'; import * as common from './toolbox_common' import { getHardwareCategory } from './hardware_category'; -import { category as eventCategory } from './event_category'; +import { getCategory as eventCategory } from './event_category'; export function getToolboxJSON( shownPythonToolboxCategories: Set | null, @@ -16,7 +16,7 @@ export function getToolboxJSON( getHardwareCategory(currentModule), { kind: 'sep' }, ...common.getToolboxItems(shownPythonToolboxCategories), - eventCategory, + eventCategory(), ] }; case commonStorage.MODULE_TYPE_OPMODE: diff --git a/src/toolbox/toolbox_common.ts b/src/toolbox/toolbox_common.ts index 0cff3f95..08573d7b 100644 --- a/src/toolbox/toolbox_common.ts +++ b/src/toolbox/toolbox_common.ts @@ -21,13 +21,15 @@ import * as robotPyToolbox from './robotpy_toolbox'; import * as toolboxItems from './items'; -import { category as logicCategory } from './logic_category'; -import { category as loopCategory } from './loop_category'; -import { category as mathCategory } from './math_category'; -import { category as textCategory } from './text_category'; -import { category as listsCategory } from './lists_category'; -import { category as miscCategory } from './misc_category'; -import { category as methodsCategory } from './methods_category'; +import * as Blockly from 'blockly/core'; +import { getCategory as logicCategory } from './logic_category'; +import { getCategory as loopCategory } from './loop_category'; +import { getCategory as mathCategory } from './math_category'; +import { getCategory as textCategory } from './text_category'; +import { getCategory as listsCategory } from './lists_category'; +import { getCategory as miscCategory } from './misc_category'; +import { getCategory as methodsCategory } from './methods_category'; + export function getToolboxItems( shownPythonToolboxCategories: Set | null) { @@ -47,22 +49,22 @@ export function getToolboxItems( contents.push.apply( contents, [ - logicCategory, - loopCategory, - mathCategory, - textCategory, - listsCategory, - miscCategory, + logicCategory(), + loopCategory(), + mathCategory(), + textCategory(), + listsCategory(), + miscCategory(), { kind: 'sep', }, { kind: 'category', - name: 'Variables', + name: Blockly.Msg['MRC_CATEGORY_VARIABLES'], categorystyle: 'variable_category', custom: 'VARIABLE', }, - methodsCategory, + methodsCategory(), ], ); return contents; diff --git a/tests/App.test.tsx b/tests/App.test.tsx index e7d64154..1904938c 100644 --- a/tests/App.test.tsx +++ b/tests/App.test.tsx @@ -1,8 +1,28 @@ import { render } from 'vitest-browser-react'; import App from '../src/App.tsx'; -import { /*expect,*/ test } from "vitest"; +import { beforeAll, /*expect,*/ test } from "vitest"; +import i18n from 'i18next'; +import { I18nextProvider, initReactI18next } from 'react-i18next'; + +beforeAll(() => { + // Initialize i18next with a basic configuration for testing + i18n + .use(initReactI18next) + .init({ + lng: 'en', + fallbackLng: 'en', + resources: { + en: { + translation: {} + } + } + }); +}); test('renders app', async () => { - render(); + render( + + + ); // TODO: Add some expect statements. }); diff --git a/vite.config.mts b/vite.config.mts index e332f139..27545602 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -12,6 +12,10 @@ export default defineConfig({ src: 'oss-attribution/**/*', dest: "./", }, + { + src: 'src/i18n/locales/**/*', + dest: "./locales/", + }, ], }), ],