diff --git a/Makefile b/Makefile index a5ab86ef..aefe2dce 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,10 @@ UNAME := $(shell uname) +transifex_langs = "ar,fr,es_419,zh_CN" i18n = ./src/i18n transifex_input = $(i18n)/transifex_input.json transifex_utils = ./node_modules/.bin/edx_reactifex +generate_supported_langs = src/i18n/scripts/generateSupportedLangs.js # This directory must match .babelrc . transifex_temp = ./temp/babel-plugin-react-intl @@ -90,9 +92,19 @@ push_translations: $$(npm bin)/edx_reactifex $(transifex_temp) --comments --v3-scripts-path ./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh -pull_translations: ## must be exactly this name for edx tooling support, see ecommerce-scripts/transifex/pull.py - # explicit list of languages defined here and in currentlySupportedLangs.jsx - tx pull -t -f --mode reviewed --languages="ar,fr,es_419,zh_CN" + +ifeq ($(OPENEDX_ATLAS_PULL),) +# Pulls translations from Transifex. +pull_translations: + tx pull -t -f --mode reviewed --languages=$(transifex_langs) +else +# Experimental: OEP-58 Pulls translations using atlas +pull_translations: + rm -rf src/i18n/messages + cd src/i18n/ \ + && atlas pull --filter=$(transifex_langs) translations/studio-frontend/src/i18n/messages:messages + $(generate_supported_langs) $(transifex_langs) +endif copy-dist: for f in dist/*; do docker cp $$f edx.devstack.studio:/edx/app/edxapp/edx-platform/node_modules/@edx/studio-frontend/dist/; done diff --git a/src/i18n/scripts/generateSupportedLangs.js b/src/i18n/scripts/generateSupportedLangs.js new file mode 100755 index 00000000..fdb3ecd1 --- /dev/null +++ b/src/i18n/scripts/generateSupportedLangs.js @@ -0,0 +1,119 @@ +#!/usr/bin/env node + +const scriptHelpDocument = ` +NAME + generateSupportedLangs.js — Script to generate the 'src/i18n/messages/currentlySupportedLangs.jsx' file which contains static import for react-intl data. + +SYNOPSIS + generateSupportedLangs.js [comma separated list of languages] + + + +DESCRIPTION + + Run this script after 'atlas' has pulled the files in the following structure: + + $ generateSupportedLangs.js ar,es_419,fr_CA + + 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. +`; + +const fs = require('fs'); +const path = require('path'); + +const loggingPrefix = path.basename(`${__filename}`); // the name of this JS file + +// Header note for generated src/i18n/index.js file +const filesCodeGeneratorNoticeHeader = '// This file is generated by the "i18n/scripts/generateSupportedLangs.js" script.'; + +/** + * Create main `src/i18n/index.js` messages import file. + * + * + * @param languages - List of directories with a boolean flag whether its "index.js" file is written + * The format is "[\{ directory: "frontend-component-example", isWritten: false \}, ...]" + * @param log - Mockable process.stdout.write + * @param writeFileSync - Mockable fs.writeFileSync + * @param i18nDir` - Path to `src/i18n` directory + */ +function generateSupportedLangsFile({ + languages, + log, + writeFileSync, + i18nDir, +}) { + const importLines = []; + const exportLines = []; + + languages.forEach(language => { + const [languageFamilyCode] = language.split('_'); // Get `es` from `es-419` + + const importVariableName = `${languageFamilyCode.toLowerCase()}Data`; + const dashLanguageCode = language.toLowerCase().replace(/_/g, '-'); + importLines.push(`import ${importVariableName} from 'react-intl/locale-data/${languageFamilyCode}';`); + + // Note: These imports are not directly consumed by the studio-frontend React app. They're imported to ensure that + // the messages/*.json files exists and they can be loaded via the load_sfe_i18n_messages() function in + // the `edx-platform`. + // + // This pattern should probably be refactored to pull the translations directly within the `edx-platform`. + const jsonFilename = `${language}.json`; + if (fs.existsSync(`${i18nDir}/messages/${jsonFilename}`)) { + importLines.push(`import './${jsonFilename}';`); + log(`${loggingPrefix}: Notice: Not importing 'messages/${jsonFilename}' because the file wasn't found.\n`); + } + + exportLines.push(` '${dashLanguageCode}': ${importVariableName},`); + }); + + // See the help message above for sample output. + const indexFileContent = [ + filesCodeGeneratorNoticeHeader, + importLines.join('\n'), + '\nexport default {', + exportLines.join('\n'), + '};\n', + ].join('\n'); + + writeFileSync(`${i18nDir}/messages/currentlySupportedLangs.jsx`, indexFileContent); +} + +/* + * Main function of the file. + */ +function main({ + parameters, + log, + writeFileSync, + pwd, +}) { + const i18nDir = `${pwd}/src/i18n`; // The Micro-frontend i18n root directory + const [languagesString] = parameters; + + if (parameters.includes('--help') || parameters.includes('-h')) { + log(scriptHelpDocument); + } else if (!parameters.length) { + log(scriptHelpDocument); + log(`${loggingPrefix}: Error: A comma separated list of languages is required.\n`); + } else { + generateSupportedLangsFile({ + languages: languagesString.split(','), + log, + writeFileSync, + i18nDir, + }); + log(`${loggingPrefix}: Finished generating the 'currentlySupportedLangs.jsx' file.`); + } +} + +if (require.main === module) { + // Run the main() function if called from the command line. + main({ + parameters: process.argv.slice(2), + log: text => process.stdout.write(text), + writeFileSync: fs.writeFileSync, + pwd: process.env.PWD, + }); +} + +module.exports.main = main; // Allow tests to use the main function. diff --git a/src/i18n/scripts/generateSupportedLangs.test.js b/src/i18n/scripts/generateSupportedLangs.test.js new file mode 100755 index 00000000..0f0afb08 --- /dev/null +++ b/src/i18n/scripts/generateSupportedLangs.test.js @@ -0,0 +1,90 @@ +// Tests for the generateSupportedLangs.js command line. + +import path from 'path'; +import { main as realMain } from './generateSupportedLangs'; + +const sempleAppDirectory = path.join(__dirname, '../../../test-app'); + +// History for `process.stdout.write` mock calls. +const logHistory = { + log: [], + latest: '', +}; + +// History for `fs.writeFileSync` mock calls. +const writeFileHistory = { + log: [], + latest: null, +}; + +// Mock for process.stdout.write +const log = (text) => { + logHistory.latest = text; + logHistory.log.push(text); +}; + +// Mock for fs.writeFileSync +const writeFileSync = (filename, content) => { + const entry = { filename, content }; + writeFileHistory.latest = entry; + writeFileHistory.log.push(entry); +}; + +// Main with mocked output +const main = (...parameters) => realMain({ + parameters, + log, + writeFileSync, + pwd: sempleAppDirectory, +}); + +// Clean up mock histories +afterEach(() => { + logHistory.log = []; + logHistory.latest = null; + writeFileHistory.log = []; + writeFileHistory.latest = null; +}); + +describe('help document', () => { + it('should print help for --help', () => { + main('--help'); + expect(logHistory.latest).toMatch( + "generateSupportedLangs.js — Script to generate the 'src/i18n/messages/currentlySupportedLangs.jsx'" + ); + }); + + it('should print help for -h', () => { + main('-h'); + expect(logHistory.latest).toMatch( + "generateSupportedLangs.js — Script to generate the 'src/i18n/messages/currentlySupportedLangs.jsx'" + ); + }); +}); + +describe('generate with three languages', () => { + main('ar,de,fr_CA'); // German doesn't have a messages file in the `test-app` + + expect(writeFileHistory.log.length).toBe(1); + expect(writeFileHistory.latest.filename).toBe(`${sempleAppDirectory}/src/i18n/messages/currentlySupportedLangs.jsx`); + + // It should write the file with the following content: + // - import 'react-intl/locale-data/ar' and ar.json messages + // - import 'react-intl/locale-data/de' without de.json because it doesn't exist in the + // test-app/src/i18n/messages directory + // - import 'react-intl/locale-data/fr' and fr_CA.json messages + // - export the imported locale-data + expect(writeFileHistory.latest.content).toMatch(`// This file is generated by the "i18n/scripts/generateSupportedLangs.js" script. +import arData from 'react-intl/locale-data/ar'; +import './ar.json'; +import deData from 'react-intl/locale-data/de'; +import frData from 'react-intl/locale-data/fr'; +import './fr_CA.json'; + +export default { + 'ar': arData, + 'de': deData, + 'fr-ca': frData, +}; +`); +}); diff --git a/test-app/src/i18n/README.md b/test-app/src/i18n/README.md new file mode 100644 index 00000000..945fdd2f --- /dev/null +++ b/test-app/src/i18n/README.md @@ -0,0 +1,3 @@ +# Test i18n directory + +These test files are used by the `src/i18n/scripts/generateSupportedLangs.test.js` file. diff --git a/test-app/src/i18n/messages/ar.json b/test-app/src/i18n/messages/ar.json new file mode 100644 index 00000000..f6ee667b --- /dev/null +++ b/test-app/src/i18n/messages/ar.json @@ -0,0 +1,3 @@ +{ + "a11yBodyPolicyLink": "سياسة إمكانية الوصول" +} \ No newline at end of file diff --git a/test-app/src/i18n/messages/fr_CA.json b/test-app/src/i18n/messages/fr_CA.json new file mode 100644 index 00000000..4128aea2 --- /dev/null +++ b/test-app/src/i18n/messages/fr_CA.json @@ -0,0 +1,3 @@ +{ + "a11yBodyPolicyLink": "Politique d'accessibilité" +} \ No newline at end of file diff --git a/test-app/src/i18n/messages/zh_CN.json b/test-app/src/i18n/messages/zh_CN.json new file mode 100644 index 00000000..6b1e0771 --- /dev/null +++ b/test-app/src/i18n/messages/zh_CN.json @@ -0,0 +1,3 @@ +{ + "a11yBodyPolicyLink": "网站可访问策略" +} \ No newline at end of file