From 412c2f44d8d0a7cd330efa86307bfc71fcbdcbcb Mon Sep 17 00:00:00 2001 From: Max Kasperowski Date: Fri, 3 Jul 2026 13:55:22 +0200 Subject: [PATCH 1/5] add simplified chinese language option --- frontend/index.html | 5 + frontend/localization.js | 4 +- locales/de/de_locale.js | 4 +- locales/de/de_tokens.js | 2 +- locales/en/en_locale.js | 4 +- locales/en/en_tokens.js | 6 +- locales/zh/zh_locale.js | 46 ++++++++ locales/zh/zh_tokens.js | 101 ++++++++++++++++++ test/validate_translation_consistency.test.js | 3 +- 9 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 locales/zh/zh_locale.js create mode 100644 locales/zh/zh_tokens.js diff --git a/frontend/index.html b/frontend/index.html index f16c096..fcb433e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -23,6 +23,7 @@ RailBlocks + @@ -52,6 +53,10 @@

RailBlocks

English + diff --git a/frontend/localization.js b/frontend/localization.js index ffff149..eb53555 100644 --- a/frontend/localization.js +++ b/frontend/localization.js @@ -19,6 +19,7 @@ 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' @@ -26,7 +27,8 @@ 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 = { en: en_locale, - de: de_locale + de: de_locale, + zh: zh_locale } /** 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..6d95d5d --- /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..e987426 --- /dev/null +++ b/locales/zh/zh_tokens.js @@ -0,0 +1,101 @@ +/* + * RailBlocks - A Blockly RailSL Implementation + * + * https://github.com/kieler/RailBlocks + * + * Copyright 2026 by + * + Tokessa Hamann and + * + Kiel University + * + 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..144719d 100644 --- a/test/validate_translation_consistency.test.js +++ b/test/validate_translation_consistency.test.js @@ -2,8 +2,9 @@ import { describe, test, expect } from 'vitest'; import { en_locale } from '../locales/en/en_locale.js'; import { de_locale } from '../locales/de/de_locale.js'; +import { zh_locale } from '../locales/zh/zh_locale.js'; -const translations = { en_locale, de_locale }; +const translations = { en_locale, de_locale, zh_locale }; function compare(reference, translation, path = '') { const missing = []; From 2393010f70cf14eb68d44b7c611afec2bc9e92c0 Mon Sep 17 00:00:00 2001 From: Max Kasperowski Date: Fri, 3 Jul 2026 14:12:25 +0200 Subject: [PATCH 2/5] fix issues --- frontend/index.html | 2 +- frontend/main.js | 1 + locales/zh/zh_locale.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index fcb433e..0ab33e2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -55,7 +55,7 @@

RailBlocks

diff --git a/frontend/main.js b/frontend/main.js index 08c90f2..4afe4f3 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -76,6 +76,7 @@ function applyHtmlLabels (languageId) { document.getElementById('editorLanguage').textContent = labels.languageMenuLabel document.getElementById('language_de_label').textContent = getLabel('de') document.getElementById('language_en_label').textContent = getLabel('en') + document.getElementById('language_zh_label').textContent = getLabel('zh') document.getElementById('fileLoadLabel').title = labels.loadTitle document.getElementById("generatedCodeTitle").textContent = labels.generatedCodeTitle document.getElementById("logsTitle").textContent = labels.logsTitle diff --git a/locales/zh/zh_locale.js b/locales/zh/zh_locale.js index 6d95d5d..24db324 100644 --- a/locales/zh/zh_locale.js +++ b/locales/zh/zh_locale.js @@ -32,7 +32,7 @@ export const zh_locale = { generatedCodeTitle: '生成的 RailSL', logsTitle: '日志', deployConfirm: '确定要部署到铁路吗?', - saveTitle: '保存将当前工作区', + saveTitle: '保存当前工作区', loadTitle: '加载工作区', attributionsTitle: '鸣谢', attributionsText: From 788ec073d9a4c7a4d884d47f21d62ada5187a8fb Mon Sep 17 00:00:00 2001 From: Max Kasperowski Date: Fri, 3 Jul 2026 14:28:10 +0200 Subject: [PATCH 3/5] automatically create language selection buttons --- frontend/index.html | 13 +----------- frontend/localization.js | 2 +- frontend/main.js | 43 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 0ab33e2..0b390d6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -45,18 +45,7 @@

RailBlocks

diff --git a/frontend/localization.js b/frontend/localization.js index eb53555..0823e16 100644 --- a/frontend/localization.js +++ b/frontend/localization.js @@ -25,7 +25,7 @@ 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, zh: zh_locale diff --git a/frontend/main.js b/frontend/main.js index 4afe4f3..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,9 +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') - document.getElementById('language_zh_label').textContent = getLabel('zh') + 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 From 38c833fad368db00b73994804c810b6be9e957d6 Mon Sep 17 00:00:00 2001 From: Max Kasperowski Date: Fri, 3 Jul 2026 14:29:49 +0200 Subject: [PATCH 4/5] automatically test all registered locales --- test/validate_translation_consistency.test.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/test/validate_translation_consistency.test.js b/test/validate_translation_consistency.test.js index 144719d..8f9f4ad 100644 --- a/test/validate_translation_consistency.test.js +++ b/test/validate_translation_consistency.test.js @@ -1,10 +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'; -import { zh_locale } from '../locales/zh/zh_locale.js'; - -const translations = { en_locale, de_locale, zh_locale }; +import { LANGUAGE_CONFIGS } from '../frontend/localization.js'; function compare(reference, translation, path = '') { const missing = []; @@ -60,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 }, @@ -74,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` From 4fd18909e0b89fdcf33a69b537c9ef553c963d10 Mon Sep 17 00:00:00 2001 From: Max Kasperowski Date: Fri, 3 Jul 2026 14:31:07 +0200 Subject: [PATCH 5/5] fix license header --- locales/zh/zh_tokens.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/locales/zh/zh_tokens.js b/locales/zh/zh_tokens.js index e987426..160c3dd 100644 --- a/locales/zh/zh_tokens.js +++ b/locales/zh/zh_tokens.js @@ -4,8 +4,7 @@ * https://github.com/kieler/RailBlocks * * Copyright 2026 by - * + Tokessa Hamann and - * + Kiel University + * + Kiel University and others * + Department of Computer Science * + Real-Time and Embedded Systems Group *