-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #287 from ibi-group/check-i18n-ymls
Check i18n ymls
- Loading branch information
Showing
8 changed files
with
841 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{ | ||
"name": "@opentripplanner/scripts", | ||
"version": "1.0.0", | ||
"description": "Utility scripts for the OTP-UI library", | ||
"main": "lib/index.js", | ||
"module": "esm/index.js", | ||
"types": "lib/index.d.js", | ||
"repository": "https://github.com/opentripplanner/otp-ui.git", | ||
"homepage": "https://github.com/opentripplanner/otp-ui#readme", | ||
"author": "Binh Dam", | ||
"license": "MIT", | ||
"private": false, | ||
"dependencies": { | ||
"@formatjs/cli": "^4.2.33", | ||
"flat": "^5.0.2", | ||
"glob": "^8.0.3", | ||
"glob-promise": "^4.2.2", | ||
"js-yaml": "^4.1.0" | ||
}, | ||
"scripts": { | ||
"collect-i18n": "node lib/collect-i18n-messages.js ../**/src ../**/i18n/en-US.yml > i18n.csv", | ||
"tsc": "tsc", | ||
"validate-i18n": "node lib/validate-i18n.js ../**/src ../**/i18n" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/opentripplanner/otp-ui/issues" | ||
}, | ||
"gitHead": "0af1b7cda60bd4252b219dcf893e01c2acb2ed5d" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
/* eslint-disable no-console */ | ||
/** | ||
* This script collects message ids gathered by the formatjs extract command in the specified files and folder(s) | ||
* and creates a CSV file with the id, description, and messages in the selected language(s). | ||
* This script is shipped as part of a package so it can be used in other code bases as needed. | ||
*/ | ||
// Example usage for all packages and all languages in this repo: | ||
// node path-to/lib/collect-i18n-messages.js ../**/src ../**/i18n | ||
// Example usage for all packages and one language in this repo: | ||
// node path-to-lib/collect-i18n-messages.js ../**/src ../**/i18n/en-US.yml | ||
|
||
import { extract } from "@formatjs/cli"; | ||
import flatten from "flat"; | ||
|
||
import { isNotSpecialId, loadYamlFile, sortSourceAndYmlFiles } from "./util"; | ||
|
||
// The data that corresponds to rows in the CSV output. | ||
type MessageData = Record< | ||
string, | ||
Record<string, string> & { | ||
description: string; | ||
} | ||
>; | ||
|
||
/** | ||
* Collect all messages and create a formatted output. | ||
*/ | ||
async function collectAndPrintOutMessages({ sourceFiles, ymlFilesByLocale }) { | ||
// Gather message ids from code. | ||
const messagesFromCode = JSON.parse(await extract(sourceFiles, {})); | ||
const messageIdsFromCode = Object.keys(messagesFromCode); | ||
const allLocales = Object.keys(ymlFilesByLocale); | ||
|
||
// CSV heading | ||
console.log(`ID,Description,${allLocales.join(",")}`); | ||
|
||
// Will contain id, description, and a column for each language. | ||
const messageData: MessageData = {}; | ||
|
||
// For each locale, check that all ids in messages are in the yml files. | ||
// Accessorily, log message ids from yml files that are not used in the code. | ||
await Promise.all( | ||
allLocales.map(async locale => { | ||
const allI18nPromises = ymlFilesByLocale[locale].map(loadYamlFile); | ||
const allI18nMessages = await Promise.all(allI18nPromises); | ||
let allI18nMessagesFlattened = {}; | ||
|
||
allI18nMessages.forEach(i18nMessages => { | ||
const flattenedMessages: Record<string, string> = flatten(i18nMessages); | ||
allI18nMessagesFlattened = { | ||
...allI18nMessagesFlattened, | ||
...flattenedMessages | ||
}; | ||
}); | ||
|
||
messageIdsFromCode.filter(isNotSpecialId).forEach(id => { | ||
const { description } = messagesFromCode[id]; | ||
const message = allI18nMessagesFlattened[id]?.trim() || undefined; | ||
|
||
if (!messageData[id]) { | ||
messageData[id] = { | ||
description | ||
}; | ||
} | ||
messageData[id][locale] = message; | ||
}); | ||
}) | ||
); | ||
|
||
Object.keys(messageData).forEach(id => { | ||
const row = messageData[id]; | ||
const messages = allLocales.map(locale => row[locale]); | ||
console.log(`${id},"${row.description}","${messages}"`); | ||
}); | ||
} | ||
|
||
sortSourceAndYmlFiles(process.argv).then(collectAndPrintOutMessages); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import { promises as fs } from "fs"; | ||
import { load } from "js-yaml"; | ||
import glob from "glob-promise"; | ||
import path from "path"; | ||
|
||
export interface SourceFilesAndYmlFilesByLocale { | ||
sourceFiles: string[]; | ||
ymlFilesByLocale: Record<string, string>; | ||
} | ||
|
||
function shouldProcessFile(fileName: string): boolean { | ||
return ( | ||
!fileName.includes("/__") && | ||
!fileName.includes("node_modules") && | ||
!fileName.endsWith(".d.ts") | ||
); | ||
} | ||
|
||
/** | ||
* @returns true if the id is not special or reserved (i.e. doesn't start with "_"). | ||
*/ | ||
export function isNotSpecialId(id: string): boolean { | ||
return !id.startsWith("_"); | ||
} | ||
|
||
/** | ||
* Helper function that sorts yml and source files into two buckets. | ||
* @param argv The value from process.argv. | ||
* @returns A composite object with a list for yml files by locale, and a list for source files. | ||
*/ | ||
export async function sortSourceAndYmlFiles(argv: string[]) { | ||
const sourceFiles = []; | ||
const ymlFilesByLocale = {}; | ||
|
||
// Places the give file into the source or yml file bucket above. | ||
function sortFile(fileName: string): void { | ||
const parsedArg = path.parse(fileName); | ||
if (parsedArg.ext === ".yml") { | ||
const locale = parsedArg.name; | ||
if (!ymlFilesByLocale[locale]) { | ||
ymlFilesByLocale[locale] = []; | ||
} | ||
const ymlFilesForLocale = ymlFilesByLocale[locale]; | ||
if (!ymlFilesForLocale.includes(fileName)) { | ||
ymlFilesForLocale.push(fileName); | ||
} | ||
ymlFilesByLocale[locale].push(fileName); | ||
} else if (!sourceFiles.includes(fileName)) { | ||
sourceFiles.push(fileName); | ||
} | ||
} | ||
|
||
// Note: reminder that node.js provides the first two argv values: | ||
// - argv[0] is the name of the executable file. | ||
// - argv[1] is the path to the script file. | ||
// - argv[2] and beyond are the folders passed to the script. | ||
const allGlobPromises = []; | ||
for (let i = 2; i < argv.length; i++) { | ||
// List the files recursively (glob) for this folder. | ||
const arg = argv[i]; | ||
|
||
// If argument ends with .yml, treat as a file. | ||
if (arg.endsWith(".yml")) { | ||
sortFile(arg); | ||
} else { | ||
// Otherwise, it is a folder, and use glob to get files recursively. | ||
// For glob argument info, see their docs at https://github.com/ahmadnassri/node-glob-promise#api. | ||
allGlobPromises.push(glob(`${arg}/**/*.{{j,t}s{,x},yml}`)); | ||
} | ||
} | ||
|
||
const allFileLists = await Promise.all(allGlobPromises); | ||
allFileLists.forEach(files => | ||
files.filter(shouldProcessFile).forEach(sortFile) | ||
); | ||
|
||
return { | ||
sourceFiles, | ||
ymlFilesByLocale | ||
}; | ||
} | ||
|
||
/** | ||
* Load yaml from a file into a js object | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export async function loadYamlFile(filename: string): Promise<any> { | ||
return load(await fs.readFile(filename)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
/* eslint-disable no-console */ | ||
/** | ||
* This script checks that message ids gathered by the formatjs extract command | ||
* are present in the specified folder(s). | ||
* It will produce an error code if message ids are present in a language but not another, | ||
* or if message ids are in a i18n yml files but not in the code or vice-versa. | ||
* This script is shipped as part of a package so it can be used in other code bases as needed. | ||
*/ | ||
// Example usage for one package in this repo: | ||
// node path-to/lib/validate-i18n.js ../trip-details/src ../trip-details/i18n | ||
// Example usage for all packages in this repo: | ||
// node path-to/lib/validate-i18n.js ../**/src ../**/i18n | ||
|
||
const { extract } = require("@formatjs/cli"); | ||
const flatten = require("flat"); | ||
|
||
const { | ||
isNotSpecialId, | ||
loadYamlFile, | ||
sortSourceAndYmlFiles | ||
} = require("./util"); | ||
|
||
/** | ||
* Checks message ids completeness between code and yml files for all locales in repo. | ||
*/ | ||
async function checkI18n({ sourceFiles, ymlFilesByLocale }) { | ||
// Gather message ids from code. | ||
const messagesFromCode = JSON.parse(await extract(sourceFiles, {})); | ||
const messageIdsFromCode = Object.keys(messagesFromCode); | ||
console.log( | ||
`Checking ${messageIdsFromCode.length} strings from ${ | ||
Object.keys(ymlFilesByLocale["en-US"]).length | ||
} message files against ${sourceFiles.length} source files.` | ||
); | ||
let errorCount = 0; | ||
|
||
// For each locale, check that all ids in messages are in the yml files. | ||
// Accessorily, log message ids from yml files that are not used in the code. | ||
await Promise.all( | ||
Object.keys(ymlFilesByLocale).map(async locale => { | ||
const idsChecked = []; | ||
const idsNotInCode = []; | ||
|
||
const allI18nPromises = ymlFilesByLocale[locale].map(loadYamlFile); | ||
const allI18nMessages = await Promise.all(allI18nPromises); | ||
|
||
allI18nMessages.forEach(i18nMessages => { | ||
const flattenedMessages = flatten(i18nMessages); | ||
|
||
// Message ids from code must be present in yml. | ||
messageIdsFromCode | ||
.filter(id => flattenedMessages[id]) | ||
.forEach(id => idsChecked.push(id)); | ||
|
||
// Message ids from yml (except those starting with "_") must be present in code. | ||
Object.keys(flattenedMessages) | ||
.filter(isNotSpecialId) | ||
.filter(id => !messageIdsFromCode.includes(id)) | ||
.forEach(id => idsNotInCode.push(id)); | ||
}); | ||
|
||
// Collect ids in code not found in yml. | ||
const missingIdsForLocale = messageIdsFromCode.filter( | ||
id => !idsChecked.includes(id) | ||
); | ||
|
||
// Print errors. | ||
missingIdsForLocale.forEach(id => { | ||
console.error(`Message '${id}' is missing from locale ${locale}.`); | ||
}); | ||
idsNotInCode.forEach(id => { | ||
console.error( | ||
`Message '${id}' from locale ${locale} is not used in code.` | ||
); | ||
}); | ||
errorCount += missingIdsForLocale.length + idsNotInCode.length; | ||
}) | ||
); | ||
|
||
console.log(`There were ${errorCount} error(s).`); | ||
if (errorCount > 0) { | ||
process.exit(1); | ||
} | ||
} | ||
|
||
sortSourceAndYmlFiles(process.argv).then(checkI18n); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"compilerOptions": { | ||
"outDir": "./lib" | ||
}, | ||
"include": ["src/**/*"] | ||
} |
Oops, something went wrong.