Skip to content

Commit da86f38

Browse files
committed
Language support for blockly
The change of language now works for blockly. The standard blocks get the translation from the blockly install, our custom blocks get their translation from specific i18N files in our utils.
1 parent e7251d2 commit da86f38

12 files changed

Lines changed: 1010 additions & 622 deletions

File tree

src/components/blockly.tsx

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { registerFieldColour } from '@blockly/field-colour';
33
import { BlocklyWorkspace, Workspace } from 'react-blockly';
44
import BlocklyConfigs from '@components/blockly/xrp_blockly_configs';
55
import * as Blockly from 'blockly/core';
6-
import { setBlocklyLocale } from '@/utils/blockly-locales';
6+
import { applyBlocklyLocale, refreshBlocklyWorkspace } from '@/utils/blockly-locales';
77
import AppMgr, { EventType, LoginStatus, Themes } from '@/managers/appmgr';
88
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
99
import { useHotkeys } from 'react-hotkeys-hook';
@@ -214,8 +214,9 @@ function blocklyToPython(ws: Workspace) {
214214
* @returns
215215
*/
216216
function BlocklyEditor({ tabId, tabName }: BlocklyEditorProps) {
217-
/** Bumped when the shared toolbox JSON is updated (e.g. after XRP connect). */
217+
/** Bumped when the shared toolbox JSON is updated (e.g. after XRP connect or language change). */
218218
const [toolboxRevision, setToolboxRevision] = useState(0);
219+
const [language, setLanguage] = useState(i18n.language);
219220
const [isLoading, setIsLoading] = useState<boolean>(true);
220221
const [name, setName] = useState<string>(tabName);
221222
const nameRef = useRef(name);
@@ -259,8 +260,8 @@ function BlocklyEditor({ tabId, tabName }: BlocklyEditorProps) {
259260

260261
// Shallow copy gives react-blockly a new reference so its updateToolbox effect runs.
261262
const toolboxConfiguration = useMemo(
262-
() => ({ ...BlocklyConfigs.ToolboxJson }),
263-
[toolboxRevision],
263+
() => ({ ...BlocklyConfigs.getLocalizedToolboxJson() }) as typeof BlocklyConfigs.ToolboxJson,
264+
[toolboxRevision, language],
264265
);
265266

266267
useEffect(() => {
@@ -438,17 +439,27 @@ function BlocklyEditor({ tabId, tabName }: BlocklyEditorProps) {
438439
}
439440
}
440441

441-
// Set up language based on stored preference
442-
const setupLanguage = () => {
443-
const storedLanguage = localStorage.getItem(StorageKeys.LANGUAGE) || 'en';
444-
setBlocklyLocale(storedLanguage);
445-
};
446-
447-
// Call setupLanguage to initialize Blockly locale
448-
setupLanguage();
442+
applyBlocklyLocale(i18n.language);
449443

450444
}, [saveEditor, tabId, restoreViewportForTab, persistViewport, evaluateEditBlocked]);
451445

446+
useEffect(() => {
447+
const handleLanguageChanged = (lang: string) => {
448+
applyBlocklyLocale(lang);
449+
setLanguage(lang);
450+
setToolboxRevision((prev) => prev + 1);
451+
const ws = EditorMgr.getInstance().getEditorSession(tabId)?.workspace;
452+
if (ws) {
453+
refreshBlocklyWorkspace(ws);
454+
}
455+
};
456+
457+
i18n.on('languageChanged', handleLanguageChanged);
458+
return () => {
459+
i18n.off('languageChanged', handleLanguageChanged);
460+
};
461+
}, [tabId]);
462+
452463
useEffect(() => {
453464
evaluateEditBlocked();
454465
}, [evaluateEditBlocked]);

src/components/blockly/xrp_blockly_configs.ts

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,53 @@
11

22
import '@components/blockly/xrp_blocks';
3-
import '@components/blockly/xrp_blocks_python'
3+
import '@components/blockly/xrp_blocks_python';
4+
import i18n from '@/utils/i18n';
45

56
const InitialJson = {};
67

8+
export const BlocklyCategoryIds = {
9+
INDIVIDUAL_MOTORS: 'individualMotors',
10+
DRIVE_TRAIN: 'driveTrain',
11+
SERVOS: 'servos',
12+
SENSORS: 'sensors',
13+
REFLECTANCE: 'reflectance',
14+
CONTROL_BOARD: 'controlBoard',
15+
GAMEPAD: 'gamepad',
16+
THIRD_PARTY: 'thirdParty',
17+
} as const;
18+
19+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
20+
type ToolboxCategory = Record<string, any>;
21+
22+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
23+
function localizeToolboxCategory(category: ToolboxCategory): ToolboxCategory {
24+
const localized = { ...category };
25+
if (localized.categoryId) {
26+
localized.name = i18n.t(`blockly.toolbox.${localized.categoryId}`);
27+
}
28+
if (Array.isArray(localized.contents)) {
29+
localized.contents = localized.contents.map((item: ToolboxCategory) =>
30+
item.kind === 'CATEGORY' ? localizeToolboxCategory(item) : item,
31+
);
32+
}
33+
return localized;
34+
}
35+
36+
export function getLocalizedToolboxJson() {
37+
return {
38+
...ToolboxJson,
39+
contents: (ToolboxJson.contents as ToolboxCategory[]).map((item) =>
40+
item.kind === 'CATEGORY' ? localizeToolboxCategory(item) : item,
41+
),
42+
};
43+
}
44+
745
const ToolboxJson = {
846
kind: "categoryToolbox",
947
"contents": [
1048
{
1149
"kind": "CATEGORY",
50+
"categoryId": "individualMotors",
1251
"name": "Individual Motors",
1352
"colour": "#a55b65", // crimson red
1453
"contents": [
@@ -50,6 +89,7 @@ const ToolboxJson = {
5089
},
5190
{
5291
"kind": "CATEGORY",
92+
"categoryId": "driveTrain",
5393
"name": "DriveTrain",
5494
"colour": "#a5675b", // rust orange
5595
"contents": [
@@ -113,6 +153,7 @@ const ToolboxJson = {
113153
},
114154
{
115155
"kind": "CATEGORY",
156+
"categoryId": "servos",
116157
"name": "Servos",
117158
"colour": "#a55ba5", // purple/pink
118159
"contents": [
@@ -127,11 +168,13 @@ const ToolboxJson = {
127168
},
128169
{
129170
"kind": "CATEGORY",
171+
"categoryId": "sensors",
130172
"name": "Sensors",
131173
"colour": "#80a55b", // LIGHT GREEN
132174
"contents": [
133175
{
134176
"kind": "CATEGORY",
177+
"categoryId": "distance",
135178
"name": "Distance",
136179
"colour": "#80a55b",
137180
"contents": [
@@ -142,6 +185,7 @@ const ToolboxJson = {
142185
]},
143186
{
144187
"kind": "CATEGORY",
188+
"categoryId": "reflectance",
145189
"name": "Reflectance",
146190
"colour": "#80a55b",
147191
"contents": [
@@ -156,6 +200,7 @@ const ToolboxJson = {
156200
]},
157201
{
158202
"kind": "CATEGORY",
203+
"categoryId": "gyro",
159204
"name": "Gyro",
160205
"colour": "#80a55b",
161206
"contents": [
@@ -174,6 +219,7 @@ const ToolboxJson = {
174219
]},
175220
{
176221
"kind": "CATEGORY",
222+
"categoryId": "accelerometer",
177223
"name": "Accelerometer",
178224
"colour": "#80a55b",
179225
"contents": [
@@ -194,6 +240,7 @@ const ToolboxJson = {
194240
},
195241
{
196242
"kind": "CATEGORY",
243+
"categoryId": "controlBoard",
197244
"name": "Control Board",
198245
"colour": "#5ba580", // cool green
199246
"contents": [
@@ -217,6 +264,7 @@ const ToolboxJson = {
217264
},
218265
{
219266
"kind": "CATEGORY",
267+
"categoryId": "webServer",
220268
"name": "Web Server",
221269
"colour": "#5b99a5", // turquoise
222270
"contents": [
@@ -264,6 +312,7 @@ const ToolboxJson = {
264312
]
265313
},{
266314
"kind": "CATEGORY",
315+
"categoryId": "dashboard",
267316
"name": "Dashboard",
268317
"colour": "#0080ff", // bright blue
269318
"contents": [
@@ -291,6 +340,7 @@ const ToolboxJson = {
291340
},
292341
{
293342
"kind": "CATEGORY",
343+
"categoryId": "gamepad",
294344
"name": "Gamepad",
295345
"colour": "#ff9248", // turquoise
296346
"contents": [
@@ -346,6 +396,7 @@ const ToolboxJson = {
346396
"type": "logic_ternary"
347397
}
348398
],
399+
"categoryId": "logic",
349400
"name": "Logic",
350401
"colour": "#5b80a5" // slate blue
351402
},
@@ -383,6 +434,7 @@ const ToolboxJson = {
383434
"type": "controls_flow_statements"
384435
}
385436
],
437+
"categoryId": "loops",
386438
"name": "Loops",
387439
"colour": "#5ba55b" // grass green
388440
},
@@ -449,6 +501,7 @@ const ToolboxJson = {
449501
"type": "math_random_float"
450502
}
451503
],
504+
"categoryId": "math",
452505
"name": "Math",
453506
"colour": "#5b67a5" // indigo blue
454507
},
@@ -520,6 +573,7 @@ const ToolboxJson = {
520573
"type": "text_prompt_ext"
521574
}
522575
],
576+
"categoryId": "text",
523577
"name": "Text",
524578
"colour": "#5ba58c" // seafoam green
525579
},
@@ -582,17 +636,20 @@ const ToolboxJson = {
582636
"type": "lists_sort"
583637
}
584638
],
639+
"categoryId": "lists",
585640
"name": "Lists",
586641
"colour": "#745ba5" // eggplant purple
587642
},
588643
{
589644
"kind": "CATEGORY",
645+
"categoryId": "variables",
590646
"name": "Variables",
591647
"colour": "#a55b80", // fuschia
592648
"custom": "VARIABLE"
593649
},
594650
{
595651
"kind": "CATEGORY",
652+
"categoryId": "functions",
596653
"name": "Functions",
597654
"colour": "#995ba5", // purple
598655
"custom": "PROCEDURE"
@@ -608,7 +665,9 @@ const ToolboxJson = {
608665

609666
const BlocklyConfigs = {
610667
InitialJson,
611-
ToolboxJson
612-
}
668+
ToolboxJson,
669+
getLocalizedToolboxJson,
670+
BlocklyCategoryIds,
671+
};
613672

614673
export default BlocklyConfigs;

0 commit comments

Comments
 (0)