diff --git a/frontend/index.html b/frontend/index.html index f16c096..0b390d6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -23,6 +23,7 @@ RailBlocks + @@ -44,14 +45,7 @@

RailBlocks

diff --git a/frontend/localization.js b/frontend/localization.js index ffff149..0823e16 100644 --- a/frontend/localization.js +++ b/frontend/localization.js @@ -19,14 +19,16 @@ import * as Blockly from 'blockly/core' import { en_locale } from '../locales/en/en_locale' import { de_locale } from '../locales/de/de_locale' +import { zh_locale } from '../locales/zh/zh_locale' const LANGUAGE_STORAGE_KEY = 'railblocks.language' // This file contains constants related to localization, such as retrieving the correct labels for the current language and applying the selected language to the Blockly editor and the page. -const LANGUAGE_CONFIGS = { +export const LANGUAGE_CONFIGS = { en: en_locale, - de: de_locale + de: de_locale, + zh: zh_locale } /** diff --git a/frontend/main.js b/frontend/main.js index 08c90f2..8e34067 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -23,7 +23,7 @@ import './renderer.js' import './dynamic_blocks.js' import { blockDefinitionsJson, createToolbox } from './blocks.js' import { compile } from './generator.js' -import { getLabel, getToolBoxLabels, getHtmlLabels, getStoredLanguage, applyLanguage, LANGUAGE_STORAGE_KEY } from './localization.js' +import { getLabel, getToolBoxLabels, getHtmlLabels, getStoredLanguage, applyLanguage, LANGUAGE_STORAGE_KEY, LANGUAGE_CONFIGS } from './localization.js' // MAIN PROGRAM const workspaceStorageKey = 'railblocks.workspace' @@ -53,11 +53,42 @@ const theme = Blockly.Theme.defineTheme('theme', { // language menu elements const languageMenu = document.getElementById('language_menu') +const languageOptionsContainer = document.getElementById('language_options') const languageButton = document.getElementById('button_language') const simulationButton = document.getElementById('button_sim') const runButton = document.getElementById('button_run') const settingsButton = document.getElementById('button_options') const saveButton = document.getElementById('button_save') + +/** + * Creates a language option button for the language switcher. + * @param {String} languageId The id of the language to render. + * @param {Object} languageConfig The config entry for the language. + * @returns {HTMLButtonElement} The rendered language button. + */ +function createLanguageOptionButton (languageId, languageConfig) { + const button = document.createElement('button') + button.type = 'button' + button.className = 'language_option' + button.dataset.language = languageId + button.setAttribute('aria-pressed', 'false') + + const marker = document.createElement('span') + marker.className = 'language_option_marker' + marker.setAttribute('aria-hidden', 'true') + + const label = document.createElement('span') + label.className = 'language_option_label' + label.textContent = languageConfig.label + + button.append(marker, label) + return button +} + +Object.entries(LANGUAGE_CONFIGS).forEach(([languageId, languageConfig]) => { + languageOptionsContainer.appendChild(createLanguageOptionButton(languageId, languageConfig)) +}) + const languageOptions = Array.from(document.querySelectorAll('.language_option')) /** @@ -74,8 +105,13 @@ function applyHtmlLabels (languageId) { settingsButton.title = labels.optionsTitle saveButton.title = labels.saveTitle document.getElementById('editorLanguage').textContent = labels.languageMenuLabel - document.getElementById('language_de_label').textContent = getLabel('de') - document.getElementById('language_en_label').textContent = getLabel('en') + languageOptions.forEach(option => { + const languageId = option.dataset.language + const label = option.querySelector('.language_option_label') + if (label && languageId) { + label.textContent = getLabel(languageId) + } + }) document.getElementById('fileLoadLabel').title = labels.loadTitle document.getElementById("generatedCodeTitle").textContent = labels.generatedCodeTitle document.getElementById("logsTitle").textContent = labels.logsTitle diff --git a/locales/de/de_locale.js b/locales/de/de_locale.js index 6927ae0..ff8df4a 100644 --- a/locales/de/de_locale.js +++ b/locales/de/de_locale.js @@ -32,8 +32,8 @@ export const de_locale = { generatedCodeTitle: 'Generierter RailSL', logsTitle: 'Protokolle', deployConfirm: 'Wirklich auf der Anlage ausführen?', - saveTitle: 'Aktuellen Arbeitsbereich auf der Festplatte speichern', - loadTitle: 'Arbeitsbereich von der Festplatte laden', + saveTitle: 'Aktuellen Arbeitsbereich speichern', + loadTitle: 'Arbeitsbereich laden', attributionsTitle: 'Danksagungen', attributionsText: 'Bilder erstellt und verteilt von user jucy_fish (Zugsymbol), Freepik (Sanduhr-GIF), Flaticon und svgrepo.' }, diff --git a/locales/de/de_tokens.js b/locales/de/de_tokens.js index 0f34655..52d22ed 100644 --- a/locales/de/de_tokens.js +++ b/locales/de/de_tokens.js @@ -71,7 +71,7 @@ export const de_tokens = { RAILBLOCKS_POINT_TEXT_START: 'Setze die Weiche', RAILBLOCKS_POINT_TEXT_END: 'auf', - RAILBLOCKS_POINT_TOOLTIP: 'Setze alle ausgewählten Weichen auf entweder abbiegen oder geradeaus.\n' + 'Klicke auf das Plus / Minus Symbol, um die Anzahl der Weichen zu steuern.', + RAILBLOCKS_POINT_TOOLTIP: 'Setze alle ausgewählten Weichen auf abbiegen oder geradeaus.\n' + 'Klicke auf das Plus / Minus Symbol, um die Anzahl der Weichen zu steuern.', RAILBLOCKS_POINT_STRAIGHT: 'geradeaus', RAILBLOCKS_POINT_BRANCH: 'abbiegen', diff --git a/locales/en/en_locale.js b/locales/en/en_locale.js index 4347367..572859c 100644 --- a/locales/en/en_locale.js +++ b/locales/en/en_locale.js @@ -32,8 +32,8 @@ export const en_locale = { generatedCodeTitle: 'Generated RailSL', logsTitle: 'Logs', deployConfirm: 'Really deploy on the railway?', - saveTitle: 'Save current workspace to disk', - loadTitle: 'Load workspace from disk', + saveTitle: 'Save current workspace', + loadTitle: 'Load workspace', attributionsTitle: 'Attributions', attributionsText: 'Images created and distributed by user jucy_fish (train icon), Freepik (hourglass gif), Flaticon and svgrepo.' }, diff --git a/locales/en/en_tokens.js b/locales/en/en_tokens.js index eb49cdd..792da9c 100644 --- a/locales/en/en_tokens.js +++ b/locales/en/en_tokens.js @@ -71,12 +71,12 @@ export const en_tokens = { RAILBLOCKS_POINT_TEXT_START: 'Set point', RAILBLOCKS_POINT_TEXT_END: 'to', - RAILBLOCKS_POINT_TOOLTIP: 'Sets a number of points to either branch or be straight.\n' + 'Click the plus/minus symbol to control the number of points.', + RAILBLOCKS_POINT_TOOLTIP: 'Sets a number of points to either branch or straight.\n' + 'Click the plus/minus symbol to control the number of points.', RAILBLOCKS_POINT_STRAIGHT: 'straight', RAILBLOCKS_POINT_BRANCH: 'branch', - RAILBLOCKS_WARNING_UNREACHABLE_STRONG: 'Blocks after this will not be reached because of a loop inside this.', - RAILBLOCKS_WARNING_UNREACHABLE_WEAK: 'Blocks after this may not be reached because of a loop inside this.', + RAILBLOCKS_WARNING_UNREACHABLE_STRONG: 'Blocks after this will not be reached because of a loop inside this block.', + RAILBLOCKS_WARNING_UNREACHABLE_WEAK: 'Blocks after this may not be reached because of a loop inside this block.', RAILBLOCKS_WARNING_EMPTY_INPUT: 'This block has empty inputs and will cause a syntax error!', RAILBLOCKS_WARNING_UNUSED: 'Unused block', } diff --git a/locales/zh/zh_locale.js b/locales/zh/zh_locale.js new file mode 100644 index 0000000..24db324 --- /dev/null +++ b/locales/zh/zh_locale.js @@ -0,0 +1,46 @@ +/* + * RailBlocks - A Blockly RailSL Implementation + * + * https://github.com/kieler/RailBlocks + * + * Copyright 2026 by + * + Kiel University and others + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made + * available under the terms of the MIT License which + * is available at https://opensource.org/license/MIT. + * + * SPDX-License-Identifier: MIT + */ + +import * as ZhHans from 'blockly/msg/zh-hans' +import { zh_tokens } from './zh_tokens' + +export const zh_locale = { + label: '简体中文', + locale: ZhHans, + tokens: zh_tokens, + htmlLabels: { + title: 'RailBlocks', + simulationTitle: '仿真', + deployTitle: '部署到铁路', + optionsTitle: '查看开发者信息', + languageButton: '语言', + languageMenuLabel: '编辑器语言', + generatedCodeTitle: '生成的 RailSL', + logsTitle: '日志', + deployConfirm: '确定要部署到铁路吗?', + saveTitle: '保存当前工作区', + loadTitle: '加载工作区', + attributionsTitle: '鸣谢', + attributionsText: + '图片由用户 jucy_fish(火车图标)、Freepik(沙漏动图)、Flaticonsvgrepo 提供。' + }, + toolboxLabels: { + setStatements: '设置语句', + waitStatements: '等待语句', + controlFlow: '控制流程' + } +} diff --git a/locales/zh/zh_tokens.js b/locales/zh/zh_tokens.js new file mode 100644 index 0000000..160c3dd --- /dev/null +++ b/locales/zh/zh_tokens.js @@ -0,0 +1,100 @@ +/* + * RailBlocks - A Blockly RailSL Implementation + * + * https://github.com/kieler/RailBlocks + * + * Copyright 2026 by + * + Kiel University and others + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This program and the accompanying materials are made + * available under the terms of the MIT License which + * is available at https://opensource.org/license/MIT. + * + * SPDX-License-Identifier: MIT + */ + +// This file contains the Simplified Chinese translations for the block labels, tooltips and warnings. +export const zh_tokens = { + RAILBLOCKS_PROGRAM_TEXT: '程序', + RAILBLOCKS_PROGRAM_TOOLTIP: + '需要执行的程序。\n所有后续命令都应放在此块中。', + + RAILBLOCKS_LOOP_TEXT: '循环', + RAILBLOCKS_LOOP_TOOLTIP: + '重复执行此块中的所有代码。', + + RAILBLOCKS_STOP_TEXT: '停止', + RAILBLOCKS_STOP_TOOLTIP: + '将轨道设为无速度。', + + RAILBLOCKS_DIR_TEXT: '速度:%1 反向:%2', + RAILBLOCKS_DIR_TOOLTIP: + '设置轨道的速度和方向。', + RAILBLOCKS_DIR_SLOW_TEXT: '慢速', + RAILBLOCKS_DIR_FULL_TEXT: '全速', + + RAILBLOCKS_CONTACT_WAIT_TEXT: + '等待直到 %3 的第 %2 个接触点被%1', + RAILBLOCKS_CONTACT_WAIT_TOOLTIP: + '等待列车与轨道发生交互。', + RAILBLOCKS_CONTACT_WAIT_REACHED: '到达', + RAILBLOCKS_CONTACT_WAIT_PASSED: '通过', + RAILBLOCKS_CONTACT_WAIT_FIRST: '一', + RAILBLOCKS_CONTACT_WAIT_SECOND: '二', + + RAILBLOCKS_TIME_WAIT_TEXT: + '等待 %1 秒', + RAILBLOCKS_TIME_WAIT_TOOLTIP: + '等待指定时间。', + + RAILBLOCKS_CROSSING_TEXT: '%1 道口', + RAILBLOCKS_CROSSING_TOOLTIP: + '切换道口状态', + RAILBLOCKS_CROSSING_OPEN: '打开', + RAILBLOCKS_CROSSING_CLOSE: '关闭', + + RAILBLOCKS_CONDITIONAL_TEXT: + '如果 %2 的第 %1 个接触点最先被到达\n%3否则如果 %5 的第 %4 个接触点最先被到达', + RAILBLOCKS_CONDITIONAL_TOOLTIP: + '根据列车最先到达的轨道执行不同的代码块。', + RAILBLOCKS_CONDITIONAL_FIRST: '一', + RAILBLOCKS_CONDITIONAL_SECOND: '二', + RAILBLOCKS_CONDITIONAL_TEXT_START: '如果', + RAILBLOCKS_CONDITIONAL_TEXT_MIDDLE: '的接触点', + RAILBLOCKS_CONDITIONAL_TEXT_END: '最先被到达', + RAILBLOCKS_CONDITIONAL_TITLE_TEXT: '分支', + + RAILBLOCKS_PARALLEL_TEXT: '并行', + RAILBLOCKS_PARALLEL_TOOLTIP: + '同时执行多个代码块。', + + RAILBLOCKS_LIGHTS_TEXT_START: '把灯', + RAILBLOCKS_LIGHTS_TEXT_END: '设为', + RAILBLOCKS_LIGHTS_TOOLTIP: + '将一个或多个灯设置为开启或关闭。\n点击加号/减号可调整灯数量。', + RAILBLOCKS_LIGHTS_ON: '开启', + RAILBLOCKS_LIGHTS_OFF: '关闭', + + RAILBLOCKS_TRACK_TEXT_START: '将轨道', + RAILBLOCKS_TRACK_TEXT_END: '设为', + RAILBLOCKS_TRACK_TOOLTIP: + '设置一个或多个轨道的速度和方向。\n点击加号/减号可调整轨道数量。', + + RAILBLOCKS_POINT_TEXT_START: '将道岔', + RAILBLOCKS_POINT_TEXT_END: '设为', + RAILBLOCKS_POINT_TOOLTIP: + '将一个或多个道岔设置为直行或分岔。\n点击加号/减号可调整道岔数量。', + RAILBLOCKS_POINT_STRAIGHT: '直行', + RAILBLOCKS_POINT_BRANCH: '分岔', + + RAILBLOCKS_WARNING_UNREACHABLE_STRONG: + '由于此处代码块包含循环,后续代码块将无法执行。', + RAILBLOCKS_WARNING_UNREACHABLE_WEAK: + '由于此处代码块包含循环,后续代码块可能无法执行。', + RAILBLOCKS_WARNING_EMPTY_INPUT: + '此代码块存在空输入,将导致语法错误!', + RAILBLOCKS_WARNING_UNUSED: + '未使用的代码块' +} diff --git a/test/validate_translation_consistency.test.js b/test/validate_translation_consistency.test.js index 37d6504..8f9f4ad 100644 --- a/test/validate_translation_consistency.test.js +++ b/test/validate_translation_consistency.test.js @@ -1,9 +1,6 @@ import { describe, test, expect } from 'vitest'; -import { en_locale } from '../locales/en/en_locale.js'; -import { de_locale } from '../locales/de/de_locale.js'; - -const translations = { en_locale, de_locale }; +import { LANGUAGE_CONFIGS } from '../frontend/localization.js'; function compare(reference, translation, path = '') { const missing = []; @@ -59,8 +56,8 @@ function findNonStrings(obj, path = '') { describe('Localization', () => { test('all localization files contain the same keys', () => { - for (const [name, translation] of Object.entries(translations)) { - const { missing, extra } = compare(en_locale, translation); + for (const [name, translation] of Object.entries(LANGUAGE_CONFIGS)) { + const { missing, extra } = compare(LANGUAGE_CONFIGS.en, translation); expect( { missing, extra }, @@ -73,7 +70,7 @@ describe('Localization', () => { }); test('all translation values are strings', () => { - for (const [name, translation] of Object.entries(translations)) { + for (const [name, translation] of Object.entries(LANGUAGE_CONFIGS)) { expect( findNonStrings(translation), `${name} contains non-string values`