Last active
November 14, 2024 22:28
-
-
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)
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
// 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