Skip to content

Commit 1f92f04

Browse files
Migrate to i18next for internationalization (#4731)
* Add files via upload * Add files via upload * Migrate internationalization from webL10n to i18next in JS files * removed random encodings spoiling the .po files * Add files via upload * Changed the function definition of _() suited for i18next * updated the i18next json files * changed the language switch functions to i18next standard in activity.js so that renderLanguageSelectIcon() works correctly * Add files via upload * Add files via upload * Delete locales/ja-kana.json * Delete locales/ja.json * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Update translate_ai.py * Add files via upload * migration to i18next * migration to i18next * fixed the linting issue * fixed some test cases of toolbar due to addition of TR language had to increase the call times * added newly updated TR file * fix: update package-lock.json to sync with package.json * removed old kana kanji jap files * added docstring in po to json converter * added license and copyright in the converter file * added worflow for auto conversion of po to json files * testing worflow * chore(i18n): auto-update JSON files from updated PO files * resolved the lng change issues in japanese * solved the lang change issue for japanese kana and kanji * solved the issue of separate loader image in case of jap lang and advance mode * po files updation * chore(i18n): auto-update JSON files from updated PO files --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 37cde6d commit 1f92f04

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+132938
-220
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Auto-convert PO to JSON and commit
2+
3+
on:
4+
push:
5+
paths:
6+
- 'po/**/*.po'
7+
8+
jobs:
9+
convert-and-commit:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout code
14+
uses: actions/checkout@v4
15+
with:
16+
persist-credentials: true
17+
fetch-depth: 0
18+
19+
- name: Set up Python
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: '3.x'
23+
24+
- name: Find changed .po files
25+
id: find_po
26+
run: |
27+
git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep '^po/.*\.po$' > changed_po_files.txt || true
28+
cat changed_po_files.txt
29+
echo "po_files<<EOF" >> $GITHUB_OUTPUT
30+
echo "$(cat changed_po_files.txt)" >> $GITHUB_OUTPUT
31+
echo "EOF" >> $GITHUB_OUTPUT
32+
33+
- name: Run conversion script
34+
if: steps.find_po.outputs.po_files != ''
35+
run: |
36+
mkdir -p locales
37+
while IFS= read -r po_file; do
38+
echo "▶ Converting $po_file"
39+
python3 convert_po_to_json.py "$po_file" "locales"
40+
done < changed_po_files.txt
41+
42+
- name: Commit and push updated JSON
43+
if: steps.find_po.outputs.po_files != ''
44+
run: |
45+
git config --global user.name "github-actions[bot]"
46+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
47+
48+
git add locales/*.json
49+
50+
if git diff --cached --quiet; then
51+
echo "✅ No JSON changes to commit."
52+
else
53+
git commit -m "chore(i18n): auto-update JSON files from updated PO files"
54+
git push origin ${{ github.ref }}
55+
echo "🚀 Pushed updated JSON files."
56+
fi

convert_po_to_json.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Copyright (c) 2025 Walter Bender
2+
# Copyright (c) 2025 Aman Chadha, DMP'25
3+
#
4+
# This program is free software; you can redistribute it and/or
5+
# modify it under the terms of the The GNU Affero General Public
6+
# License as published by the Free Software Foundation; either
7+
# version 3 of the License, or (at your option) any later version.
8+
#
9+
# You should have received a copy of the GNU Affero General Public
10+
# License along with this library; if not, write to the Free Software
11+
# Foundation, 51 Franklin Street, Suite 500 Boston, MA 02110-1335 USA
12+
#
13+
# Note: This script converts .po translation files into JSON format,
14+
# following the i18n infrastructure used in Music Blocks. -- Aman Chadha, July 2025
15+
16+
import os
17+
import json
18+
import re
19+
20+
def parse_po_file(po_file):
21+
"""Parse a .po file and return a dict of {msgid: msgstr}."""
22+
data = {}
23+
current_msgid = None
24+
current_msgstr = None
25+
26+
with open(po_file, "r", encoding="utf-8") as f:
27+
for line in f:
28+
line = line.strip()
29+
if line.startswith("msgid"):
30+
current_msgid = re.findall(r'"(.*)"', line)[0]
31+
elif line.startswith("msgstr"):
32+
current_msgstr = re.findall(r'"(.*)"', line)[0]
33+
if current_msgid is not None:
34+
data[current_msgid] = current_msgstr or current_msgid
35+
current_msgid = None
36+
return data
37+
38+
def convert_po_to_json(po_file, output_dir):
39+
"""Convert a .po file to .json. Special case: ja.po + ja-kana.po -> merged ja.json."""
40+
41+
lang_code = os.path.splitext(os.path.basename(po_file))[0]
42+
43+
# Special handling for Japanese
44+
if lang_code in ["ja", "ja-kana"]:
45+
ja_file = os.path.join(os.path.dirname(po_file), "ja.po")
46+
kana_file = os.path.join(os.path.dirname(po_file), "ja-kana.po")
47+
48+
if os.path.exists(ja_file) and os.path.exists(kana_file):
49+
ja_dict = parse_po_file(ja_file)
50+
kana_dict = parse_po_file(kana_file)
51+
52+
combined = {}
53+
all_keys = set(ja_dict.keys()) | set(kana_dict.keys())
54+
for key in all_keys:
55+
combined[key] = {
56+
"kanji": ja_dict.get(key, key),
57+
"kana": kana_dict.get(key, key),
58+
}
59+
60+
output_path = os.path.join(output_dir, "ja.json")
61+
os.makedirs(output_dir, exist_ok=True)
62+
with open(output_path, "w", encoding="utf-8") as f:
63+
json.dump(combined, f, indent=2, ensure_ascii=False)
64+
print(f"✅ Combined ja.po + ja-kana.po → {output_path}")
65+
return # Don’t fall through to default case
66+
67+
# Default for all other langs
68+
json_data = parse_po_file(po_file)
69+
output_path = os.path.join(output_dir, f"{lang_code}.json")
70+
os.makedirs(output_dir, exist_ok=True)
71+
with open(output_path, "w", encoding="utf-8") as f:
72+
json.dump(json_data, f, indent=2, ensure_ascii=False)
73+
print(f"✅ Converted {po_file}{output_path}")
74+
75+
def convert_all_po_files(po_dir, output_dir):
76+
"""Convert all .po files in the given directory to .json format."""
77+
for root, _, files in os.walk(po_dir):
78+
for file in files:
79+
if file.endswith(".po"):
80+
convert_po_to_json(os.path.join(root, file), output_dir)
81+
82+
convert_all_po_files("./po", "./locales")

index.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@
454454
<li><a id="ko"></a></li>
455455
<li><a id="zhCN"></a></li>
456456
<li><a id="th"></a></li>
457+
<li><a id="tr"></a></li>
457458
<li><a id="ayc"></a></li>
458459
<li><a id="quz"></a></li>
459460
<li><a id="gug"></a></li>
@@ -543,8 +544,8 @@
543544
}
544545

545546
const container = document.getElementById("loading-media");
546-
const content = lang === "ja"
547-
? `<img src="loading-animation-ja.svg" loading="eager" fetchpriority="high" style="width: 70%; height: 90%; object-fit: contain;" alt="Loading animation">`
547+
const content = lang.startsWith("ja")
548+
? `<img src="loading-animation-ja.svg" loading="eager" fetchpriority="high" style="width: 70%; height: 90%; object-fit: contain;" alt="Loading animation">`
548549
: `<video loop autoplay muted playsinline fetchpriority="high" style="width: 90%; height: 100%; object-fit: contain;">
549550
<source src="loading-animation.webm" type="video/webm">
550551
<source src="loading-animation.mp4" type="video/mp4">

js/__tests__/languagebox.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ describe("LanguageBox Class", () => {
138138

139139
languageBox.ja_onclick();
140140

141-
expect(languageBox._language).toBe("ja");
141+
expect(languageBox._language).toBe("ja-kanji");
142142
expect(mockActivity.storage.kanaPreference).toBe("kanji");
143143
expect(hideSpy).toHaveBeenCalled();
144144
});
@@ -148,7 +148,7 @@ describe("LanguageBox Class", () => {
148148

149149
languageBox.kana_onclick();
150150

151-
expect(languageBox._language).toBe("ja");
151+
expect(languageBox._language).toBe("ja-kana");
152152
expect(mockActivity.storage.kanaPreference).toBe("kana");
153153
expect(hideSpy).toHaveBeenCalled();
154154
});

js/__tests__/toolbar.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,14 @@ describe("Toolbar Class", () => {
9595
test("sets correct strings for _THIS_IS_MUSIC_BLOCKS_ true", () => {
9696
global._THIS_IS_MUSIC_BLOCKS_ = true;
9797
toolbar.init({});
98-
expect(global._).toHaveBeenCalledTimes(132);
98+
expect(global._).toHaveBeenCalledTimes(134);
9999
expect(global._).toHaveBeenNthCalledWith(1, "About Music Blocks");
100100
});
101101

102102
test("sets correct strings for _THIS_IS_MUSIC_BLOCKS_ false", () => {
103103
global._THIS_IS_MUSIC_BLOCKS_ = false;
104104
toolbar.init({});
105-
expect(global._).toHaveBeenCalledTimes(114);
105+
expect(global._).toHaveBeenCalledTimes(116);
106106
expect(global._).toHaveBeenNthCalledWith(1, "About Turtle Blocks");
107107
});
108108

js/activity.js

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -313,19 +313,20 @@ class Activity {
313313
let lang = "en";
314314
if (this.storage.languagePreference !== undefined) {
315315
lang = this.storage.languagePreference;
316-
document.webL10n.setLanguage(lang);
316+
if (lang.startsWith("ja")) lang = "ja"; // normalize Japanese
317+
i18next.changeLanguage(lang);
317318
} else {
318319
lang = navigator.language;
319320
if (lang.includes("-")) {
320321
lang = lang.slice(0, lang.indexOf("-"));
321-
document.webL10n.setLanguage(lang);
322322
}
323+
i18next.changeLanguage(lang);
323324
}
324325
} catch (e) {
325-
// eslint-disable-next-line no-console
326326
console.error(e);
327327
}
328328

329+
329330
this.KeySignatureEnv = ["C", "major", false];
330331
try {
331332
if (this.storage.KeySignatureEnv !== undefined) {
@@ -566,15 +567,6 @@ class Activity {
566567
if (helpfulWheelTop + 350 > windowHeight) {
567568
docById("helpfulWheelDiv").style.top = (windowHeight - 350) + "px";
568569
}
569-
const selectedBlocksCount = this.blocks.selectedBlocks.filter(block => !block.trash).length;
570-
571-
if (selectedBlocksCount) {
572-
this.helpfulWheelItems.find(ele => ele.label === "Move to trash").display = true;
573-
this.helpfulWheelItems.find(ele => ele.label === "Duplicate").display = true;
574-
} else {
575-
this.helpfulWheelItems.find(ele => ele.label === "Move to trash").display = false;
576-
this.helpfulWheelItems.find(ele => ele.label === "Duplicate").display = false;
577-
}
578570

579571
docById("helpfulWheelDiv").style.display = "";
580572

@@ -1424,6 +1416,14 @@ class Activity {
14241416
const confirmBtn = document.createElement("button");
14251417
confirmBtn.classList.add("confirm-button");
14261418
confirmBtn.textContent = "Confirm";
1419+
confirmBtn.style.backgroundColor = platformColor.blueButton;
1420+
confirmBtn.style.color = "white";
1421+
confirmBtn.style.border = "none";
1422+
confirmBtn.style.borderRadius = "4px";
1423+
confirmBtn.style.padding = "8px 16px";
1424+
confirmBtn.style.fontWeight = "bold";
1425+
confirmBtn.style.cursor = "pointer";
1426+
confirmBtn.style.marginRight = "16px";
14271427
confirmBtn.addEventListener("click", () => {
14281428
document.body.removeChild(modal);
14291429
clearCanvasAction();
@@ -1432,6 +1432,13 @@ class Activity {
14321432
const cancelBtn = document.createElement("button");
14331433
cancelBtn.classList.add("cancel-button");
14341434
cancelBtn.textContent = "Cancel";
1435+
cancelBtn.style.backgroundColor = "#f1f1f1";
1436+
cancelBtn.style.color = "black";
1437+
cancelBtn.style.border = "none";
1438+
cancelBtn.style.borderRadius = "4px";
1439+
cancelBtn.style.padding = "8px 16px";
1440+
cancelBtn.style.fontWeight = "bold";
1441+
cancelBtn.style.cursor = "pointer";
14351442
cancelBtn.addEventListener("click", () => {
14361443
document.body.removeChild(modal);
14371444
});
@@ -2980,6 +2987,7 @@ class Activity {
29802987
// note block to the active block.
29812988
this.blocks.activeBlock = this.blocks.blockList.length - 1;
29822989
};
2990+
29832991

29842992
//To create a sampler widget
29852993
this.makeSamplerWidget = (sampleName, sampleData) => {
@@ -2997,6 +3005,7 @@ class Activity {
29973005
this.blocks.loadNewBlocks(samplerStack);
29983006
};
29993007

3008+
30003009
/*
30013010
* Handles keyboard shortcuts in MB
30023011
*/
@@ -4270,6 +4279,33 @@ class Activity {
42704279
}, 5000);
42714280
};
42724281

4282+
4283+
const standardDurations = [
4284+
{ value: "1/1", duration: 1 },
4285+
{ value: "1/2", duration: 0.5 },
4286+
{ value: "1/4", duration: 0.25 },
4287+
{ value: "1/8", duration: 0.125 },
4288+
{ value: "1/16", duration: 0.0625 },
4289+
{ value: "1/32", duration: 0.03125 },
4290+
{ value: "1/64", duration: 0.015625 },
4291+
{ value: "1/128", duration: 0.0078125 }
4292+
];
4293+
4294+
this.getClosestStandardNoteValue = function(duration) {
4295+
let closest = standardDurations[0];
4296+
let minDiff = Math.abs(duration - closest.duration);
4297+
4298+
for (let i = 1; i < standardDurations.length; i++) {
4299+
let diff = Math.abs(duration - standardDurations[i].duration);
4300+
if (diff < minDiff) {
4301+
closest = standardDurations[i];
4302+
minDiff = diff;
4303+
}
4304+
}
4305+
4306+
return closest.value.split("/").map(Number);
4307+
};
4308+
42734309
/**
42744310
* Loads MB project from Planet.
42754311
* @param projectID {Planet project ID}
@@ -5658,13 +5694,7 @@ class Activity {
56585694

56595695
if (!this.helpfulWheelItems.find(ele => ele.label === "Select"))
56605696
this.helpfulWheelItems.push({label: "Select", icon: "imgsrc:data:image/svg+xml;base64," + window.btoa(base64Encode(SELECTBUTTON)), display: true, fn: this.selectMode });
5661-
5662-
if (!this.helpfulWheelItems.find(ele => ele.label === "Move to trash"))
5663-
this.helpfulWheelItems.push({label: "Move to trash", icon: "imgsrc:header-icons/empty-trash-button.svg", display: false, fn: this.deleteMultipleBlocks });
5664-
5665-
if (!this.helpfulWheelItems.find(ele => ele.label === "Duplicate"))
5666-
this.helpfulWheelItems.push({label: "Duplicate", icon: "imgsrc:header-icons/copy-button.svg" , display: false, fn: this.copyMultipleBlocks});
5667-
5697+
56685698
if (!this.helpfulWheelItems.find(ele => ele.label === "Clear"))
56695699
this.helpfulWheelItems.push({label: "Clear", icon: "imgsrc:data:image/svg+xml;base64," + window.btoa(base64Encode(CLEARBUTTON)), display: true, fn: () => this._allClear(false)});
56705700

@@ -6033,6 +6063,7 @@ class Activity {
60336063
// end the drag on navbar
60346064
document.getElementById("toolbars").addEventListener("mouseover", () => {this.isDragging = false;});
60356065

6066+
60366067
this.deleteMultipleBlocks = () => {
60376068
if (this.blocks.selectionModeOn) {
60386069
const blocksArray = this.blocks.selectedBlocks;
@@ -6095,13 +6126,15 @@ class Activity {
60956126
};
60966127

60976128

6129+
60986130
this.selectMode = () => {
60996131
this.moving = false;
61006132
this.isSelecting = !this.isSelecting;
61016133
(this.isSelecting) ? this.textMsg(_("Select is enabled.")) : this.textMsg(_("Select is disabled."));
61026134
docById("helpfulWheelDiv").style.display = "none";
61036135
};
61046136

6137+
61056138
this._create2Ddrag = () => {
61066139
this.dragArea = {};
61076140
this.selectedBlocks = [];
@@ -6595,8 +6628,6 @@ class Activity {
65956628
that.errorMsg(
65966629
_("Cannot load project from the file. Please check the file type.")
65976630
);
6598-
} else if (files[0].type === "audio/wav") {
6599-
this.makeSamplerWidget(files[0].name, reader.result);
66006631
} else {
66016632
const cleanData = rawData.replace("\n", " ");
66026633
let obj;
@@ -6713,12 +6744,6 @@ class Activity {
67136744
abcReader.readAsText(files[0]);
67146745
return;
67156746
}
6716-
6717-
if (files[0].type === "audio/wav") {
6718-
reader.readAsDataURL(files[0]);
6719-
return;
6720-
}
6721-
67226747
reader.readAsText(files[0]);
67236748
reader.readAsText(files[0]);
67246749
window.scroll(0, 0);

0 commit comments

Comments
 (0)