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

Detect localisation sync issues for password requirements #3606

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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 .bacon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,10 @@ test_suites:
script_name: vrt-v3-get-screenshots
criteria: OPTIONAL
queue_name: small
- name: verify-translations
script_path: /root/okta/okta-signin-widget/scripts
sort_order: '34'
timeout: '15'
script_name: verify-translations
criteria: MERGE
queue_name: small
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"clean:esm": "rimraf target/esm",
"codegen": "./scripts/codegen.sh",
"find-internal-packages": "node scripts/find-internal-packages",
"verify-translations": "node scripts/buildtools verify-translations",
"generate-phone-codes": "node scripts/buildtools generate-phone-codes",
"generate-config": "node scripts/buildtools generate-language-config",
"weekly-release-update": "./scripts/weekly-release-update.sh",
Expand Down
11 changes: 11 additions & 0 deletions packages/@okta/verify-translations/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "verify-translations",
"version": "1.0.0",
"description": "Sync translations from core to SIW",
"scripts": {
"verify": "node verify-translations.js"
},
"devDependencies": {
"properties": "^1.2.1"
}
}
211 changes: 211 additions & 0 deletions packages/@okta/verify-translations/verify-translations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
const { resolve } = require('path');
const fs = require('fs');
const properties = require('properties');

const { CI, I18N_REPO_PATH, WRITE_FIXED_I18N, USE_I18N_REPO_ONLY } = process.env;
const ROOT_DIR = resolve(__dirname, '../../../');
const OKTA_I18N_PROPERTIES = `${ROOT_DIR}/packages/@okta/i18n/src/properties`;
const I18N_REPO = I18N_REPO_PATH ? I18N_REPO_PATH : resolve(ROOT_DIR, '../i18n');
const CORE_REPO = resolve(`${process.env.HOME}/okta/okta-core`);


const parseProperties = async ({ lang, resourcePath, bundle }) => {
const langPostfix = lang === '' ? '' : '_'+lang;
const propertiesPath = `${resourcePath}/${bundle}${langPostfix}.properties`;
const propertiesExists = fs.existsSync(propertiesPath);
if (!propertiesExists) {
return null;
}
return new Promise((resolve, reject) => {
properties.parse(propertiesPath, {
path: true,
}, (error, res) => {
if (error) {
reject(`Unable to parse properties file ${propertiesPath}`);
} else {
resolve(res);
}
});
});
};

const updateProperties = ({ lang, resourcePath, bundle, updates }) => {
const langPostfix = lang === '' ? '' : '_'+lang;
const propertiesPath = `${resourcePath}/${bundle}${langPostfix}.properties`;
const propertiesData = fs.readFileSync(propertiesPath, 'utf8');
const propertiesLines = propertiesData.split('\n');
for (let i = 0 ; i < propertiesLines.length ; i++) {
const line = propertiesLines[i];
const lineTrimmed = line.trim();
const isDataLine = lineTrimmed.length && !lineTrimmed.startsWith('#');
if (isDataLine) {
const k = lineTrimmed.substring(0, lineTrimmed.indexOf('=')).trim();
const v = updates[k]?.to;
if (k && v) {
const newLine = properties.stringify({ [k]: v }, {
unicode: true
});
propertiesLines[i] = newLine;
}
}
}
const newPropertiesData = propertiesLines.join('\n');
fs.writeFileSync(propertiesPath, newPropertiesData);
};

const getCoreResourcesPath = () => {
// Get `messages-translated` resources from `i18n` or `okta-core` repo
if (fs.existsSync(I18N_REPO) && fs.existsSync(`${I18N_REPO}/packages/messages-translated`)) {
return `${I18N_REPO}/packages/messages-translated`;
}
if (fs.existsSync(CORE_REPO) && fs.existsSync(`${CORE_REPO}/resources/src/main/resources`)) {
return `${CORE_REPO}/resources/src/main/resources`;
}
throw new Error(`No i18n repo found at ${I18N_REPO}`);
};

const getSiwResourcesPath = () => {
if (USE_I18N_REPO_ONLY === 'true') {
return `${I18N_REPO}/packages/login`;
}
return OKTA_I18N_PROPERTIES;
};

const getLanguges = ({ resourcePath, bundle }) => {
const fileNames = fs.readdirSync(resourcePath).filter(fileName =>
fileName.startsWith(bundle) && fileName.endsWith('.properties')
);
const langs = fileNames.map(fileName => {
const langPostfix = fileName.split('.')[0].substring(bundle.length);
return langPostfix.startsWith('_') ? langPostfix.substring(1) : langPostfix;
});
const skipLangs = [ 'ok_PL', 'ok_SK', 'in' ];
return langs.filter(lang => !skipLangs.includes(lang));
};

const getCorelang = (siwLang, coreLangs) => coreLangs.find(coreLang => (
coreLang === siwLang
|| coreLang === siwLang.replace('_', '-')
|| coreLang.split('-')[0] === siwLang
));

const buildCompexityKeysMapping = (siwTranslations, coreTranslations) => {
const siwPrefix = 'password.complexity.';
const corePrefixes = [
siwPrefix,
'password_policy.',
'password_policy.new.text.',
'password_policy.complexity.',
'password_policy.description.',
];
const mappingOverride = {
'password.complexity.history': 'password_policy.new.text.history',
};

const siwKeys = Object.keys(siwTranslations).filter(k =>
k.startsWith(siwPrefix)
&& !k.endsWith('.description') && !k.endsWith('.header')
);
const coreKeys = Object.keys(coreTranslations).filter(k =>
corePrefixes.find(prefix => k.startsWith(prefix))
&& !k.endsWith('.html')
);

const mapping = {};
for (const siwKey of siwKeys) {
const baseKey = siwKey.substring(siwPrefix.length);
const coreKey = coreKeys.find(coreKey => (
corePrefixes.find(prefix => coreKey === (prefix + baseKey))
));
mapping[siwKey] = mappingOverride[siwKey] || coreKey;
}

const unmappedKeys = siwKeys.filter(k => !mapping[k]);
if (unmappedKeys.length) {
console.warn(`No mapping found for keys: ${unmappedKeys.join(', ')}`);
}

return mapping;
};

const verifyTranslations = async ({ canUpdate }) => {
let res = 0;
const coreResourcesPath = getCoreResourcesPath();
const siwResourcesPath = getSiwResourcesPath();
console.log(`Using core resource path: ${coreResourcesPath}`);
console.log(`Using widget resource path: ${siwResourcesPath}`);
const siwLangs = getLanguges({
resourcePath: siwResourcesPath,
bundle: 'login',
});
const coreLangs = getLanguges({
resourcePath: coreResourcesPath,
bundle: 'messages-translated',
});
for (let siwLang of siwLangs) {
const siwProperties = await parseProperties({
resourcePath: siwResourcesPath,
bundle: 'login',
lang: siwLang,
});
if (!siwProperties) {
console.error(`Missing SIW properties file for lang ${siwLang || 'default'}`);
res = 2;
}

const coreLang = getCorelang(siwLang, coreLangs);
const coreProperties = await parseProperties({
resourcePath: coreResourcesPath,
bundle: 'messages-translated',
lang: coreLang,
});
if (!coreProperties) {
console.error(`Missing core properties file for lang ${siwLang || 'default'}`);
res = 2;
}

if (siwProperties && coreProperties) {
const mapping = buildCompexityKeysMapping(siwProperties, coreProperties);
const updates = {};
for (let k in mapping) {
const siwTranslation = siwProperties[k];
const coreTranslation = coreProperties[mapping[k]];
if (siwTranslation && coreTranslation && siwTranslation !== coreTranslation) {
updates[k] = {
from: siwTranslation,
to: coreTranslation,
};
}
}
if (Object.keys(updates).length) {
console.log(`Need to update ${Object.keys(updates).length} translations for lang ${siwLang || 'default'}`);
console.log(updates);
if (canUpdate) {
updateProperties({
resourcePath: siwResourcesPath,
bundle: 'login',
lang: siwLang,
updates,
});
} else {
res = 1;
}
}
}
}

if (res === 1 && !canUpdate) {
console.log("Please run `yarn verify-translations --write` and commit changes.");
}

return res;
};

const start = async () => {
const res = await verifyTranslations({
canUpdate: !CI && WRITE_FIXED_I18N === 'true' && USE_I18N_REPO_ONLY !== 'true'
});
process.exit(res);
};

start();
8 changes: 8 additions & 0 deletions packages/@okta/verify-translations/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


properties@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/properties/-/properties-1.2.1.tgz#0ee97a7fc020b1a2a55b8659eda4aa8d869094bd"
integrity sha512-qYNxyMj1JeW54i/EWEFsM1cVwxJbtgPp8+0Wg9XjNaK6VE/c4oRi6PNu5p7w1mNXEIQIjV5Wwn8v8Gz82/QzdQ==
49 changes: 49 additions & 0 deletions scripts/buildtools/commands/verify-translations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const { resolve } = require('path');
const { execSync } = require('child_process');

exports.command = 'verify-translations';
exports.describe = 'Internal: Compare core and SIW translations that needs to be in sync';

exports.builder = {
i18nRepoPath: {
description: 'Path to i18n repo',
type: 'string',
},
write: {
description: 'True to write fixed translations into packages/@okta/i18n/src/properties',
type: 'boolean',
default: false,
},
useI18nRepoOnly: {
description: 'True to compare core and SIW translations in i18n repo only, ignoring translations in SIW repo',
type: 'boolean',
default: false,
}
};

exports.handler = async (argv) => {
const packagePath = resolve(__dirname, '../../../packages/@okta/verify-translations');
const installCmd = 'yarn install --silent';

// Step 1: Install internal dependencies for the package
execSync(installCmd, {
cwd: packagePath,
stdio: 'inherit'
});

// Step 2: Run the verify command
let verifyCmd = 'yarn verify';
if (argv.i18nRepoPath) {
verifyCmd = `I18N_REPO_PATH="${argv.i18nRepoPath}" ${verifyCmd}`;
}
if (argv.useI18nRepoOnly) {
verifyCmd = `USE_I18N_REPO_ONLY=true ${verifyCmd}`;
}
if (argv.write) {
verifyCmd = `WRITE_FIXED_I18N=true ${verifyCmd}`;
}
execSync(verifyCmd, {
cwd: packagePath,
stdio: 'inherit'
});
};
18 changes: 18 additions & 0 deletions scripts/verify-translations.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash -v

source $OKTA_HOME/$REPO/scripts/setup.sh

setup_github_token atko-eng
clone_repo i18n atko-eng
pushd "${OKTA_HOME}/i18n"
git checkout master
popd

cd ${OKTA_HOME}/${REPO}

if ! yarn verify-translations; then
echo "Verify failed! Exiting..."
exit ${TEST_FAILURE}
fi

exit $SUCCESS