Skip to content

Commit 732481c

Browse files
authored
Merge pull request learningequality#5831 from rtibbles/i18n
Migrate i18n to crowdin-cli and kolibri-build/kolibri-i18n
2 parents d4184a4 + 1c6571b commit 732481c

26 files changed

Lines changed: 985 additions & 2082 deletions

File tree

.github/dependabot.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ updates:
2121
cooldown:
2222
default-days: 7
2323
exclude:
24+
- kolibri-build
2425
- kolibri-constants
2526
- kolibri-design-system
26-
- kolibri-logging
2727
- kolibri-format
28-
- kolibri-tools
28+
- kolibri-i18n
29+
- kolibri-logging
2930
groups:
3031
babel:
3132
patterns:
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
name: Download translations from Crowdin
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
download:
8+
name: Download translations and update files
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout repository
12+
uses: actions/checkout@v6
13+
14+
- name: Install uv
15+
uses: astral-sh/setup-uv@v7
16+
with:
17+
python-version: '3.10'
18+
activate-environment: "true"
19+
enable-cache: "true"
20+
21+
- name: Install Python dependencies
22+
run: uv pip sync requirements.txt
23+
24+
- name: Use pnpm
25+
uses: pnpm/action-setup@v5
26+
27+
- name: Use Node.js
28+
uses: actions/setup-node@v6
29+
with:
30+
node-version: '20.x'
31+
cache: 'pnpm'
32+
33+
- name: Setup Java for crowdin-cli
34+
uses: actions/setup-java@v5
35+
with:
36+
distribution: 'temurin'
37+
java-version: '17'
38+
39+
- name: Install gettext
40+
run: sudo apt-get update && sudo apt-get install -y gettext
41+
42+
- name: Install JavaScript dependencies
43+
run: pnpm install --frozen-lockfile
44+
45+
- name: Download translations
46+
env:
47+
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_API_KEY }}
48+
run: make i18n-download
49+
50+
- name: Generate App Token
51+
id: generate-token
52+
uses: actions/create-github-app-token@v3
53+
with:
54+
app-id: ${{ secrets.LE_BOT_APP_ID }}
55+
private-key: ${{ secrets.LE_BOT_PRIVATE_KEY }}
56+
57+
- name: Create Pull Request
58+
uses: peter-evans/create-pull-request@v8
59+
with:
60+
token: ${{ steps.generate-token.outputs.token }}
61+
commit-message: |
62+
Update translations from Crowdin
63+
64+
This includes:
65+
- Updated translation files (.po and .json)
66+
- Compiled Django messages (.mo files)
67+
- Updated frontend i18n files
68+
branch: i18n-update-${{ github.ref_name }}
69+
base: ${{ github.ref_name }}
70+
delete-branch: true
71+
title: 'Update translations from Crowdin for ${{ github.ref_name }}'
72+
body: |
73+
## Summary of changes
74+
75+
This PR updates translations downloaded from Crowdin.
76+
77+
### Changes included:
78+
- Updated translation files (`.po` and `.json`)
79+
- Compiled Django messages (`.mo` files)
80+
- Updated frontend i18n files
81+
82+
### Testing checklist:
83+
- [ ] Verify that translations are not obviously empty or untranslated in the message files
84+
- [ ] Switch to a few different languages and navigate the app
85+
labels: |
86+
i18n
87+
TAG: user strings

.github/workflows/i18n-upload.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Upload translations to Crowdin
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
upload:
8+
name: Extract and upload strings to Crowdin
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout repository
12+
uses: actions/checkout@v6
13+
14+
- name: Install uv
15+
uses: astral-sh/setup-uv@v7
16+
with:
17+
python-version: '3.10'
18+
activate-environment: "true"
19+
enable-cache: "true"
20+
21+
- name: Install Python dependencies
22+
run: uv pip sync requirements.txt
23+
24+
- name: Use pnpm
25+
uses: pnpm/action-setup@v5
26+
27+
- name: Use Node.js
28+
uses: actions/setup-node@v6
29+
with:
30+
node-version: '20.x'
31+
cache: 'pnpm'
32+
33+
- name: Setup Java for crowdin-cli
34+
uses: actions/setup-java@v5
35+
with:
36+
distribution: 'temurin'
37+
java-version: '17'
38+
39+
- name: Install gettext
40+
run: sudo apt-get update && sudo apt-get install -y gettext
41+
42+
- name: Install JavaScript dependencies
43+
run: pnpm install --frozen-lockfile
44+
45+
- name: Extract and upload strings to Crowdin
46+
env:
47+
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_API_KEY }}
48+
run: |
49+
make i18n-upload
50+
make i18n-pretranslate-approve-all

Makefile

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,29 +83,39 @@ i18n-django-compilemessages:
8383
# finds only the .po files nested there.
8484
cd contentcuration && python manage.py compilemessages
8585

86+
CROWDIN_BRANCH ?= unstable
87+
8688
i18n-upload: i18n-extract
87-
python node_modules/kolibri-tools/lib/i18n/crowdin.py upload-sources ${branch}
89+
pnpm exec crowdin upload sources --branch ${CROWDIN_BRANCH}
8890

8991
i18n-pretranslate:
90-
python node_modules/kolibri-tools/lib/i18n/crowdin.py pretranslate ${branch}
92+
pnpm exec crowdin pre-translate --branch ${CROWDIN_BRANCH} --translate-untranslated-only --method=tm
9193

9294
i18n-pretranslate-approve-all:
93-
python node_modules/kolibri-tools/lib/i18n/crowdin.py pretranslate ${branch} --approve-all
94-
95-
i18n-download-translations:
96-
python node_modules/kolibri-tools/lib/i18n/crowdin.py rebuild-translations ${branch}
97-
python node_modules/kolibri-tools/lib/i18n/crowdin.py download-translations ${branch}
98-
pnpm exec kolibri-tools i18n-code-gen -- --output-dir ./contentcuration/contentcuration/frontend/shared/i18n
95+
pnpm exec crowdin pre-translate --branch ${CROWDIN_BRANCH} --translate-untranslated-only --method=tm --auto-approve-option=all
96+
97+
i18n-download-translations: i18n-extract-frontend
98+
touch contentcuration/locale/.crowdin-download-marker
99+
pnpm exec crowdin download --branch ${CROWDIN_BRANCH}
100+
@if [ -z "$$(find contentcuration/locale/*/LC_MESSAGES -type f \( -name '*.po' -o -name '*.csv' \) -newer contentcuration/locale/.crowdin-download-marker 2>/dev/null)" ]; then \
101+
echo "❌ ERROR: No translation files were downloaded - Crowdin download may have failed silently"; \
102+
echo "Check the output above for errors during the download process"; \
103+
rm -f contentcuration/locale/.crowdin-download-marker; \
104+
exit 1; \
105+
fi
106+
@echo "✅ Translation files downloaded successfully"
107+
rm -f contentcuration/locale/.crowdin-download-marker
108+
pnpm exec kolibri-i18n code-gen --output-dir ./contentcuration/contentcuration/frontend/shared/i18n
99109
$(MAKE) i18n-django-compilemessages
100-
pnpm exec kolibri-tools i18n-create-message-files -- --namespace contentcuration --searchPath ./contentcuration/contentcuration/frontend
110+
pnpm exec kolibri-i18n create-message-files --namespace contentcuration --searchPath ./contentcuration/contentcuration/frontend
101111

102112
i18n-download: i18n-download-translations
103113

104114
i18n-download-glossary:
105-
python node_modules/kolibri-tools/lib/i18n/crowdin.py download-glossary
115+
pnpm exec crowdin glossary download
106116

107117
i18n-upload-glossary:
108-
python node_modules/kolibri-tools/lib/i18n/crowdin.py upload-glossary
118+
pnpm exec crowdin glossary upload
109119

110120
###############################################################
111121
# END I18N COMMANDS ###########################################

babel.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
module.exports = require('kolibri-tools/babel.config');
1+
module.exports = require('kolibri-build/babel.config');

contentcuration/contentcuration/frontend/administration/components/sidePanels/__tests__/ReviewSubmissionSidePanel.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ describe('ReviewSubmissionSidePanel', () => {
153153
expect(wrapper.find('[data-test="languages"]').text()).toBe('English, Czech');
154154
expect(wrapper.find('[data-test="categories"]').text()).toBe('School, Algebra');
155155
expect(wrapper.find('[data-test="licenses"]').text()).toBe('CC BY, CC BY-SA');
156-
expect(wrapper.findComponent(CommunityLibraryStatusChip).attributes('status')).toEqual(
156+
expect(wrapper.findComponent(CommunityLibraryStatusChip).props('status')).toEqual(
157157
submission.status,
158158
);
159159
expect(wrapper.find('[data-test="submission-notes"]').text()).toBe(submission.description);

contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LoadingText.vue

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@
66
class="loader-wrapper"
77
>
88
<KCircularLoader :size="16" />
9-
{{ $tr('checking') }}
109
</div>
1110
<div v-else-if="finishedLoading">
1211
<slot></slot>
1312
</div>
14-
<div v-else>{{ $tr('error') }}</div>
13+
<div v-else>{{ communityChannelsStrings.$tr('loadError') }}</div>
1514
</div>
1615
<div v-else><KEmptyPlaceholder /></div>
1716

@@ -20,6 +19,8 @@
2019

2120
<script setup>
2221
22+
import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings';
23+
2324
defineProps({
2425
loading: {
2526
type: Boolean,
@@ -39,18 +40,6 @@
3940
</script>
4041

4142

42-
<script>
43-
44-
export default {
45-
$trs: {
46-
checking: 'Checking...',
47-
error: 'Error loading data.',
48-
},
49-
};
50-
51-
</script>
52-
53-
5443
<style scoped lang="scss">
5544
5645
.loader-wrapper {

contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/__tests__/SubmitToCommunityLibrarySidePanel.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ describe('SubmitToCommunityLibrarySidePanel', () => {
431431
});
432432

433433
const statusChip = wrapper.findComponent(CommunityLibraryStatusChip);
434-
expect(statusChip.attributes('status')).toBe(chipStatus);
434+
expect(statusChip.props('status')).toBe(chipStatus);
435435
});
436436
}
437437

contentcuration/contentcuration/frontend/shared/i18n/index.js

Lines changed: 19 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export function crossComponentTranslator(Component) {
190190
return new Translator(Component.name, Component.$trs);
191191
}
192192

193-
function _setUpVueIntl() {
193+
async function _setUpVueIntl() {
194194
/**
195195
* Use the vue-intl plugin.
196196
*
@@ -213,7 +213,12 @@ function _setUpVueIntl() {
213213
if (window.ALL_MESSAGES) {
214214
Vue.registerMessages(currentLanguage, window.ALL_MESSAGES);
215215
}
216-
importVueIntlLocaleData().forEach(localeData => VueIntl.addLocaleData(localeData));
216+
217+
// Load vue-intl locale data asynchronously for current language
218+
const vueIntlLanguageCode = languageIdToCode(currentLanguage);
219+
const module = await importVueIntlLocaleData(vueIntlLanguageCode);
220+
const localeData = module.default || module;
221+
VueIntl.addLocaleData(localeData);
217222

218223
_i18nReady = true;
219224
}
@@ -229,7 +234,7 @@ export function updateTabTitle(title) {
229234
}
230235
}
231236

232-
export function i18nSetup(skipPolyfill = false) {
237+
export async function i18nSetup(skipPolyfill = false) {
233238
/**
234239
* Load fonts, app strings, and Intl polyfills
235240
**/
@@ -250,38 +255,18 @@ export function i18nSetup(skipPolyfill = false) {
250255

251256
// If the browser doesn't support the Intl polyfill, we retrieve that and
252257
// the modules need to wait until that happens.
253-
return new Promise((resolve, reject) => {
254-
if (Object.prototype.hasOwnProperty.call(global, 'Intl') || skipPolyfill) {
255-
_setUpVueIntl();
256-
resolve();
257-
} else {
258-
Promise.all([
259-
new Promise(res => {
260-
require.ensure(
261-
['intl'],
262-
require => {
263-
res(() => require('intl'));
264-
},
265-
'intl',
266-
);
267-
}),
268-
importIntlLocale(currentLanguage),
269-
]).then(
270-
// eslint-disable-line
271-
([requireIntl, requireIntlLocaleData]) => {
272-
requireIntl(); // requireIntl must run before requireIntlLocaleData
273-
requireIntlLocaleData();
274-
_setUpVueIntl();
275-
resolve();
276-
},
277-
error => {
278-
// eslint-disable-next-line no-console
279-
console.error('An error occurred trying to setup Internationalization', error);
280-
reject();
281-
},
282-
);
258+
if (Object.prototype.hasOwnProperty.call(global, 'Intl') || skipPolyfill) {
259+
await _setUpVueIntl();
260+
} else {
261+
try {
262+
await Promise.all([import('intl'), importIntlLocale(currentLanguage)]);
263+
await _setUpVueIntl();
264+
} catch (error) {
265+
// eslint-disable-next-line no-console
266+
console.error('An error occurred trying to setup Internationalization', error);
267+
throw error;
283268
}
284-
});
269+
}
285270
}
286271

287272
/**

0 commit comments

Comments
 (0)