Files
afilmory/plugins/eslint/eslint-check-i18n-json.js
Innei c775f82153 feat(i18n): integrate i18next for internationalization support
- Added i18next and react-i18next for multi-language support in the application.
- Created localization files for English, Japanese, Korean, Traditional Chinese, and Simplified Chinese.
- Implemented translation hooks in various components to replace hardcoded strings with translatable keys.
- Updated ESLint configuration to include new i18n JSON validation rules.
- Introduced a new event bus for handling i18n updates during development.

Signed-off-by: Innei <tukon479@gmail.com>
2025-06-12 17:56:11 +08:00

141 lines
3.9 KiB
JavaScript

// @ts-check
/** @type {import("eslint").ESLint.Plugin} */
import fs from 'node:fs'
import path from 'node:path'
import { cleanJsonText } from './utils.js'
export default {
rules: {
'valid-i18n-keys': {
meta: {
type: 'problem',
docs: {
description:
'Ensure i18n JSON keys are flat and valid as object paths',
category: 'Possible Errors',
recommended: true,
},
fixable: null,
},
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`,
})
}
}
},
}
},
},
'no-extra-keys': {
meta: {
type: 'problem',
docs: {
description:
"Ensure non-English JSON files don't have extra keys not present in en.json",
category: 'Possible Errors',
recommended: true,
},
fixable: 'code', // Set fixable to "code"
},
create(context) {
return {
Program(node) {
const { filename, sourceCode } = context
if (!filename.endsWith('.json')) return
const parts = filename.split(path.sep)
const lang = parts.at(-1).split('.')[0]
const namespace = parts.at(-2)
if (lang === 'en') return
let currentJson = {}
let englishJson = {}
try {
currentJson = JSON.parse(sourceCode.text)
const englishFilePath = path.join(
path.dirname(filename),
'../',
namespace,
'en.json',
)
englishJson = JSON.parse(fs.readFileSync(englishFilePath, 'utf8'))
} catch (error) {
context.report({
node,
message: `Error parsing JSON: ${error.message}`,
})
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}.json but not in en.json for namespace "${namespace}"`,
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)
},
})
}
},
}
},
},
},
}