diff --git a/packages/grafana-i18n/src/eslint/README.md b/packages/grafana-i18n/src/eslint/README.md index 12b3a559f9b..963aba21fc7 100644 --- a/packages/grafana-i18n/src/eslint/README.md +++ b/packages/grafana-i18n/src/eslint/README.md @@ -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: +No results found + +// With namespace: 'dashboard', auto-fix generates: +No results found + +// For JSX attributes, auto-fix generates: +
+``` + #### JSXText ```tsx diff --git a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs index 2b058426d53..4a1e5eaf54a 100644 --- a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs +++ b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs @@ -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, }, diff --git a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.test.js b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.test.js index 684823ae152..6268b807b27 100644 --- a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.test.js +++ b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.test.js @@ -1082,6 +1082,99 @@ const Foo = () => { errors: [{ messageId: 'noUntranslatedStringsProp' }], }, + // NAMESPACE FUNCTIONALITY TESTS + { + name: 'Basic untranslated text with namespace configuration', + code: ` +const Foo = () =>
Untranslated text
`, + filename, + options: [{ namespace: 'my-namespace' }], + errors: [ + { + messageId: 'noUntranslatedStrings', + suggestions: [ + { + messageId: 'wrapWithTrans', + output: ` +${TRANS_IMPORT} +const Foo = () =>
Untranslated text
`, + }, + ], + }, + ], + }, + + { + name: 'Prop fix with namespace configuration', + code: ` +const Foo = () => { + return ( +
+ ) +}`, + filename, + options: [{ namespace: 'alerts' }], + errors: [ + { + messageId: 'noUntranslatedStringsProp', + suggestions: [ + { + messageId: 'wrapWithT', + output: ` +${T_IMPORT} +const Foo = () => { + return ( +
+ ) +}`, + }, + ], + }, + ], + }, + + { + name: 'Empty namespace should not add prefix', + code: ` +const Foo = () =>
Test text
`, + filename, + options: [{ namespace: '' }], + errors: [ + { + messageId: 'noUntranslatedStrings', + suggestions: [ + { + messageId: 'wrapWithTrans', + output: ` +${TRANS_IMPORT} +const Foo = () =>
Test text
`, + }, + ], + }, + ], + }, + + { + name: 'Namespace with special characters gets used as-is', + code: ` +const Foo = () =>
Test content
`, + filename, + options: [{ namespace: 'feature.sub-module' }], + errors: [ + { + messageId: 'noUntranslatedStrings', + suggestions: [ + { + messageId: 'wrapWithTrans', + output: ` +${TRANS_IMPORT} +const Foo = () =>
Test content
`, + }, + ], + }, + ], + }, + // TODO: Enable test once all top-level issues have been fixed // and rule is enabled again // { diff --git a/packages/grafana-i18n/src/eslint/no-untranslated-strings/translation-utils.cjs b/packages/grafana-i18n/src/eslint/no-untranslated-strings/translation-utils.cjs index 8d7d4456ace..106a7aae80b 100644 --- a/packages/grafana-i18n/src/eslint/no-untranslated-strings/translation-utils.cjs +++ b/packages/grafana-i18n/src/eslint/no-untranslated-strings/translation-utils.cjs @@ -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; }; /**