Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
d78919c
Add files via upload
ac-mmi Feb 19, 2025
ac566eb
Add files via upload
ac-mmi Feb 20, 2025
29a4416
Merge branch 'sugarlabs:master' into master
ac-mmi Feb 21, 2025
1678672
Merge branch 'sugarlabs:master' into master
ac-mmi Feb 24, 2025
5ead512
Migrate internationalization from webL10n to i18next in JS files
ac-mmi Feb 24, 2025
b844c94
removed random encodings spoiling the .po files
ac-mmi Feb 24, 2025
5cf011e
Add files via upload
ac-mmi Feb 24, 2025
1680230
Changed the function definition of _() suited for i18next
ac-mmi Feb 24, 2025
d8bbb85
Merge branch 'internalization-js' of https://github.com/ac-mmi/musicb…
ac-mmi Feb 24, 2025
d6b1114
updated the i18next json files
ac-mmi Feb 24, 2025
e6e8c5a
changed the language switch functions to i18next standard in activity…
ac-mmi Feb 25, 2025
c21fb14
Merge branch 'master' into internalization-js
ac-mmi Mar 4, 2025
7c46058
Add files via upload
ac-mmi Mar 7, 2025
a238ef8
Add files via upload
ac-mmi Mar 7, 2025
ce5a5e5
Delete locales/ja-kana.json
ac-mmi Mar 7, 2025
c9d69ca
Delete locales/ja.json
ac-mmi Mar 7, 2025
fe630ea
Add files via upload
ac-mmi Mar 7, 2025
c25e7fb
Merge branch 'master' into internalization-js
ac-mmi Mar 7, 2025
740b4da
Merge branch 'master' into internalization-js
ac-mmi Mar 15, 2025
dcb2d7e
Merge branch 'master' into internalization-js
ac-mmi Mar 23, 2025
040a030
Add files via upload
ac-mmi Mar 24, 2025
bbaca35
Add files via upload
ac-mmi Mar 24, 2025
4540621
Add files via upload
ac-mmi Mar 24, 2025
75f50db
Add files via upload
ac-mmi Mar 24, 2025
45990f8
Update translate_ai.py
ac-mmi Mar 24, 2025
ea46f98
Add files via upload
ac-mmi Mar 30, 2025
8abe738
migration to i18next
ac-mmi Jul 24, 2025
ca1987f
migration to i18next
ac-mmi Jul 24, 2025
d4bd26f
Merge branch 'master' into i18next
ac-mmi Jul 24, 2025
25ecd9b
fixed the linting issue
ac-mmi Jul 24, 2025
72bfad3
Merge branch 'i18next' of https://github.com/ac-mmi/musicblocks into …
ac-mmi Jul 24, 2025
a0c1ef5
fixed some test cases of toolbar due to addition of TR language had t…
ac-mmi Jul 24, 2025
3b0d1d5
added newly updated TR file
ac-mmi Jul 27, 2025
d6e56e4
fix: update package-lock.json to sync with package.json
ac-mmi Jul 27, 2025
c09ab33
Merge branch 'i18next' of https://github.com/ac-mmi/musicblocks into …
ac-mmi Jul 27, 2025
13b94b9
removed old kana kanji jap files
ac-mmi Jul 27, 2025
4d88b7e
added docstring in po to json converter
ac-mmi Jul 28, 2025
97ecdee
added license and copyright in the converter file
ac-mmi Jul 28, 2025
c9695d5
added worflow for auto conversion of po to json files
ac-mmi Jul 31, 2025
b19803b
testing worflow
ac-mmi Jul 31, 2025
d01f9c9
chore(i18n): auto-update JSON files from updated PO files
github-actions[bot] Jul 31, 2025
ae4a2a4
resolved the lng change issues in japanese
ac-mmi Sep 7, 2025
cd88498
Merge branch 'i18next' of https://github.com/ac-mmi/musicblocks into …
ac-mmi Sep 7, 2025
0e9346e
solved the lang change issue for japanese kana and kanji
ac-mmi Sep 7, 2025
3e0f528
solved the issue of separate loader image in case of jap lang and adv…
ac-mmi Sep 8, 2025
19f02bf
po files updation
ac-mmi Sep 8, 2025
7b11ec7
chore(i18n): auto-update JSON files from updated PO files
github-actions[bot] Sep 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/workflows/po-to-json-autocommit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Auto-convert PO to JSON and commit

on:
push:
paths:
- 'po/**/*.po'

jobs:
convert-and-commit:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: true
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Find changed .po files
id: find_po
run: |
git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep '^po/.*\.po$' > changed_po_files.txt || true
cat changed_po_files.txt
echo "po_files<<EOF" >> $GITHUB_OUTPUT
echo "$(cat changed_po_files.txt)" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

- name: Run conversion script
if: steps.find_po.outputs.po_files != ''
run: |
mkdir -p locales
while IFS= read -r po_file; do
echo "▶ Converting $po_file"
python3 convert_po_to_json.py "$po_file" "locales"
done < changed_po_files.txt

- name: Commit and push updated JSON
if: steps.find_po.outputs.po_files != ''
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"

git add locales/*.json

if git diff --cached --quiet; then
echo "✅ No JSON changes to commit."
else
git commit -m "chore(i18n): auto-update JSON files from updated PO files"
git push origin ${{ github.ref }}
echo "🚀 Pushed updated JSON files."
fi
82 changes: 82 additions & 0 deletions convert_po_to_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Copyright (c) 2025 Walter Bender
# Copyright (c) 2025 Aman Chadha, DMP'25
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the The GNU Affero General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# You should have received a copy of the GNU Affero General Public
# License along with this library; if not, write to the Free Software
# Foundation, 51 Franklin Street, Suite 500 Boston, MA 02110-1335 USA
#
# Note: This script converts .po translation files into JSON format,
# following the i18n infrastructure used in Music Blocks. -- Aman Chadha, July 2025

import os
import json
import re

def parse_po_file(po_file):
"""Parse a .po file and return a dict of {msgid: msgstr}."""
data = {}
current_msgid = None
current_msgstr = None

with open(po_file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line.startswith("msgid"):
current_msgid = re.findall(r'"(.*)"', line)[0]
elif line.startswith("msgstr"):
current_msgstr = re.findall(r'"(.*)"', line)[0]
if current_msgid is not None:
data[current_msgid] = current_msgstr or current_msgid
current_msgid = None
return data

def convert_po_to_json(po_file, output_dir):
"""Convert a .po file to .json. Special case: ja.po + ja-kana.po -> merged ja.json."""

lang_code = os.path.splitext(os.path.basename(po_file))[0]

# Special handling for Japanese
if lang_code in ["ja", "ja-kana"]:
ja_file = os.path.join(os.path.dirname(po_file), "ja.po")
kana_file = os.path.join(os.path.dirname(po_file), "ja-kana.po")

if os.path.exists(ja_file) and os.path.exists(kana_file):
ja_dict = parse_po_file(ja_file)
kana_dict = parse_po_file(kana_file)

combined = {}
all_keys = set(ja_dict.keys()) | set(kana_dict.keys())
for key in all_keys:
combined[key] = {
"kanji": ja_dict.get(key, key),
"kana": kana_dict.get(key, key),
}

output_path = os.path.join(output_dir, "ja.json")
os.makedirs(output_dir, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
json.dump(combined, f, indent=2, ensure_ascii=False)
print(f"✅ Combined ja.po + ja-kana.po → {output_path}")
return # Don’t fall through to default case

# Default for all other langs
json_data = parse_po_file(po_file)
output_path = os.path.join(output_dir, f"{lang_code}.json")
os.makedirs(output_dir, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
json.dump(json_data, f, indent=2, ensure_ascii=False)
print(f"✅ Converted {po_file} → {output_path}")

def convert_all_po_files(po_dir, output_dir):
"""Convert all .po files in the given directory to .json format."""
for root, _, files in os.walk(po_dir):
for file in files:
if file.endswith(".po"):
convert_po_to_json(os.path.join(root, file), output_dir)

convert_all_po_files("./po", "./locales")
5 changes: 3 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@
<li><a id="ko"></a></li>
<li><a id="zhCN"></a></li>
<li><a id="th"></a></li>
<li><a id="tr"></a></li>
<li><a id="ayc"></a></li>
<li><a id="quz"></a></li>
<li><a id="gug"></a></li>
Expand Down Expand Up @@ -543,8 +544,8 @@
}

const container = document.getElementById("loading-media");
const content = lang === "ja"
? `<img src="loading-animation-ja.svg" loading="eager" fetchpriority="high" style="width: 70%; height: 90%; object-fit: contain;" alt="Loading animation">`
const content = lang.startsWith("ja")
? `<img src="loading-animation-ja.svg" loading="eager" fetchpriority="high" style="width: 70%; height: 90%; object-fit: contain;" alt="Loading animation">`
: `<video loop autoplay muted playsinline fetchpriority="high" style="width: 90%; height: 100%; object-fit: contain;">
<source src="loading-animation.webm" type="video/webm">
<source src="loading-animation.mp4" type="video/mp4">
Expand Down
4 changes: 2 additions & 2 deletions js/__tests__/languagebox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ describe("LanguageBox Class", () => {

languageBox.ja_onclick();

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

languageBox.kana_onclick();

expect(languageBox._language).toBe("ja");
expect(languageBox._language).toBe("ja-kana");
expect(mockActivity.storage.kanaPreference).toBe("kana");
expect(hideSpy).toHaveBeenCalled();
});
Expand Down
4 changes: 2 additions & 2 deletions js/__tests__/toolbar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,14 @@ describe("Toolbar Class", () => {
test("sets correct strings for _THIS_IS_MUSIC_BLOCKS_ true", () => {
global._THIS_IS_MUSIC_BLOCKS_ = true;
toolbar.init({});
expect(global._).toHaveBeenCalledTimes(132);
expect(global._).toHaveBeenCalledTimes(134);
expect(global._).toHaveBeenNthCalledWith(1, "About Music Blocks");
});

test("sets correct strings for _THIS_IS_MUSIC_BLOCKS_ false", () => {
global._THIS_IS_MUSIC_BLOCKS_ = false;
toolbar.init({});
expect(global._).toHaveBeenCalledTimes(114);
expect(global._).toHaveBeenCalledTimes(116);
expect(global._).toHaveBeenNthCalledWith(1, "About Turtle Blocks");
});

Expand Down
79 changes: 52 additions & 27 deletions js/activity.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,19 +310,20 @@ class Activity {
let lang = "en";
if (this.storage.languagePreference !== undefined) {
lang = this.storage.languagePreference;
document.webL10n.setLanguage(lang);
if (lang.startsWith("ja")) lang = "ja"; // normalize Japanese
i18next.changeLanguage(lang);
} else {
lang = navigator.language;
if (lang.includes("-")) {
lang = lang.slice(0, lang.indexOf("-"));
document.webL10n.setLanguage(lang);
}
i18next.changeLanguage(lang);
}
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
}


this.KeySignatureEnv = ["C", "major", false];
try {
if (this.storage.KeySignatureEnv !== undefined) {
Expand Down Expand Up @@ -563,15 +564,6 @@ class Activity {
if (helpfulWheelTop + 350 > windowHeight) {
docById("helpfulWheelDiv").style.top = (windowHeight - 350) + "px";
}
const selectedBlocksCount = this.blocks.selectedBlocks.filter(block => !block.trash).length;

if (selectedBlocksCount) {
this.helpfulWheelItems.find(ele => ele.label === "Move to trash").display = true;
this.helpfulWheelItems.find(ele => ele.label === "Duplicate").display = true;
} else {
this.helpfulWheelItems.find(ele => ele.label === "Move to trash").display = false;
this.helpfulWheelItems.find(ele => ele.label === "Duplicate").display = false;
}

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

Expand Down Expand Up @@ -1421,6 +1413,14 @@ class Activity {
const confirmBtn = document.createElement("button");
confirmBtn.classList.add("confirm-button");
confirmBtn.textContent = "Confirm";
confirmBtn.style.backgroundColor = platformColor.blueButton;
confirmBtn.style.color = "white";
confirmBtn.style.border = "none";
confirmBtn.style.borderRadius = "4px";
confirmBtn.style.padding = "8px 16px";
confirmBtn.style.fontWeight = "bold";
confirmBtn.style.cursor = "pointer";
confirmBtn.style.marginRight = "16px";
confirmBtn.addEventListener("click", () => {
document.body.removeChild(modal);
clearCanvasAction();
Expand All @@ -1429,6 +1429,13 @@ class Activity {
const cancelBtn = document.createElement("button");
cancelBtn.classList.add("cancel-button");
cancelBtn.textContent = "Cancel";
cancelBtn.style.backgroundColor = "#f1f1f1";
cancelBtn.style.color = "black";
cancelBtn.style.border = "none";
cancelBtn.style.borderRadius = "4px";
cancelBtn.style.padding = "8px 16px";
cancelBtn.style.fontWeight = "bold";
cancelBtn.style.cursor = "pointer";
cancelBtn.addEventListener("click", () => {
document.body.removeChild(modal);
});
Expand Down Expand Up @@ -2977,6 +2984,7 @@ class Activity {
// note block to the active block.
this.blocks.activeBlock = this.blocks.blockList.length - 1;
};


//To create a sampler widget
this.makeSamplerWidget = (sampleName, sampleData) => {
Expand All @@ -2994,6 +3002,7 @@ class Activity {
this.blocks.loadNewBlocks(samplerStack);
};


/*
* Handles keyboard shortcuts in MB
*/
Expand Down Expand Up @@ -4265,6 +4274,33 @@ class Activity {
}, 5000);
};


const standardDurations = [
{ value: "1/1", duration: 1 },
{ value: "1/2", duration: 0.5 },
{ value: "1/4", duration: 0.25 },
{ value: "1/8", duration: 0.125 },
{ value: "1/16", duration: 0.0625 },
{ value: "1/32", duration: 0.03125 },
{ value: "1/64", duration: 0.015625 },
{ value: "1/128", duration: 0.0078125 }
];

this.getClosestStandardNoteValue = function(duration) {
let closest = standardDurations[0];
let minDiff = Math.abs(duration - closest.duration);

for (let i = 1; i < standardDurations.length; i++) {
let diff = Math.abs(duration - standardDurations[i].duration);
if (diff < minDiff) {
closest = standardDurations[i];
minDiff = diff;
}
}

return closest.value.split("/").map(Number);
};

/**
* Loads MB project from Planet.
* @param projectID {Planet project ID}
Expand Down Expand Up @@ -5653,13 +5689,7 @@ class Activity {

if (!this.helpfulWheelItems.find(ele => ele.label === "Select"))
this.helpfulWheelItems.push({label: "Select", icon: "imgsrc:data:image/svg+xml;base64," + window.btoa(base64Encode(SELECTBUTTON)), display: true, fn: this.selectMode });

if (!this.helpfulWheelItems.find(ele => ele.label === "Move to trash"))
this.helpfulWheelItems.push({label: "Move to trash", icon: "imgsrc:header-icons/empty-trash-button.svg", display: false, fn: this.deleteMultipleBlocks });

if (!this.helpfulWheelItems.find(ele => ele.label === "Duplicate"))
this.helpfulWheelItems.push({label: "Duplicate", icon: "imgsrc:header-icons/copy-button.svg" , display: false, fn: this.copyMultipleBlocks});


if (!this.helpfulWheelItems.find(ele => ele.label === "Clear"))
this.helpfulWheelItems.push({label: "Clear", icon: "imgsrc:data:image/svg+xml;base64," + window.btoa(base64Encode(CLEARBUTTON)), display: true, fn: () => this._allClear(false)});

Expand Down Expand Up @@ -6028,6 +6058,7 @@ class Activity {
// end the drag on navbar
document.getElementById("toolbars").addEventListener("mouseover", () => {this.isDragging = false;});


this.deleteMultipleBlocks = () => {
if (this.blocks.selectionModeOn) {
const blocksArray = this.blocks.selectedBlocks;
Expand Down Expand Up @@ -6090,13 +6121,15 @@ class Activity {
};



this.selectMode = () => {
this.moving = false;
this.isSelecting = !this.isSelecting;
(this.isSelecting) ? this.textMsg(_("Select is enabled.")) : this.textMsg(_("Select is disabled."));
docById("helpfulWheelDiv").style.display = "none";
};


this._create2Ddrag = () => {
this.dragArea = {};
this.selectedBlocks = [];
Expand Down Expand Up @@ -6590,8 +6623,6 @@ class Activity {
that.errorMsg(
_("Cannot load project from the file. Please check the file type.")
);
} else if (files[0].type === "audio/wav") {
this.makeSamplerWidget(files[0].name, reader.result);
} else {
const cleanData = rawData.replace("\n", " ");
let obj;
Expand Down Expand Up @@ -6708,12 +6739,6 @@ class Activity {
abcReader.readAsText(files[0]);
return;
}

if (files[0].type === "audio/wav") {
reader.readAsDataURL(files[0]);
return;
}

reader.readAsText(files[0]);
reader.readAsText(files[0]);
window.scroll(0, 0);
Expand Down
Loading