Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: STRF-10101 Check translation rows compilation on stencil bundle/start #1145

Merged
merged 1 commit into from
Nov 7, 2023
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
7 changes: 7 additions & 0 deletions lib/bundle-validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const privateThemeConfigValidationSchema = require('./schemas/privateThemeConfig
const themeConfigValidationSchema = require('./schemas/themeConfig.json');
const themeValidationSchema = require('./schemas/themeSchema.json');
const ScssValidator = require('./ScssValidator');
const LangHelpersValidator = require('./lang/validator');

const VALID_IMAGE_TYPES = ['.jpg', '.jpeg', '.png', '.gif'];
const WIDTH_COMPOSED = 600;
Expand Down Expand Up @@ -42,6 +43,7 @@ class BundleValidator {
this.objectsToValidate = ['head.scripts', 'footer.scripts'];
this.jsonSchemaValidatorOptions = { schemaId: 'auto', allErrors: true };
this.scssValidator = new ScssValidator(themePath, themeConfig);
this.langHelpersValidator = new LangHelpersValidator(themePath, themeConfig);

// Array of tasks used in async.series
this.validationTasks = [
Expand All @@ -50,6 +52,7 @@ class BundleValidator {
this._validateSchemaTranslations.bind(this),
this._validateTemplatesFrontmatter.bind(this),
this._validateCssFiles.bind(this),
this._validateLangFiles.bind(this),
];

if (!this.isPrivate) {
Expand Down Expand Up @@ -405,6 +408,10 @@ class BundleValidator {
return true;
}

async _validateLangFiles() {
await this.langHelpersValidator.run();
}

async _validateCssFiles() {
await this.scssValidator.run();
}
Expand Down
2 changes: 1 addition & 1 deletion lib/bundle-validator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe('BundleValidator', () => {

const res = await promisify(validator.validateTheme.bind(validator))();

expect(res).toHaveLength(6); // 6 validation tasks
expect(res).toHaveLength(7); // 7 validation tasks
expect(res).not.toContain(false);
});

Expand Down
113 changes: 113 additions & 0 deletions lib/lang/validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
require('colors');
const fs = require('fs');
const path = require('path');
const { recursiveReadDir } = require('../utils/fsUtils');

const LANG_HELPER_REGEXP = /{{\s*lang\s*(?:'|")((?:\w*(?:-\w*)*(\.\w*(?:-\w*)*)*)+)/gim;

class LangpathsValidator {
/**
*
* @param {String} themePath
*/
constructor(themePath) {
this.themePath = themePath;
}

async run(defaultLang = null) {
const templatesPath = path.join(this.themePath, 'templates');
const paths = await this.getLangHelpersPaths(templatesPath);
const dedupePaths = [...new Set(paths)];
const langFiles = await this.getLangFilesContent(defaultLang);
const errors = this.validate(dedupePaths, langFiles);
this.printErrors(errors);
return errors;
}

printErrors(errors) {
if (errors.length > 0) {
console.log(
'Warning: Your theme has some missing translations used in the theme:'.yellow,
);
console.log(errors.join('\n').yellow);
}
}

searchLangPaths(fileContent, langPath) {
const keys = langPath.split('.');
let value = fileContent;

for (const key of keys) {
// eslint-disable-next-line no-prototype-builtins
if (value && value.hasOwnProperty(key)) {
value = value[key];
} else {
return false;
}
}

return value;
}

validate(paths, langFiles) {
const errors = [
...this.checkLangFiles(langFiles),
...this.checkForMissingTranslations(paths, langFiles),
];
return errors;
}

checkForMissingTranslations(paths, langFiles) {
const errors = [];
for (const langPath of paths) {
// eslint-disable-next-line no-restricted-syntax,guard-for-in
for (const langFile in langFiles) {
const translation = this.searchLangPaths(langFiles[langFile], langPath);
if (!translation) {
errors.push(`Missing translation for ${langPath} in ${langFile}`);
}
}
}
return errors;
}

checkLangFiles(files) {
if (files.length === 0) {
return ['No lang files found in your theme'];
}
return [];
}

async getLangHelpersPaths(templatesPath) {
const files = await recursiveReadDir(templatesPath);
const paths = [];
for await (const file of files) {
const content = await fs.promises.readFile(file, { encoding: 'utf-8' });
const result = content.matchAll(LANG_HELPER_REGEXP);
const arr = [...result];
if (arr.length > 0) {
const langPath = arr[0][1];
paths.push(langPath);
}
}
return paths;
}

async getLangFilesContent(defaultLang = null) {
const filesContent = {};
const langPath = path.join(this.themePath, 'lang');
let files = await recursiveReadDir(langPath);

if (defaultLang) {
files = files.filter((file) => file.includes(defaultLang));
}

for await (const file of files) {
const content = await fs.promises.readFile(file, { encoding: 'utf-8' });
filesContent[file] = JSON.parse(content);
}
return filesContent;
}
}

module.exports = LangpathsValidator;
37 changes: 37 additions & 0 deletions lib/lang/validator.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const path = require('path');

const LangFilesValidator = require('./validator');

describe('lang/validator.js tests', () => {
afterEach(() => {
jest.restoreAllMocks();
});

describe('valid', () => {
it('run with no errors', async () => {
const themePath = path.join(process.cwd(), 'test/_mocks/themes/valid');
const validator = new LangFilesValidator(themePath);
const errors = await validator.run();

expect(errors).toHaveLength(0);
});

it('run with no errors providing default lang', async () => {
const themePath = path.join(process.cwd(), 'test/_mocks/themes/valid');
const validator = new LangFilesValidator(themePath);
const errors = await validator.run('en');

expect(errors).toHaveLength(0);
});
});

describe('not valid', () => {
it('run with lang helper that is not presented in lang file', async () => {
const themePath = path.join(process.cwd(), 'test/_mocks/themes/invalid-translations');
const validator = new LangFilesValidator(themePath);
const errors = await validator.run();

expect(errors).toHaveLength(1);
});
});
});
4 changes: 4 additions & 0 deletions lib/stencil-start.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const cliCommonModule = require('./cliCommon');
const themeApiClientModule = require('./theme-api-client');
const storeSettingsApiClientModule = require('./store-settings-api-client');
const LangHelper = require('./lang-helper');
const LangValidator = require('./lang/validator');

class StencilStart {
constructor({
Expand All @@ -32,6 +33,7 @@ class StencilStart {
CyclesDetector = Cycles,
stencilPushUtils = stencilPushUtilsModule,
logger = console,
langValidator = new LangValidator(THEME_PATH),
} = {}) {
this._browserSync = browserSync;
this._themeApiClient = themeApiClient;
Expand All @@ -46,6 +48,7 @@ class StencilStart {
this._CyclesDetector = CyclesDetector;
this._stencilPushUtils = stencilPushUtils;
this._logger = logger;
this._langValidator = langValidator;
}

async run(cliOptions) {
Expand Down Expand Up @@ -289,6 +292,7 @@ class StencilStart {
} else {
try {
await this._langHelper.checkLangKeysPresence(filesPaths, defaultShopperLanguage);
await this._langValidator.run(defaultShopperLanguage);
} catch (e) {
this._logger.error(e);
}
Expand Down
20 changes: 20 additions & 0 deletions test/_mocks/themes/invalid-translations/lang/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"header": {
"welcome_back": "Welcome back, {name}"
},
"footer": {
"brands": "Popular Brands",
"navigate": "Navigate",
"info": "Info",
"categories": "Categories",
"call_us": "Call us at {phone_number}"
},
"home": {
"heading": "Home"
},
"blog": {
"recent_posts": "Recent Posts",
"label": "Blog",
"posted_by": "Posted by {name}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
a
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
b


<!-- test
{{{stylesheet '/assets/custom/css/test.css'}}} -->

more text here
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>page.html</title>
</head>
<body>
{{ lang 'failed' }}
</body>
</html>
12 changes: 12 additions & 0 deletions test/_mocks/themes/invalid-translations/templates/pages/page2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>page2.html</title>
{{head.scripts}}
</head>
<body>
<h1>{{theme_settings.customizable_title}}</h1>
{{> components/b}}
{{footer.scripts}}
</body>
</html>
Loading