Skip to content

Commit

Permalink
Merge pull request #2726 from newrelic/campfire/translate-deletions
Browse files Browse the repository at this point in the history
Create check for out of date translated files
  • Loading branch information
zstix committed Jun 21, 2021
2 parents 81d7cff + c0f883d commit bca1b65
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/fetch-swiftype-content.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ jobs:
branch: 'main',
required_status_checks: {
strict: false,
contexts: ['build the docs site']
contexts: ['build the docs site', 'unpaired translations removed']
},
restrictions: {
users: [],
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update-whats-new-ids.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
branch: 'develop',
required_status_checks: {
strict: false,
contexts: ['run linter', 'run tests', 'license/cla']
contexts: ['run linter', 'run tests', 'license/cla', 'unpaired translations removed']
},
restrictions: {
users: [],
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/validate-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,31 @@ jobs:

- name: Run Eslint
run: yarn lint

unpaired-translations-removed:
name: unpaired translations removed
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 12

- name: Cache dependencies
id: yarn-cache
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/yarn.lock') }}

- name: Install dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile

- name: check for outdated localized files
run: |
URL="https://api.github.com/repos/${GITHUB_REPOSITORY}/pulls/${{ github.event.pull_request.number }}/files"
yarn check-for-outdated-translations $URL
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"jest-emotion": "^10.0.32",
"jsdom": "^16.5.0",
"lodash": "^4.17.21",
"parse-link-header": "^1.0.1",
"prop-types": "^15.7.2",
"react": "^17.0.0",
"react-dom": "^17.0.0",
Expand Down Expand Up @@ -144,7 +145,8 @@
"codemod": "node scripts/codemod.js",
"postinstall": "patch-package",
"extract-i18n": "i18next",
"get-translated-files": "node scripts/actions/add-files-to-translation-queue.js"
"get-translated-files": "node scripts/actions/add-files-to-translation-queue.js",
"check-for-outdated-translations": "node scripts/actions/check-for-outdated-translations.js"
},
"husky": {
"hooks": {
Expand Down
79 changes: 79 additions & 0 deletions scripts/actions/__tests__/check-for-outdated-translations.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const fs = require('fs');
const fetch = require('node-fetch');

const checkForOutdatedTranslations = require('../check-for-outdated-translations');

// Mock node-fetch so we can avoid calling out to GitHub
jest.mock('node-fetch');

global.process.exit = jest.fn();

// Helper function to mock a response from Github
const mockGithubResponse = (result) => {
fetch.mockResolvedValueOnce({
ok: true,
json: jest.fn(() => Promise.resolve(result)),
headers: { get: jest.fn() },
});
};

// Mock fs so we don't manipulate local files
jest.mock('fs');

// helper function to construct and mock the response from readFileSync
const mockReadFileSync = (translate = []) => {
const mdx = `---
title: A test file
${translate.length ? `translate:\n - ${translate.join('\n - ')}` : ''}
---
This is a test file
`;

fs.readFileSync.mockReturnValueOnce(mdx);
};

const mockExistsSync = (bool) => fs.existsSync.mockReturnValueOnce(bool);

const STATUS = {
ADDED: 'added',
MODIFIED: 'modified',
REMOVED: 'removed',
};

describe('Action: Check for outdated translations', () => {
afterEach(() => {
jest.resetAllMocks();
});

test('should succeed when no files are found for deletion', async () => {
const filename = '/content/bar.mdx';
mockGithubResponse([
{
filename,
status: STATUS.ADDED,
},
]);

mockReadFileSync(['jp']);
mockExistsSync(false);

await checkForOutdatedTranslations();

expect(global.process.exit).toHaveBeenLastCalledWith(0);
});

test('should fail when files are found for deletion', async () => {
const filename = '/content/bar.mdx';
mockGithubResponse([
{
filename,
status: STATUS.REMOVED,
},
]);

mockExistsSync(true);
await checkForOutdatedTranslations();

expect(global.process.exit).toHaveBeenLastCalledWith(1);
});
});
29 changes: 15 additions & 14 deletions scripts/actions/add-files-to-translation-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,23 @@ const getUpdatedQueue = async (url, queue) => {
const files = await resp.json();

const mdxFiles = files
? files
.filter((file) => path.extname(file.filename) === '.mdx')
.filter((file) => file.status !== 'removed')
.reduce((files, file) => {
const contents = fs.readFileSync(
path.join(process.cwd(), file.filename)
);
const { data } = frontmatter(contents);

return data.translate && data.translate.length
? [...files, { ...file, locales: data.translate }]
: files;
}, [])
? files.filter((file) => path.extname(file.filename) === '.mdx')
: [];

const addedMdxFiles = mdxFiles.reduce((files, file) => {
const mdxFilesToAdd = mdxFiles
.filter((file) => file.status !== 'removed')
.reduce((files, file) => {
const contents = fs.readFileSync(
path.join(process.cwd(), file.filename)
);
const { data } = frontmatter(contents);

return data.translate && data.translate.length
? [...files, { ...file, locales: data.translate }]
: files;
}, []);

const addedMdxFiles = mdxFilesToAdd.reduce((files, file) => {
return file.locales.reduce(
(acc, locale) => ({
...acc,
Expand Down
118 changes: 118 additions & 0 deletions scripts/actions/check-for-outdated-translations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const frontmatter = require('@github-docs/frontmatter');
const parseLinkHeader = require('parse-link-header');

const checkArgs = require('./utils/check-args');
const { prop } = require('../utils/functional');
const { ADDITIONAL_LOCALES } = require('../utils/constants');

const doI18nFilesExist = (fileName, locales) => {
const i18nPrefix = path.join(process.cwd(), 'src/i18n/content');
const baseFileName = fileName.replace('src/content/', '');

return locales
.map((locale) => {
const filePath = path.join(i18nPrefix, locale, baseFileName);
const fileExists = fs.existsSync(filePath);
return fileExists ? filePath : null;
})
.filter(Boolean);
};

const fetchFilesFromGH = async (url) => {
let files = [];
let nextPageLink = url;

while (nextPageLink) {
const resp = await fetch(nextPageLink, {
headers: { authorization: `token ${process.env.GITHUB_TOKEN}` },
});
if (!resp.ok) {
throw new Error(
`Github API returned status ${resp.code} - ${resp.message}`
);
}
const page = await resp.json();
nextPageLink = getNextLink(resp.headers.get('Link'));
files = [...files, ...page];
}

return files;
};

const getNextLink = (linkHeader) => {
const parsedLinkHeader = parseLinkHeader(linkHeader);
if (parsedLinkHeader && parsedLinkHeader.next) {
return parsedLinkHeader.next.url || null;
}
return null;
};

/**
* @param {string} url The API url that is used to fetch files.
*/
const checkOutdatedTranslations = async (url) => {
const files = await fetchFilesFromGH(url);
const mdxFiles = files
? files.filter((file) => path.extname(file.filename) === '.mdx')
: [];

const mdxFilesContent = mdxFiles
.filter((file) => file.status !== 'removed')
.reduce((files, file) => {
const contents = fs.readFileSync(path.join(process.cwd(), file.filename));
const { data } = frontmatter(contents);
return [...files, { path: file.filename, locales: data.translate || [] }];
}, []);

const removedMdxFileNames = mdxFiles
.filter((f) => f.status === 'removed')
.map(prop('filename'));

// if a locale was removed from the translate frontmatter, we want to remove the translated version of that file.

const modifiedFiles = mdxFilesContent.flatMap((file) => {
const unsetLocales = ADDITIONAL_LOCALES.filter(
(l) => !file.locales.includes(l)
);
return doI18nFilesExist(file.path, unsetLocales);
});

const removedFiles = removedMdxFileNames.flatMap((name) =>
doI18nFilesExist(name, ADDITIONAL_LOCALES)
);

const orphanedI18nFiles = [...modifiedFiles, ...removedFiles];

if (orphanedI18nFiles.length > 0) {
orphanedI18nFiles.forEach((f) =>
console.log(
`ACTION NEEDED: Unpaired translation found -> ${f.replace(
`${process.cwd()}/`,
''
)}`
)
);
throw new Error(
'Files without matching english counterparts were found, see logs for filenames'
);
}
};

/** Entrypoint. */
const main = async () => {
try {
checkArgs(3);
const url = process.argv[2];

await checkOutdatedTranslations(url);
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};

module.exports = main;
4 changes: 2 additions & 2 deletions scripts/utils/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const path = require('path');

module.exports = {
CONTENT_DIR: 'src/content',
NAV_DIR: 'src/nav',
Expand All @@ -11,6 +9,8 @@ module.exports = {
DATA_DIR: 'src/data',
JP_DIR: 'src/i18n/content/jp',

ADDITIONAL_LOCALES: ['jp'],

INSTRUCTIONS: {
ADD: 'ADD',
MOVE: 'MOVE',
Expand Down
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -13867,6 +13867,13 @@ parse-latin@^4.0.0:
unist-util-modify-children "^1.0.0"
unist-util-visit-children "^1.0.0"

parse-link-header@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parse-link-header/-/parse-link-header-1.0.1.tgz#bedfe0d2118aeb84be75e7b025419ec8a61140a7"
integrity sha1-vt/g0hGK64S+deewJUGeyKYRQKc=
dependencies:
xtend "~4.0.1"

parse-path@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-4.0.2.tgz#ef14f0d3d77bae8dd4bc66563a4c151aac9e65aa"
Expand Down

0 comments on commit bca1b65

Please sign in to comment.