Skip to content

Instantly share code, notes, and snippets.

@LeCoupa
Last active November 14, 2024 22:28
Show Gist options
  • Save LeCoupa/2f7624395401d48519a61e449b672665 to your computer and use it in GitHub Desktop.
Save LeCoupa/2f7624395401d48519a61e449b672665 to your computer and use it in GitHub Desktop.
ESLint custom rule to check missing i18n locales or missing keys in .vue files (or nuxt)
// Check our product: https://www.thecompaniesapi.com/
import { locales } from '@thecompaniesapi/shared'
import type { Locales } from '@thecompaniesapi/shared'
import type { Rule } from 'eslint'
const rule: Rule.RuleModule = {
meta: {
docs: {
category: 'Possible Errors',
description: 'Enforce consistent i18n locale keys across translations',
recommended: true,
},
schema: [],
type: 'problem',
},
create(context) {
if (!context.filename.endsWith('.vue')) {
return {}
}
return {
Program() {
const source = context.sourceCode.getText()
const i18nMatch = source.match(/(<i18n\s+lang=["']json["']>)([\s\S]*?)(<\/i18n>)/i)
if (i18nMatch) {
const startOffset = i18nMatch.index! + i18nMatch[1].length
const i18nContent = i18nMatch[2].trim()
try {
const parsed = JSON.parse(i18nContent)
// Check if all required locales exist
for (const locale of locales) {
if (!parsed[locale]) {
context.report({
loc: {
end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
start: context.sourceCode.getLocFromIndex(startOffset),
},
message: `Missing required locale: ${locale}`,
})
}
}
// Check if no extra locales exist
for (const locale of Object.keys(parsed)) {
if (!locales.includes(locale as Locales)) {
const localeMatch = new RegExp(`"${locale}"\\s*:`, 'g').exec(i18nContent)
if (localeMatch) {
const localeOffset = startOffset + localeMatch.index
context.report({
loc: {
end: context.sourceCode.getLocFromIndex(localeOffset + locale.length + 2),
start: context.sourceCode.getLocFromIndex(localeOffset),
},
message: `Invalid locale: ${locale}. Allowed locales are: ${locales.join(', ')}`,
})
}
}
}
// Get all unique keys across all locales
const allKeys = new Set<string>()
for (const locale of locales) {
if (parsed[locale]) {
const keys = getAllKeys(parsed[locale])
keys.forEach(key => allKeys.add(key))
}
}
// Check that each locale has all keys
for (const locale of locales) {
if (parsed[locale]) {
const localeKeys = getAllKeys(parsed[locale])
const missingKeys = [...allKeys].filter(key => !localeKeys.includes(key))
if (missingKeys.length > 0) {
const localeMatch = new RegExp(`"${locale}"\\s*:\\s*{`, 'g').exec(i18nContent)
if (localeMatch) {
const localeOffset = startOffset + localeMatch.index
context.report({
loc: {
end: context.sourceCode.getLocFromIndex(localeOffset + locale.length + 2),
start: context.sourceCode.getLocFromIndex(localeOffset),
},
message: `Missing translations in "${locale}" locale: ${missingKeys.join(', ')}`,
})
}
}
}
}
}
catch (error) {
context.report({
loc: {
end: context.sourceCode.getLocFromIndex(startOffset + i18nContent.length),
start: context.sourceCode.getLocFromIndex(startOffset),
},
message: `Invalid JSON in i18n block: ${error}`,
})
}
}
function getAllKeys(object: any, prefix = ''): string[] {
let keys: string[] = []
for (const key in object) {
const newPrefix = prefix ? `${prefix}.${key}` : key
if (typeof object[key] === 'object' && object[key] !== null) {
keys = [...keys, ...getAllKeys(object[key], newPrefix)]
}
else {
keys.push(newPrefix)
}
}
return keys
}
},
}
},
}
export default rule
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment