From 4c52228600f9fe2297994f83686317e9d72627f0 Mon Sep 17 00:00:00 2001 From: Omar Al-Ithawi Date: Fri, 2 Feb 2024 15:52:15 +0300 Subject: [PATCH] feat: generate currentlySupportedLangs.jsx dynamically instead of hardcoded `transifex_langs = "ar,fr,es_419,zh_CN"` now it generates the file based on the `atlas pull` result --- Makefile | 5 +- .../scripts/generateSupportedLangs.test.js | 90 ------------ .../i18n/scripts/generateSupportedLangs.js | 72 +++++----- .../scripts/generateSupportedLangs.test.js | 128 ++++++++++++++++++ test-app/src/i18n/README.md | 3 - test-apps/README.md | 3 + .../src/i18n/messages/ar.json | 0 .../src/i18n/messages/fr_CA.json | 0 .../src/i18n/messages/zh_CN.json | 0 .../src/i18n/messages/.gitkeep | 1 + 10 files changed, 174 insertions(+), 128 deletions(-) delete mode 100755 src/i18n/scripts/generateSupportedLangs.test.js rename src/{ => utils}/i18n/scripts/generateSupportedLangs.js (65%) create mode 100755 src/utils/i18n/scripts/generateSupportedLangs.test.js delete mode 100644 test-app/src/i18n/README.md create mode 100644 test-apps/README.md rename {test-app => test-apps/app-with-translations}/src/i18n/messages/ar.json (100%) rename {test-app => test-apps/app-with-translations}/src/i18n/messages/fr_CA.json (100%) rename {test-app => test-apps/app-with-translations}/src/i18n/messages/zh_CN.json (100%) create mode 100644 test-apps/app-without-translations/src/i18n/messages/.gitkeep diff --git a/Makefile b/Makefile index aefe2dce..fd7e1659 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,6 @@ 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 @@ -102,8 +101,8 @@ else 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) + && atlas pull $(ATLAS_OPTIONS) translations/studio-frontend/src/i18n/messages:messages + cd src/i18n/ && node ../../src/utils/i18n/scripts/generateSupportedLangs.js endif copy-dist: diff --git a/src/i18n/scripts/generateSupportedLangs.test.js b/src/i18n/scripts/generateSupportedLangs.test.js deleted file mode 100755 index 0f0afb08..00000000 --- a/src/i18n/scripts/generateSupportedLangs.test.js +++ /dev/null @@ -1,90 +0,0 @@ -// 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/src/i18n/scripts/generateSupportedLangs.js b/src/utils/i18n/scripts/generateSupportedLangs.js similarity index 65% rename from src/i18n/scripts/generateSupportedLangs.js rename to src/utils/i18n/scripts/generateSupportedLangs.js index fdb3ecd1..1f4fc1ff 100755 --- a/src/i18n/scripts/generateSupportedLangs.js +++ b/src/utils/i18n/scripts/generateSupportedLangs.js @@ -5,15 +5,17 @@ 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] - + generateSupportedLangs.js [-h | --help] MESSAGES_DIR DESCRIPTION Run this script after 'atlas' has pulled the files in the following structure: - - $ generateSupportedLangs.js ar,es_419,fr_CA + + $ node src/utils/i18n/scripts/generateSupportedLangs.js src/i18n/messages + + This script will generate the 'src/i18n/messages/currentlySupportedLangs.jsx' file which contains static import for + react-intl data based on the JSON language files present in the 'src/i18n/messages' directory. 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. `; @@ -24,7 +26,7 @@ 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.'; +const filesCodeGeneratorNoticeHeader = '// This file is generated by the "generateSupportedLangs.js" script.'; /** * Create main `src/i18n/index.js` messages import file. @@ -32,15 +34,13 @@ const filesCodeGeneratorNoticeHeader = '// This file is generated by the "i18n/s * * @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, + i18nMessagesDir, }) { const importLines = []; const exportLines = []; @@ -58,11 +58,7 @@ function generateSupportedLangsFile({ // // 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`); - } - + importLines.push(`import './${jsonFilename}';`); exportLines.push(` '${dashLanguageCode}': ${importVariableName},`); }); @@ -75,45 +71,57 @@ function generateSupportedLangsFile({ '};\n', ].join('\n'); - writeFileSync(`${i18nDir}/messages/currentlySupportedLangs.jsx`, indexFileContent); + writeFileSync(`${i18nMessagesDir}/currentlySupportedLangs.jsx`, indexFileContent); } /* * Main function of the file. */ function main({ - parameters, log, writeFileSync, - pwd, + i18nMessagesDir, }) { - const i18nDir = `${pwd}/src/i18n`; // The Micro-frontend i18n root directory - const [languagesString] = parameters; + if (!i18nMessagesDir) { + log(scriptHelpDocument); + log(`${loggingPrefix}: Error: The "MESSAGES_DIR" parameter is required.\n`); + return false; + } - if (parameters.includes('--help') || parameters.includes('-h')) { + if (i18nMessagesDir === '-h' || i18nMessagesDir === '--help') { log(scriptHelpDocument); - } else if (!parameters.length) { + return true; + } + + const languageFiles = fs.readdirSync(`${i18nMessagesDir}`).filter(file => file.endsWith('.json')); + const languages = languageFiles.map(file => file.replace('.json', '')); + languages.sort(); + + if (!languages.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.`); + log(`${loggingPrefix}: Error: No language files found in the "${i18nMessagesDir}"'.\n`); + return false; } + + generateSupportedLangsFile({ + languages, + writeFileSync, + i18nMessagesDir, + }); + log(`${loggingPrefix}: Finished generating the 'currentlySupportedLangs.jsx' file.\n`); + return true; } +// istanbul ignore next if (require.main === module) { // Run the main() function if called from the command line. - main({ - parameters: process.argv.slice(2), + const success = main({ + i18nMessagesDir: process.argv[2], log: text => process.stdout.write(text), writeFileSync: fs.writeFileSync, - pwd: process.env.PWD, }); + + process.exit(success ? 0 : 1); } module.exports.main = main; // Allow tests to use the main function. diff --git a/src/utils/i18n/scripts/generateSupportedLangs.test.js b/src/utils/i18n/scripts/generateSupportedLangs.test.js new file mode 100755 index 00000000..bc4410d1 --- /dev/null +++ b/src/utils/i18n/scripts/generateSupportedLangs.test.js @@ -0,0 +1,128 @@ +// Tests for the generateSupportedLangs.js command line. + +import path from 'path'; +import { main as realMain } from './generateSupportedLangs'; + +const sempleAppsDirectory = path.join(__dirname, '../../../../test-apps'); + +// History for `process.stdout.write` mock calls. +const logHistory = { + log: [], + latest: null, +}; + +// 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 = (args) => realMain({ + log, + writeFileSync, + i18nMessagesDir: `${sempleAppsDirectory}/app-with-translations/src/i18n/messages`, + ...args, +}); + +// Clean up mock histories +beforeEach(() => { + logHistory.log = []; + logHistory.latest = null; + writeFileHistory.log = []; + writeFileHistory.latest = null; +}); + +describe('help document', () => { + it('should print help for --help', () => { + const success = main({ + i18nMessagesDir: '--help', + }); + expect(logHistory.latest).toMatch( + "generateSupportedLangs.js — Script to generate the 'src/i18n/messages/currentlySupportedLangs.jsx'" + ); + expect(success).toBe(true); + }); + + it('should print help for -h', () => { + const success = main({ + i18nMessagesDir: '--help', + }); + expect(logHistory.latest).toMatch( + "generateSupportedLangs.js — Script to generate the 'src/i18n/messages/currentlySupportedLangs.jsx'" + ); + expect(success).toBe(true); + }); +}); + +describe('generate with three languages', () => { + it('should generate currentlySupportedLangs.jsx', () => { + const success = main({ + i18nMessagesDir: `${sempleAppsDirectory}/app-with-translations/src/i18n/messages`, + }); + + expect(writeFileHistory.log.length).toBe(1); + expect(writeFileHistory.latest.filename).toBe(`${sempleAppsDirectory}/app-with-translations/src/i18n/messages/currentlySupportedLangs.jsx`); + expect(success).toBe(true); // Languages generated successfully + + // 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 "generateSupportedLangs.js" script. +import arData from 'react-intl/locale-data/ar'; +import './ar.json'; +import frData from 'react-intl/locale-data/fr'; +import './fr_CA.json'; +import zhData from 'react-intl/locale-data/zh'; +import './zh_CN.json'; + +export default { + 'ar': arData, + 'fr-ca': frData, + 'zh-cn': zhData, +}; +`); + }); +}); + +describe('generate errors', () => { + it('should fail with no languages', () => { + const success = main({ + i18nMessagesDir: `${sempleAppsDirectory}/app-without-translations/src/i18n/messages`, + }); + + // It should fail with the following error message: + expect(logHistory.latest).toContain('generateSupportedLangs.js: Error: No language files found in the "'); + + expect(writeFileHistory.log).toEqual([]); + expect(success).toBe(false); // No languages to generate + }); + + it('should fail with no MESSAGES_DIR parameter', () => { + const success = main({ + i18nMessagesDir: '', + }); + + // It should fail with the following error message: + expect(logHistory.latest).toBe('generateSupportedLangs.js: Error: The "MESSAGES_DIR" parameter is required.\n'); + + expect(writeFileHistory.log).toEqual([]); + expect(success).toBe(false); // MESSAGES_DIR parameter is required + }); +}); diff --git a/test-app/src/i18n/README.md b/test-app/src/i18n/README.md deleted file mode 100644 index 945fdd2f..00000000 --- a/test-app/src/i18n/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Test i18n directory - -These test files are used by the `src/i18n/scripts/generateSupportedLangs.test.js` file. diff --git a/test-apps/README.md b/test-apps/README.md new file mode 100644 index 00000000..1e042df1 --- /dev/null +++ b/test-apps/README.md @@ -0,0 +1,3 @@ +# Test apps + +These test apps are used by the `src/utils/i18n/scripts/generateSupportedLangs.test.js` file. diff --git a/test-app/src/i18n/messages/ar.json b/test-apps/app-with-translations/src/i18n/messages/ar.json similarity index 100% rename from test-app/src/i18n/messages/ar.json rename to test-apps/app-with-translations/src/i18n/messages/ar.json diff --git a/test-app/src/i18n/messages/fr_CA.json b/test-apps/app-with-translations/src/i18n/messages/fr_CA.json similarity index 100% rename from test-app/src/i18n/messages/fr_CA.json rename to test-apps/app-with-translations/src/i18n/messages/fr_CA.json diff --git a/test-app/src/i18n/messages/zh_CN.json b/test-apps/app-with-translations/src/i18n/messages/zh_CN.json similarity index 100% rename from test-app/src/i18n/messages/zh_CN.json rename to test-apps/app-with-translations/src/i18n/messages/zh_CN.json diff --git a/test-apps/app-without-translations/src/i18n/messages/.gitkeep b/test-apps/app-without-translations/src/i18n/messages/.gitkeep new file mode 100644 index 00000000..1af8dc07 --- /dev/null +++ b/test-apps/app-without-translations/src/i18n/messages/.gitkeep @@ -0,0 +1 @@ +# Empty file to preserve directory structure \ No newline at end of file