mirror of
https://github.com/langgenius/dify.git
synced 2026-02-09 23:20:12 -05:00
chore(web): add ESLint rules for i18n JSON validation (#30491)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import noAsAnyInT from './rules/no-as-any-in-t.js'
|
||||
import noExtraKeys from './rules/no-extra-keys.js'
|
||||
import noLegacyNamespacePrefix from './rules/no-legacy-namespace-prefix.js'
|
||||
import requireNsOption from './rules/require-ns-option.js'
|
||||
import validI18nKeys from './rules/valid-i18n-keys.js'
|
||||
|
||||
/** @type {import('eslint').ESLint.Plugin} */
|
||||
const plugin = {
|
||||
@@ -10,8 +12,10 @@ const plugin = {
|
||||
},
|
||||
rules: {
|
||||
'no-as-any-in-t': noAsAnyInT,
|
||||
'no-extra-keys': noExtraKeys,
|
||||
'no-legacy-namespace-prefix': noLegacyNamespacePrefix,
|
||||
'require-ns-option': requireNsOption,
|
||||
'valid-i18n-keys': validI18nKeys,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
70
web/eslint-rules/rules/no-extra-keys.js
Normal file
70
web/eslint-rules/rules/no-extra-keys.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import fs from 'node:fs'
|
||||
import path, { normalize, sep } from 'node:path'
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
export default {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Ensure non-English JSON files don\'t have extra keys not present in en-US',
|
||||
},
|
||||
fixable: 'code',
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
Program(node) {
|
||||
const { filename, sourceCode } = context
|
||||
|
||||
if (!filename.endsWith('.json'))
|
||||
return
|
||||
|
||||
const parts = normalize(filename).split(sep)
|
||||
// e.g., i18n/ar-TN/common.json -> jsonFile = common.json, lang = ar-TN
|
||||
const jsonFile = parts.at(-1)
|
||||
const lang = parts.at(-2)
|
||||
|
||||
// Skip English files
|
||||
if (lang === 'en-US')
|
||||
return
|
||||
|
||||
let currentJson = {}
|
||||
let englishJson = {}
|
||||
|
||||
try {
|
||||
currentJson = JSON.parse(sourceCode.text)
|
||||
// Look for the same filename in en-US folder
|
||||
// e.g., i18n/ar-TN/common.json -> i18n/en-US/common.json
|
||||
const englishFilePath = path.join(path.dirname(filename), '..', 'en-US', jsonFile ?? '')
|
||||
englishJson = JSON.parse(fs.readFileSync(englishFilePath, 'utf8'))
|
||||
}
|
||||
catch (error) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Error parsing JSON: ${error instanceof Error ? error.message : String(error)}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const extraKeys = Object.keys(currentJson).filter(
|
||||
key => !Object.prototype.hasOwnProperty.call(englishJson, key),
|
||||
)
|
||||
|
||||
for (const key of extraKeys) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Key "${key}" is present in ${lang}/${jsonFile} but not in en-US/${jsonFile}`,
|
||||
fix(fixer) {
|
||||
const newJson = Object.fromEntries(
|
||||
Object.entries(currentJson).filter(([k]) => !extraKeys.includes(k)),
|
||||
)
|
||||
|
||||
const newText = `${JSON.stringify(newJson, null, 2)}\n`
|
||||
|
||||
return fixer.replaceText(node, newText)
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
61
web/eslint-rules/rules/valid-i18n-keys.js
Normal file
61
web/eslint-rules/rules/valid-i18n-keys.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { cleanJsonText } from '../utils.js'
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
export default {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Ensure i18n JSON keys are flat and valid as object paths',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
Program(node) {
|
||||
const { filename, sourceCode } = context
|
||||
|
||||
if (!filename.endsWith('.json'))
|
||||
return
|
||||
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(cleanJsonText(sourceCode.text))
|
||||
}
|
||||
catch {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Invalid JSON format',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const keys = Object.keys(json)
|
||||
const keyPrefixes = new Set()
|
||||
|
||||
for (const key of keys) {
|
||||
if (key.includes('.')) {
|
||||
const parts = key.split('.')
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const prefix = parts.slice(0, i).join('.')
|
||||
if (keys.includes(prefix)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Invalid key structure: '${key}' conflicts with '${prefix}'`,
|
||||
})
|
||||
}
|
||||
keyPrefixes.add(prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
if (keyPrefixes.has(key)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Invalid key structure: '${key}' is a prefix of another key`,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
10
web/eslint-rules/utils.js
Normal file
10
web/eslint-rules/utils.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export const cleanJsonText = (text) => {
|
||||
const cleaned = text.replaceAll(/,\s*\}/g, '}')
|
||||
try {
|
||||
JSON.parse(cleaned)
|
||||
return cleaned
|
||||
}
|
||||
catch {
|
||||
return text
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user