Skip to content
Merged
4 changes: 2 additions & 2 deletions frontend/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ const blockDefinitionsJson = [
message0: "%{BKY_RAILBLOCKS_CONTACT_WAIT_REACHED}",
output: "Boolean",
colour: 155,
tooltip: "Returns true if reached"
tooltip: "%{BKY_RAILBLOCKS_CONTACT_WAIT_REACHED_TOOLTIP}"
},
// Passed track sensor value
// Shows that we passed the track sensor.
Expand All @@ -280,7 +280,7 @@ const blockDefinitionsJson = [
message0: "%{BKY_RAILBLOCKS_CONTACT_WAIT_PASSED}",
output: "Boolean",
colour: 155,
tooltip: "Returns true if passed"
tooltip: "%{BKY_RAILBLOCKS_CONTACT_WAIT_PASSED_TOOLTIP}"
},
// Integer range
// Is a range of integer numbers from start to end inclusive
Expand Down
10 changes: 2 additions & 8 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<title> RailBlocks </title>
<link rel="stylesheet" href="style.css">
<link rel="icon" href="icons/favicon.png" type="image/x-icon">
<meta charset="UTF-8">
</head>

<body>
Expand All @@ -44,14 +45,7 @@ <h1> RailBlocks </h1>
</button>
<div id="language_menu" style="display: none;" class="language_menu_panel">
<span id="editorLanguage" class="language_menu_label">Editor language</span>
<button type="button" class="language_option" data-language="de">
<span class="language_option_marker" aria-hidden="true"></span>
<span id="language_de_label">Deutsch</span>
</button>
<button type="button" class="language_option" data-language="en">
<span class="language_option_marker" aria-hidden="true"></span>
<span id="language_en_label">English</span>
</button>
<div id="language_options"></div>
</div>
</div>

Expand Down
6 changes: 4 additions & 2 deletions frontend/localization.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down
42 changes: 39 additions & 3 deletions frontend/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import './dynamic_blocks.js'
import './validators.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'
Expand Down Expand Up @@ -54,11 +54,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'))

/**
Expand All @@ -75,8 +106,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
Expand Down
4 changes: 2 additions & 2 deletions locales/de/de_locale.js
Original file line number Diff line number Diff line change
Expand Up @@ -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), <a href="https://www.flaticon.com/" title="icons source 1">Flaticon</a> und <a href="https://www.svgrepo.com" title="icons source 2">svgrepo</a>.'
},
Expand Down
4 changes: 3 additions & 1 deletion locales/de/de_tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export const de_tokens = {
RAILBLOCKS_CONTACT_WAIT_TEXT: 'Warte bis %2 Kontakt von %3 %1 wurde',
RAILBLOCKS_CONTACT_WAIT_TOOLTIP: 'Tue nichts bis ein Zug mit einem Gleis interagiert.',
RAILBLOCKS_CONTACT_WAIT_REACHED: 'erreicht',
RAILBLOCKS_CONTACT_WAIT_REACHED_TOOLTIP: 'Gibt wahr zurück wenn der Kontakt erreicht wird.',
RAILBLOCKS_CONTACT_WAIT_PASSED: 'passiert',
RAILBLOCKS_CONTACT_WAIT_PASSED_TOOLTIP: 'Gibt wahr zurück wenn der Kontakt passiert wurde.',
RAILBLOCKS_CONTACT_WAIT_FIRST: 'erster',
RAILBLOCKS_CONTACT_WAIT_SECOND: 'zweiter',

Expand Down Expand Up @@ -75,7 +77,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',

Expand Down
4 changes: 2 additions & 2 deletions locales/en/en_locale.js
Original file line number Diff line number Diff line change
Expand Up @@ -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), <a href="https://www.flaticon.com/" title="icons source 1">Flaticon</a> and <a href="https://www.svgrepo.com" title="icons source 2">svgrepo</a>.'
},
Expand Down
8 changes: 5 additions & 3 deletions locales/en/en_tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export const en_tokens = {
RAILBLOCKS_CONTACT_WAIT_TEXT: 'Wait until %2 contact of %3 is %1',
RAILBLOCKS_CONTACT_WAIT_TOOLTIP: 'Do nothing until a train interacts with a track.',
RAILBLOCKS_CONTACT_WAIT_REACHED: 'reached',
RAILBLOCKS_CONTACT_WAIT_REACHED_TOOLTIP: 'Returns true if reached',
RAILBLOCKS_CONTACT_WAIT_PASSED: 'passed',
RAILBLOCKS_CONTACT_WAIT_PASSED_TOOLTIP: 'Returns true if passed',
RAILBLOCKS_CONTACT_WAIT_FIRST: 'first',
RAILBLOCKS_CONTACT_WAIT_SECOND: 'second',

Expand Down Expand Up @@ -75,12 +77,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',
RAILBLOCKS_WARNING_PASSED: 'The "passed" block can currently not be used in this block.',
Expand Down
48 changes: 48 additions & 0 deletions locales/zh/zh_locale.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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(沙漏动图)、<a href="https://www.flaticon.com/" title="icons source 1">Flaticon</a> 和 <a href="https://www.svgrepo.com" title="icons source 2">svgrepo</a> 提供。'
},
toolboxLabels: {
setStatements: '设置语句',
waitStatements: '等待语句',
controlFlow: '控制流程',
sensorValues: '轨道传感器值',
numbers: '数字',
}
}
108 changes: 108 additions & 0 deletions locales/zh/zh_tokens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* 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_REACHED_TOOLTIP: '如果已「到达」,则返回真值。',
RAILBLOCKS_CONTACT_WAIT_PASSED: '通过',
RAILBLOCKS_CONTACT_WAIT_PASSED_TOOLTIP: '如果已「通过」,则返回真值。',
RAILBLOCKS_CONTACT_WAIT_FIRST: '第一',
RAILBLOCKS_CONTACT_WAIT_SECOND: '第二',

RAILBLOCKS_INT_RANGE_TEXT: '%1 至 %2',
RAILBLOCKS_INT_RANGE_TOOLTIP: '创建一个数字范围。',

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_1: '被',
RAILBLOCKS_CONDITIONAL_TEXT_END_2: '优先',
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: '将轨道 %1 设为 %2',
RAILBLOCKS_TRACK_NONE: '(空)',
RAILBLOCKS_TRACK_TOOLTIP:
'设置一个或多个轨道的速度和方向。',

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:
'未使用的代码块',
RAILBLOCKS_WARNING_PASSED: '「通过」块目前无法在此方块中使用。',
RAILBLOCKS_WARNING_UNSELECTED: '未选择任何轨道。',
}
11 changes: 4 additions & 7 deletions test/validate_translation_consistency.test.js
Original file line number Diff line number Diff line change
@@ -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 = [];
Expand Down Expand Up @@ -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 },
Expand All @@ -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`
Expand Down