diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index e26f73f449..d2f84ade2c 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -21,11 +21,12 @@ updates:
cooldown:
default-days: 7
exclude:
+ - kolibri-build
- kolibri-constants
- kolibri-design-system
- - kolibri-logging
- kolibri-format
- - kolibri-tools
+ - kolibri-i18n
+ - kolibri-logging
groups:
babel:
patterns:
diff --git a/.github/workflows/i18n-download.yml b/.github/workflows/i18n-download.yml
new file mode 100644
index 0000000000..09761e7255
--- /dev/null
+++ b/.github/workflows/i18n-download.yml
@@ -0,0 +1,87 @@
+name: Download translations from Crowdin
+
+on:
+ workflow_dispatch:
+
+jobs:
+ download:
+ name: Download translations and update files
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v7
+ with:
+ python-version: '3.10'
+ activate-environment: "true"
+ enable-cache: "true"
+
+ - name: Install Python dependencies
+ run: uv pip sync requirements.txt
+
+ - name: Use pnpm
+ uses: pnpm/action-setup@v5
+
+ - name: Use Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: '20.x'
+ cache: 'pnpm'
+
+ - name: Setup Java for crowdin-cli
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: Install gettext
+ run: sudo apt-get update && sudo apt-get install -y gettext
+
+ - name: Install JavaScript dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Download translations
+ env:
+ CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_API_KEY }}
+ run: make i18n-download
+
+ - name: Generate App Token
+ id: generate-token
+ uses: actions/create-github-app-token@v3
+ with:
+ app-id: ${{ secrets.LE_BOT_APP_ID }}
+ private-key: ${{ secrets.LE_BOT_PRIVATE_KEY }}
+
+ - name: Create Pull Request
+ uses: peter-evans/create-pull-request@v8
+ with:
+ token: ${{ steps.generate-token.outputs.token }}
+ commit-message: |
+ Update translations from Crowdin
+
+ This includes:
+ - Updated translation files (.po and .json)
+ - Compiled Django messages (.mo files)
+ - Updated frontend i18n files
+ branch: i18n-update-${{ github.ref_name }}
+ base: ${{ github.ref_name }}
+ delete-branch: true
+ title: 'Update translations from Crowdin for ${{ github.ref_name }}'
+ body: |
+ ## Summary of changes
+
+ This PR updates translations downloaded from Crowdin.
+
+ ### Changes included:
+ - Updated translation files (`.po` and `.json`)
+ - Compiled Django messages (`.mo` files)
+ - Updated frontend i18n files
+
+ ### Testing checklist:
+ - [ ] Verify that translations are not obviously empty or untranslated in the message files
+ - [ ] Switch to a few different languages and navigate the app
+ labels: |
+ i18n
+ TAG: user strings
diff --git a/.github/workflows/i18n-upload.yml b/.github/workflows/i18n-upload.yml
new file mode 100644
index 0000000000..0e087c82c6
--- /dev/null
+++ b/.github/workflows/i18n-upload.yml
@@ -0,0 +1,50 @@
+name: Upload translations to Crowdin
+
+on:
+ workflow_dispatch:
+
+jobs:
+ upload:
+ name: Extract and upload strings to Crowdin
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v7
+ with:
+ python-version: '3.10'
+ activate-environment: "true"
+ enable-cache: "true"
+
+ - name: Install Python dependencies
+ run: uv pip sync requirements.txt
+
+ - name: Use pnpm
+ uses: pnpm/action-setup@v5
+
+ - name: Use Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: '20.x'
+ cache: 'pnpm'
+
+ - name: Setup Java for crowdin-cli
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: Install gettext
+ run: sudo apt-get update && sudo apt-get install -y gettext
+
+ - name: Install JavaScript dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Extract and upload strings to Crowdin
+ env:
+ CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_API_KEY }}
+ run: |
+ make i18n-upload
+ make i18n-pretranslate-approve-all
diff --git a/Makefile b/Makefile
index a1101b2ce4..27c222a7d2 100644
--- a/Makefile
+++ b/Makefile
@@ -83,29 +83,39 @@ i18n-django-compilemessages:
# finds only the .po files nested there.
cd contentcuration && python manage.py compilemessages
+CROWDIN_BRANCH ?= unstable
+
i18n-upload: i18n-extract
- python node_modules/kolibri-tools/lib/i18n/crowdin.py upload-sources ${branch}
+ pnpm exec crowdin upload sources --branch ${CROWDIN_BRANCH}
i18n-pretranslate:
- python node_modules/kolibri-tools/lib/i18n/crowdin.py pretranslate ${branch}
+ pnpm exec crowdin pre-translate --branch ${CROWDIN_BRANCH} --translate-untranslated-only --method=tm
i18n-pretranslate-approve-all:
- python node_modules/kolibri-tools/lib/i18n/crowdin.py pretranslate ${branch} --approve-all
-
-i18n-download-translations:
- python node_modules/kolibri-tools/lib/i18n/crowdin.py rebuild-translations ${branch}
- python node_modules/kolibri-tools/lib/i18n/crowdin.py download-translations ${branch}
- pnpm exec kolibri-tools i18n-code-gen -- --output-dir ./contentcuration/contentcuration/frontend/shared/i18n
+ pnpm exec crowdin pre-translate --branch ${CROWDIN_BRANCH} --translate-untranslated-only --method=tm --auto-approve-option=all
+
+i18n-download-translations: i18n-extract-frontend
+ touch contentcuration/locale/.crowdin-download-marker
+ pnpm exec crowdin download --branch ${CROWDIN_BRANCH}
+ @if [ -z "$$(find contentcuration/locale/*/LC_MESSAGES -type f \( -name '*.po' -o -name '*.csv' \) -newer contentcuration/locale/.crowdin-download-marker 2>/dev/null)" ]; then \
+ echo "❌ ERROR: No translation files were downloaded - Crowdin download may have failed silently"; \
+ echo "Check the output above for errors during the download process"; \
+ rm -f contentcuration/locale/.crowdin-download-marker; \
+ exit 1; \
+ fi
+ @echo "✅ Translation files downloaded successfully"
+ rm -f contentcuration/locale/.crowdin-download-marker
+ pnpm exec kolibri-i18n code-gen --output-dir ./contentcuration/contentcuration/frontend/shared/i18n
$(MAKE) i18n-django-compilemessages
- pnpm exec kolibri-tools i18n-create-message-files -- --namespace contentcuration --searchPath ./contentcuration/contentcuration/frontend
+ pnpm exec kolibri-i18n create-message-files --namespace contentcuration --searchPath ./contentcuration/contentcuration/frontend
i18n-download: i18n-download-translations
i18n-download-glossary:
- python node_modules/kolibri-tools/lib/i18n/crowdin.py download-glossary
+ pnpm exec crowdin glossary download
i18n-upload-glossary:
- python node_modules/kolibri-tools/lib/i18n/crowdin.py upload-glossary
+ pnpm exec crowdin glossary upload
###############################################################
# END I18N COMMANDS ###########################################
diff --git a/babel.config.js b/babel.config.js
index af49bbaa62..87772455a3 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -1 +1 @@
-module.exports = require('kolibri-tools/babel.config');
+module.exports = require('kolibri-build/babel.config');
diff --git a/contentcuration/contentcuration/frontend/administration/components/sidePanels/__tests__/ReviewSubmissionSidePanel.spec.js b/contentcuration/contentcuration/frontend/administration/components/sidePanels/__tests__/ReviewSubmissionSidePanel.spec.js
index e41f1095a4..5fd6089622 100644
--- a/contentcuration/contentcuration/frontend/administration/components/sidePanels/__tests__/ReviewSubmissionSidePanel.spec.js
+++ b/contentcuration/contentcuration/frontend/administration/components/sidePanels/__tests__/ReviewSubmissionSidePanel.spec.js
@@ -153,7 +153,7 @@ describe('ReviewSubmissionSidePanel', () => {
expect(wrapper.find('[data-test="languages"]').text()).toBe('English, Czech');
expect(wrapper.find('[data-test="categories"]').text()).toBe('School, Algebra');
expect(wrapper.find('[data-test="licenses"]').text()).toBe('CC BY, CC BY-SA');
- expect(wrapper.findComponent(CommunityLibraryStatusChip).attributes('status')).toEqual(
+ expect(wrapper.findComponent(CommunityLibraryStatusChip).props('status')).toEqual(
submission.status,
);
expect(wrapper.find('[data-test="submission-notes"]').text()).toBe(submission.description);
diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LoadingText.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LoadingText.vue
index 6138ec7a0c..014aca8e54 100644
--- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LoadingText.vue
+++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LoadingText.vue
@@ -6,12 +6,11 @@
class="loader-wrapper"
>