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(沙漏动图)、Flaticon 和 svgrepo 提供。'
+ },
+ 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`