Skip to content
This repository was archived by the owner on Oct 21, 2025. It is now read-only.

Commit e5a3ee7

Browse files
Merge pull request #380 from Zeit-Labs/atlas
feat: use `atlas` in `make pull_translations` Refs: FC-0012 OEP-58
2 parents d3da9c6 + 2116cdb commit e5a3ee7

File tree

7 files changed

+236
-3
lines changed

7 files changed

+236
-3
lines changed

Makefile

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
UNAME := $(shell uname)
22

3+
transifex_langs = "ar,fr,es_419,zh_CN"
34
i18n = ./src/i18n
45
transifex_input = $(i18n)/transifex_input.json
56
transifex_utils = ./node_modules/.bin/edx_reactifex
7+
generate_supported_langs = src/i18n/scripts/generateSupportedLangs.js
68

79
# This directory must match .babelrc .
810
transifex_temp = ./temp/babel-plugin-react-intl
@@ -90,9 +92,19 @@ push_translations:
9092
$$(npm bin)/edx_reactifex $(transifex_temp) --comments --v3-scripts-path
9193
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
9294

93-
pull_translations: ## must be exactly this name for edx tooling support, see ecommerce-scripts/transifex/pull.py
94-
# explicit list of languages defined here and in currentlySupportedLangs.jsx
95-
tx pull -t -f --mode reviewed --languages="ar,fr,es_419,zh_CN"
95+
96+
ifeq ($(OPENEDX_ATLAS_PULL),)
97+
# Pulls translations from Transifex.
98+
pull_translations:
99+
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
100+
else
101+
# Experimental: OEP-58 Pulls translations using atlas
102+
pull_translations:
103+
rm -rf src/i18n/messages
104+
cd src/i18n/ \
105+
&& atlas pull --filter=$(transifex_langs) translations/studio-frontend/src/i18n/messages:messages
106+
$(generate_supported_langs) $(transifex_langs)
107+
endif
96108

97109
copy-dist:
98110
for f in dist/*; do docker cp $$f edx.devstack.studio:/edx/app/edxapp/edx-platform/node_modules/@edx/studio-frontend/dist/; done
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env node
2+
3+
const scriptHelpDocument = `
4+
NAME
5+
generateSupportedLangs.js — Script to generate the 'src/i18n/messages/currentlySupportedLangs.jsx' file which contains static import for react-intl data.
6+
7+
SYNOPSIS
8+
generateSupportedLangs.js [comma separated list of languages]
9+
10+
11+
12+
DESCRIPTION
13+
14+
Run this script after 'atlas' has pulled the files in the following structure:
15+
16+
$ generateSupportedLangs.js ar,es_419,fr_CA
17+
18+
This script is intended as a temporary solution until the studio-frontend can dynamically load the languages from the react-intl data like the other micro-frontends.
19+
`;
20+
21+
const fs = require('fs');
22+
const path = require('path');
23+
24+
const loggingPrefix = path.basename(`${__filename}`); // the name of this JS file
25+
26+
// Header note for generated src/i18n/index.js file
27+
const filesCodeGeneratorNoticeHeader = '// This file is generated by the "i18n/scripts/generateSupportedLangs.js" script.';
28+
29+
/**
30+
* Create main `src/i18n/index.js` messages import file.
31+
*
32+
*
33+
* @param languages - List of directories with a boolean flag whether its "index.js" file is written
34+
* The format is "[\{ directory: "frontend-component-example", isWritten: false \}, ...]"
35+
* @param log - Mockable process.stdout.write
36+
* @param writeFileSync - Mockable fs.writeFileSync
37+
* @param i18nDir` - Path to `src/i18n` directory
38+
*/
39+
function generateSupportedLangsFile({
40+
languages,
41+
log,
42+
writeFileSync,
43+
i18nDir,
44+
}) {
45+
const importLines = [];
46+
const exportLines = [];
47+
48+
languages.forEach(language => {
49+
const [languageFamilyCode] = language.split('_'); // Get `es` from `es-419`
50+
51+
const importVariableName = `${languageFamilyCode.toLowerCase()}Data`;
52+
const dashLanguageCode = language.toLowerCase().replace(/_/g, '-');
53+
importLines.push(`import ${importVariableName} from 'react-intl/locale-data/${languageFamilyCode}';`);
54+
55+
// Note: These imports are not directly consumed by the studio-frontend React app. They're imported to ensure that
56+
// the messages/*.json files exists and they can be loaded via the load_sfe_i18n_messages() function in
57+
// the `edx-platform`.
58+
//
59+
// This pattern should probably be refactored to pull the translations directly within the `edx-platform`.
60+
const jsonFilename = `${language}.json`;
61+
if (fs.existsSync(`${i18nDir}/messages/${jsonFilename}`)) {
62+
importLines.push(`import './${jsonFilename}';`);
63+
log(`${loggingPrefix}: Notice: Not importing 'messages/${jsonFilename}' because the file wasn't found.\n`);
64+
}
65+
66+
exportLines.push(` '${dashLanguageCode}': ${importVariableName},`);
67+
});
68+
69+
// See the help message above for sample output.
70+
const indexFileContent = [
71+
filesCodeGeneratorNoticeHeader,
72+
importLines.join('\n'),
73+
'\nexport default {',
74+
exportLines.join('\n'),
75+
'};\n',
76+
].join('\n');
77+
78+
writeFileSync(`${i18nDir}/messages/currentlySupportedLangs.jsx`, indexFileContent);
79+
}
80+
81+
/*
82+
* Main function of the file.
83+
*/
84+
function main({
85+
parameters,
86+
log,
87+
writeFileSync,
88+
pwd,
89+
}) {
90+
const i18nDir = `${pwd}/src/i18n`; // The Micro-frontend i18n root directory
91+
const [languagesString] = parameters;
92+
93+
if (parameters.includes('--help') || parameters.includes('-h')) {
94+
log(scriptHelpDocument);
95+
} else if (!parameters.length) {
96+
log(scriptHelpDocument);
97+
log(`${loggingPrefix}: Error: A comma separated list of languages is required.\n`);
98+
} else {
99+
generateSupportedLangsFile({
100+
languages: languagesString.split(','),
101+
log,
102+
writeFileSync,
103+
i18nDir,
104+
});
105+
log(`${loggingPrefix}: Finished generating the 'currentlySupportedLangs.jsx' file.`);
106+
}
107+
}
108+
109+
if (require.main === module) {
110+
// Run the main() function if called from the command line.
111+
main({
112+
parameters: process.argv.slice(2),
113+
log: text => process.stdout.write(text),
114+
writeFileSync: fs.writeFileSync,
115+
pwd: process.env.PWD,
116+
});
117+
}
118+
119+
module.exports.main = main; // Allow tests to use the main function.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Tests for the generateSupportedLangs.js command line.
2+
3+
import path from 'path';
4+
import { main as realMain } from './generateSupportedLangs';
5+
6+
const sempleAppDirectory = path.join(__dirname, '../../../test-app');
7+
8+
// History for `process.stdout.write` mock calls.
9+
const logHistory = {
10+
log: [],
11+
latest: '',
12+
};
13+
14+
// History for `fs.writeFileSync` mock calls.
15+
const writeFileHistory = {
16+
log: [],
17+
latest: null,
18+
};
19+
20+
// Mock for process.stdout.write
21+
const log = (text) => {
22+
logHistory.latest = text;
23+
logHistory.log.push(text);
24+
};
25+
26+
// Mock for fs.writeFileSync
27+
const writeFileSync = (filename, content) => {
28+
const entry = { filename, content };
29+
writeFileHistory.latest = entry;
30+
writeFileHistory.log.push(entry);
31+
};
32+
33+
// Main with mocked output
34+
const main = (...parameters) => realMain({
35+
parameters,
36+
log,
37+
writeFileSync,
38+
pwd: sempleAppDirectory,
39+
});
40+
41+
// Clean up mock histories
42+
afterEach(() => {
43+
logHistory.log = [];
44+
logHistory.latest = null;
45+
writeFileHistory.log = [];
46+
writeFileHistory.latest = null;
47+
});
48+
49+
describe('help document', () => {
50+
it('should print help for --help', () => {
51+
main('--help');
52+
expect(logHistory.latest).toMatch(
53+
"generateSupportedLangs.js — Script to generate the 'src/i18n/messages/currentlySupportedLangs.jsx'"
54+
);
55+
});
56+
57+
it('should print help for -h', () => {
58+
main('-h');
59+
expect(logHistory.latest).toMatch(
60+
"generateSupportedLangs.js — Script to generate the 'src/i18n/messages/currentlySupportedLangs.jsx'"
61+
);
62+
});
63+
});
64+
65+
describe('generate with three languages', () => {
66+
main('ar,de,fr_CA'); // German doesn't have a messages file in the `test-app`
67+
68+
expect(writeFileHistory.log.length).toBe(1);
69+
expect(writeFileHistory.latest.filename).toBe(`${sempleAppDirectory}/src/i18n/messages/currentlySupportedLangs.jsx`);
70+
71+
// It should write the file with the following content:
72+
// - import 'react-intl/locale-data/ar' and ar.json messages
73+
// - import 'react-intl/locale-data/de' without de.json because it doesn't exist in the
74+
// test-app/src/i18n/messages directory
75+
// - import 'react-intl/locale-data/fr' and fr_CA.json messages
76+
// - export the imported locale-data
77+
expect(writeFileHistory.latest.content).toMatch(`// This file is generated by the "i18n/scripts/generateSupportedLangs.js" script.
78+
import arData from 'react-intl/locale-data/ar';
79+
import './ar.json';
80+
import deData from 'react-intl/locale-data/de';
81+
import frData from 'react-intl/locale-data/fr';
82+
import './fr_CA.json';
83+
84+
export default {
85+
'ar': arData,
86+
'de': deData,
87+
'fr-ca': frData,
88+
};
89+
`);
90+
});

test-app/src/i18n/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Test i18n directory
2+
3+
These test files are used by the `src/i18n/scripts/generateSupportedLangs.test.js` file.

test-app/src/i18n/messages/ar.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"a11yBodyPolicyLink": "سياسة إمكانية الوصول"
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"a11yBodyPolicyLink": "Politique d'accessibilité"
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"a11yBodyPolicyLink": "网站可访问策略"
3+
}

0 commit comments

Comments
 (0)