Chore: Add Namespace Support To Grafana-i18n ESLint Rule (#112730)
This commit is contained in:
@@ -70,6 +70,32 @@ const bar = {
|
||||
};
|
||||
```
|
||||
|
||||
#### `namespace`
|
||||
|
||||
Allows specifying a namespace prefix that will be added to all auto-generated translation keys when using ESLint's auto-fix feature. The namespace is separated from the key with a colon (:).
|
||||
|
||||
This is useful for organizing translation keys by feature area or preventing key collisions between different parts of the application.
|
||||
|
||||
#### Example:
|
||||
|
||||
```tsx
|
||||
// Configuration:
|
||||
{
|
||||
'@grafana/i18n/no-untranslated-strings': ['error', { namespace: 'dashboard' }],
|
||||
}
|
||||
|
||||
// For a file located at src/features/search/EmptyState.tsx
|
||||
|
||||
// Without namespace, auto-fix generates:
|
||||
<Trans i18nKey="features.search.empty-state.no-results-found">No results found</Trans>
|
||||
|
||||
// With namespace: 'dashboard', auto-fix generates:
|
||||
<Trans i18nKey="dashboard:features.search.empty-state.no-results-found">No results found</Trans>
|
||||
|
||||
// For JSX attributes, auto-fix generates:
|
||||
<div title={t("dashboard:features.search.empty-state.title-no-results", "No results")} />
|
||||
```
|
||||
|
||||
#### JSXText
|
||||
|
||||
```tsx
|
||||
|
||||
+6
-2
@@ -2,8 +2,8 @@
|
||||
/** @typedef {import('@typescript-eslint/utils').TSESTree.Node} Node */
|
||||
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXElement} JSXElement */
|
||||
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXFragment} JSXFragment */
|
||||
/** @typedef {import('@typescript-eslint/utils').TSESLint.RuleModule<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{ forceFix: string[] , calleesToIgnore: string[], basePaths: string[] }]>} RuleDefinition */
|
||||
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{forceFix: string[], calleesToIgnore: string[], basePaths: string[]}]>} RuleContextWithOptions */
|
||||
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleModule<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{ forceFix: string[] , calleesToIgnore: string[], basePaths: string[], namespace?: string }]>} RuleDefinition */
|
||||
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{forceFix: string[], calleesToIgnore: string[], basePaths: string[], namespace?: string}]>} RuleContextWithOptions */
|
||||
|
||||
const {
|
||||
getNodeValue,
|
||||
@@ -308,6 +308,10 @@ const noUntranslatedStrings = createRule({
|
||||
},
|
||||
default: ['src'],
|
||||
},
|
||||
namespace: {
|
||||
type: 'string',
|
||||
description: 'Namespace to prepend to translation keys',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
|
||||
+93
@@ -1082,6 +1082,99 @@ const Foo = () => {
|
||||
errors: [{ messageId: 'noUntranslatedStringsProp' }],
|
||||
},
|
||||
|
||||
// NAMESPACE FUNCTIONALITY TESTS
|
||||
{
|
||||
name: 'Basic untranslated text with namespace configuration',
|
||||
code: `
|
||||
const Foo = () => <div>Untranslated text</div>`,
|
||||
filename,
|
||||
options: [{ namespace: 'my-namespace' }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noUntranslatedStrings',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'wrapWithTrans',
|
||||
output: `
|
||||
${TRANS_IMPORT}
|
||||
const Foo = () => <div><Trans i18nKey="my-namespace:some-feature.foo.untranslated-text">Untranslated text</Trans></div>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Prop fix with namespace configuration',
|
||||
code: `
|
||||
const Foo = () => {
|
||||
return (
|
||||
<div title="foo" />
|
||||
)
|
||||
}`,
|
||||
filename,
|
||||
options: [{ namespace: 'alerts' }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noUntranslatedStringsProp',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'wrapWithT',
|
||||
output: `
|
||||
${T_IMPORT}
|
||||
const Foo = () => {
|
||||
return (
|
||||
<div title={t("alerts:some-feature.foo.title-foo", "foo")} />
|
||||
)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Empty namespace should not add prefix',
|
||||
code: `
|
||||
const Foo = () => <div>Test text</div>`,
|
||||
filename,
|
||||
options: [{ namespace: '' }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noUntranslatedStrings',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'wrapWithTrans',
|
||||
output: `
|
||||
${TRANS_IMPORT}
|
||||
const Foo = () => <div><Trans i18nKey="some-feature.foo.test-text">Test text</Trans></div>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Namespace with special characters gets used as-is',
|
||||
code: `
|
||||
const Foo = () => <div>Test content</div>`,
|
||||
filename,
|
||||
options: [{ namespace: 'feature.sub-module' }],
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noUntranslatedStrings',
|
||||
suggestions: [
|
||||
{
|
||||
messageId: 'wrapWithTrans',
|
||||
output: `
|
||||
${TRANS_IMPORT}
|
||||
const Foo = () => <div><Trans i18nKey="feature.sub-module:some-feature.foo.test-content">Test content</Trans></div>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// TODO: Enable test once all top-level issues have been fixed
|
||||
// and rule is enabled again
|
||||
// {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXChild} JSXChild */
|
||||
/** @typedef {import('@typescript-eslint/utils').TSESTree.Property} Property */
|
||||
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleFixer} RuleFixer */
|
||||
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT', [{forceFix: string[], calleesToIgnore: string[], basePaths: string[]}]>} RuleContextWithOptions */
|
||||
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT', [{forceFix: string[], calleesToIgnore: string[], basePaths: string[], namespace?: string}]>} RuleContextWithOptions */
|
||||
const { AST_NODE_TYPES } = require('@typescript-eslint/utils');
|
||||
|
||||
/**
|
||||
@@ -168,6 +168,7 @@ function getTranslationPrefix(context) {
|
||||
*/
|
||||
const getI18nKey = (node, context) => {
|
||||
const prefixFromFilePath = getTranslationPrefix(context);
|
||||
const namespace = context.options[0]?.namespace;
|
||||
const stringValue = getNodeValue(node);
|
||||
|
||||
const componentNames = getComponentNames(node, context);
|
||||
@@ -214,7 +215,8 @@ const getI18nKey = (node, context) => {
|
||||
|
||||
const fullPrefix = [prefixFromFilePath, ...componentNames, propertyName, kebabString].filter(Boolean).join('.');
|
||||
|
||||
return fullPrefix;
|
||||
// Prepend namespace if provided
|
||||
return namespace ? `${namespace}:${fullPrefix}` : fullPrefix;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user