Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ node_modules/
/auth-storage/
/playwright-report/
/base-tests/
/i18n/

#files
/*.config.ts
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,40 @@ npx playwright test --grep-invert "@setup" --trace on

---

## 🌐 Generate Translations

The Magento 2 Playwright Testing Suite supports translations, allowing you to run tests in multiple languages. This is particularly useful for international stores with multilingual sites.

### Setting Up Translations

1. **Directory Structure**: Ensure your playwright suite is located in the `app/design/Vendor/theme/web/playwright` directory within your Magento installation. This is crucial for the Playwright suite to locate and utilize the correct files from magento.


2. **(Optional) Create Test Files**: Go to step 3 when this is the NPM installed package. Create the following directories and file:
- `i18n/app/nl_NL.csv`
- `i18n/vendor/`

After creating these, populate `nl_NL.csv` with entries that match texts from `./config/element-identifiers.json`. For instance, you might add `"Password", "Wachtwoord"`. Alternatively, you can copy a translation file from Magento into the `i18n/app` directory.

3. **Generate translation files**: run following command: `node translate-json.js nl_NL`. `nl_NL` is the language you want to translate to. For example; it will look for nl_NL.csv

3. **Configuration**: Add or Update `MAGENTO_THEME_LOCALE` configuration in your `.env` file to specify which translations to use during testing.

By following these steps, you can seamlessly integrate language support into your testing workflow, ensuring that your Magento 2 store is thoroughly tested across different languages.

### Troubleshooting

When getting the following error:

```
Translating file: i18n/nl_NL.csv
Error: Invalid Record Length: expect 2, got 1 on line 284
```

Go to line 284 to find what is wrong in your csv file.

---

## 🚀 How to use the testing suite

The Testing Suite offers a variety of tests for your Magento 2 application in Chromium, Firefox, and Webkit.
Expand Down
231 changes: 146 additions & 85 deletions translate-json.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,115 +10,176 @@ if (process.argv.length < 3) {
process.exit(1);
}

const locale = process.argv[2];
class TranslateJson {

// Function to find CSV files recursively
function findCsvFiles(dir, locale) {
let results = [];
const files = fs.readdirSync(dir);
pathToBaseDir = '../../../../../../../'; // default: when installed via npm (magento2 root folder)
locale = 'en_US';

for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
baseSourcePath = 'base-tests/config';
baseDestinationPath = 'tests/config';
jsonFiles = [
'element-identifiers.json',
'outcome-markers.json'
];

if (stat.isDirectory()) {
results = results.concat(findCsvFiles(filePath, locale));
} else if (file === `${locale}.csv`) {
results.push(filePath);
}
}

return results;
}
constructor() {
const isLocalDev = fs.existsSync(path.resolve(__dirname, '.git'));

// Change paths when running translation script in gitrepo.
if (isLocalDev) {
this.pathToBaseDir = './i18n/';
this.baseSourcePath = 'tests/config';
this.baseDestinationPath = 'base-tests/config';
}

// Function to parse CSV file
function parseCsvFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const records = csv.parse(content, {
skip_empty_lines: true,
trim: true
});

const translations = {};
for (const [key, value] of records) {
translations[key] = value;
this.locale = process.argv[2];
}

return translations;
}
// Main execution
main() {
try {
// Find and parse CSV files
const appCsvFiles = this.findCsvFiles(this.pathToBaseDir + 'app', this.locale);
const vendorCsvFiles = this.findCsvFiles(this.pathToBaseDir + 'vendor', this.locale);

let appTranslations = {};
let vendorTranslations = {};

// Parse app translations
for (const file of appCsvFiles) {
const translations = this.parseCsvFile(file);
appTranslations = { ...appTranslations, ...translations };
}

// Parse vendor translations
for (const file of vendorCsvFiles) {
try {
const translations = this.parseCsvFile(file);
vendorTranslations = { ...vendorTranslations, ...translations };
} catch (error) {
console.error(`Error processing vendor file ${file}:`, error.message);
}
}

// Merge translations with app taking precedence
const translations = this.mergeTranslations(appTranslations, vendorTranslations);

// Process JSON files
for (const fileName of this.jsonFiles) {
const sourcePath = path.resolve(this.baseSourcePath, fileName);
const destPath = path.resolve(this.baseDestinationPath, fileName);

const content = JSON.parse(fs.readFileSync(sourcePath, 'utf-8'));
let translatedContent = this.translateObject(content, translations);

// Read existing translations if the file exists
if (fs.existsSync(destPath)) {
const existingContent = JSON.parse(fs.readFileSync(destPath, 'utf-8'));

// Combine existing and new translations, preserving existing ones
translatedContent = this.mergeTranslations(translatedContent, existingContent);
}

// Ensure target directory exists
fs.mkdirSync(path.dirname(destPath), { recursive: true });

fs.writeFileSync(destPath, JSON.stringify(translatedContent, null, 2));
console.log(`Translated file written: ${destPath}`);
}

console.log('Translation completed successfully!');
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}
}

// Function to merge translations with precedence
function mergeTranslations(appTranslations, vendorTranslations) {
return { ...vendorTranslations, ...appTranslations };
}
// Function to find CSV files recursively
findCsvFiles(dir, locale) {
let results = [];
if (!fs.statSync(dir).isDirectory()) {
return results;
}

// Function to translate values in an object recursively
function translateObject(obj, translations) {
if (typeof obj === 'string') {
return translations[obj] || obj;
}
const files = fs.readdirSync(dir);

if (Array.isArray(obj)) {
return obj.map(item => translateObject(item, translations));
}
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);

if (typeof obj === 'object' && obj !== null) {
const result = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = translateObject(value, translations);
if (stat.isDirectory()) {
results = results.concat(this.findCsvFiles(filePath, locale));
} else if (file === `${locale}.csv`) {
results.push(filePath);
}
}
return result;

return results;
}

return obj;
}
parseCsvFile(filePath) {
const relativeFilePath = filePath.replace('../../../../../../../', '');
console.log("Translating file: ", relativeFilePath);

// Main execution
try {
// Find and parse CSV files
const appCsvFiles = findCsvFiles('../../../../../../../app', locale);
const vendorCsvFiles = findCsvFiles('../../../../../../../vendor', locale);
const content = fs.readFileSync(filePath, 'utf-8');
const records = csv.parse(content, {
skip_empty_lines: true,
trim: true
});

let appTranslations = {};
let vendorTranslations = {};
const translations = {};
for (const [index, record] of records.entries()) {
const [key, value] = record;
translations[key] = value;
}

// Parse app translations
for (const file of appCsvFiles) {
const translations = parseCsvFile(file);
appTranslations = { ...appTranslations, ...translations };
}
console.log("Done...");

// Parse vendor translations
for (const file of vendorCsvFiles) {
const translations = parseCsvFile(file);
vendorTranslations = { ...vendorTranslations, ...translations };
return translations;
}

// Merge translations with app taking precedence
const translations = mergeTranslations(appTranslations, vendorTranslations);
// Function to deeply merge translations with specified precedence
mergeTranslations(primaryTranslations, secondaryTranslations) {
const result = { ...secondaryTranslations };

for (const [key, value] of Object.entries(primaryTranslations)) {
if (value && typeof value === 'object' && !Array.isArray(value)) {
result[key] = this.mergeTranslations(value, result[key] || {});
} else {
if (!result.hasOwnProperty(key)) {
result[key] = value;
}
}
}

// Process JSON files
const jsonFiles = [
'element-identifiers.json',
'outcome-markers.json'
];
return result;
}

for (const fileName of jsonFiles) {
const sourcePath = path.resolve('base-tests/config', fileName);
const destPath = path.resolve('tests/config', fileName);
// Function to translate values in an object recursively
translateObject(obj, translations) {
if (typeof obj === 'string') {
return translations[obj] ?? obj;
}

const content = JSON.parse(fs.readFileSync(sourcePath, 'utf-8'));
const translatedContent = translateObject(content, translations);
if (Array.isArray(obj)) {
return obj.map(item => this.translateObject(item, translations)).filter(item => item !== null);
}

// Ensure target directory exists
fs.mkdirSync(path.dirname(destPath), { recursive: true });
if (typeof obj === 'object' && obj !== null) {
const result = {};
for (const [key, value] of Object.entries(obj)) {
const translatedValue = this.translateObject(value, translations);
if (translatedValue !== null) {
result[key] = translatedValue;
}
}
return result;
}

fs.writeFileSync(destPath, JSON.stringify(translatedContent, null, 2));
console.log(`Translated file written: ${destPath}`);
return null;
}
}

console.log('Translation completed successfully!');
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}
const translateJson = new TranslateJson();
translateJson.main();