Browse Source
* detecting unsupported modifications in locales * typo fix * limit to english locales, increased verbosity * increased verbositypull/11325/head
6 changed files with 156 additions and 2 deletions
@ -0,0 +1,34 @@ |
|||||||
|
--- |
||||||
|
name: Locales lint for Crowdin |
||||||
|
|
||||||
|
on: |
||||||
|
pull_request: |
||||||
|
branches-ignore: |
||||||
|
- 'l10n_master' |
||||||
|
- 'cf-pages' |
||||||
|
paths: |
||||||
|
- '**/messages.json' |
||||||
|
|
||||||
|
jobs: |
||||||
|
lint: |
||||||
|
name: Lint |
||||||
|
runs-on: ubuntu-22.04 |
||||||
|
steps: |
||||||
|
- name: Checkout repo |
||||||
|
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 |
||||||
|
- name: Checkout base branch repo |
||||||
|
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 |
||||||
|
with: |
||||||
|
ref: ${{ github.event.pull_request.base.sha }} |
||||||
|
path: base |
||||||
|
- name: Install dependencies |
||||||
|
run: npm ci |
||||||
|
- name: Compare |
||||||
|
run: | |
||||||
|
npm run test:locales |
||||||
|
if [ $? -eq 0 ]; then |
||||||
|
echo "Lint check successful." |
||||||
|
else |
||||||
|
echo "Lint check failed." |
||||||
|
exit 1 |
||||||
|
fi |
||||||
@ -0,0 +1,105 @@ |
|||||||
|
/* eslint no-console:0 */ |
||||||
|
import fs from "fs"; |
||||||
|
import path from "path"; |
||||||
|
|
||||||
|
type Messages = { |
||||||
|
[id: string]: { |
||||||
|
message: string; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
function findLocaleFiles(dir: string): string[] { |
||||||
|
return fs |
||||||
|
.readdirSync(dir, { encoding: null, recursive: true }) |
||||||
|
.filter((file) => path.basename(file) === "messages.json") |
||||||
|
.filter((file) => path.dirname(file).endsWith("en")) |
||||||
|
.map((file) => path.join(dir, file)); |
||||||
|
} |
||||||
|
|
||||||
|
function findAllLocaleFiles(rootDir: string): string[] { |
||||||
|
return [ |
||||||
|
...findLocaleFiles(path.join(rootDir, "apps", "browser", "src")), |
||||||
|
...findLocaleFiles(path.join(rootDir, "apps", "cli", "src")), |
||||||
|
...findLocaleFiles(path.join(rootDir, "apps", "desktop", "src")), |
||||||
|
...findLocaleFiles(path.join(rootDir, "apps", "web", "src")), |
||||||
|
].map((file) => path.relative(rootDir, file)); |
||||||
|
} |
||||||
|
|
||||||
|
function readMessagesJson(file: string): Messages { |
||||||
|
let content = fs.readFileSync(file, { encoding: "utf-8" }); |
||||||
|
// Strip BOM
|
||||||
|
content = content.replace(/^\uFEFF/, ""); |
||||||
|
try { |
||||||
|
return JSON.parse(content); |
||||||
|
} catch (e: unknown) { |
||||||
|
console.error(`ERROR: Invalid JSON file ${file}`, e); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function compareMessagesJson(beforeFile: string, afterFile: string): boolean { |
||||||
|
try { |
||||||
|
console.log("Comparing locale files:", beforeFile, afterFile); |
||||||
|
|
||||||
|
const messagesBeforeJson = readMessagesJson(beforeFile); |
||||||
|
const messagesAfterJson = readMessagesJson(afterFile); |
||||||
|
|
||||||
|
const messagesIdMapBefore = toMessageIdMap(messagesBeforeJson); |
||||||
|
const messagesIdMapAfter = toMessageIdMap(messagesAfterJson); |
||||||
|
|
||||||
|
let changed = false; |
||||||
|
|
||||||
|
for (const [id, message] of messagesIdMapAfter.entries()) { |
||||||
|
if (!messagesIdMapBefore.has(id)) { |
||||||
|
console.log("New message:", id); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (messagesIdMapBefore.get(id) !== message) { |
||||||
|
console.error("ERROR: Message changed:", id); |
||||||
|
changed = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return changed; |
||||||
|
} catch (e: unknown) { |
||||||
|
console.error(`ERROR: Unable to compare files ${beforeFile} and ${afterFile}`, e); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function toMessageIdMap(messagesJson: Messages): Map<string, string> { |
||||||
|
return Object.entries(messagesJson).reduce((map, [id, value]) => { |
||||||
|
map.set(id, value.message); |
||||||
|
return map; |
||||||
|
}, new Map<string, string>()); |
||||||
|
} |
||||||
|
|
||||||
|
const rootDir = path.join(__dirname, "..", ".."); |
||||||
|
const baseBranchRootDir = path.join(rootDir, "base"); |
||||||
|
|
||||||
|
const files = findAllLocaleFiles(rootDir); |
||||||
|
|
||||||
|
console.log("Detected valid English locale files:", files); |
||||||
|
|
||||||
|
let changedFiles = false; |
||||||
|
|
||||||
|
for (const file of files) { |
||||||
|
const baseBranchFile = path.join(baseBranchRootDir, file); |
||||||
|
if (!fs.existsSync(baseBranchFile)) { |
||||||
|
console.error("ERROR: File not found in base branch:", file); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
const changed = compareMessagesJson(baseBranchFile, path.join(rootDir, file)); |
||||||
|
changedFiles ||= changed; |
||||||
|
} |
||||||
|
|
||||||
|
if (changedFiles) { |
||||||
|
console.error( |
||||||
|
"ERROR: Incompatible Crowdin locale files. " + |
||||||
|
"All messages in messages.json locale files needs to be immutable and cannot be updated. " + |
||||||
|
"If a message needs to be changed, create a new message id and update your code to use it instead.", |
||||||
|
); |
||||||
|
process.exit(1); |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
{ |
||||||
|
"extends": "../libs/shared/tsconfig", |
||||||
|
"compilerOptions": { |
||||||
|
"outDir": "dist", |
||||||
|
"module": "NodeNext", |
||||||
|
"target": "ESNext" |
||||||
|
}, |
||||||
|
"include": ["*.ts"] |
||||||
|
} |
||||||
Loading…
Reference in new issue