import { RuleTester } from 'eslint';
import noUntranslatedStrings from '../rules/no-untranslated-strings.cjs';
RuleTester.setDefaultConfig({
languageOptions: {
ecmaVersion: 2018,
sourceType: 'module',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
});
const filename = 'public/app/features/some-feature/nested/SomeFile.tsx';
const packageName = '@grafana/i18n';
const TRANS_IMPORT = `import { Trans } from '${packageName}';`;
const T_IMPORT = `import { t } from '${packageName}/internal';`;
const USE_TRANSLATE_IMPORT = `import { useTranslate } from '${packageName}';`;
const TRANS_AND_USE_TRANSLATE_IMPORT = `import { Trans, useTranslate } from '${packageName}';`;
const ruleTester = new RuleTester();
ruleTester.run('eslint no-untranslated-strings', noUntranslatedStrings, {
test: [],
valid: [
{
name: 'Text in Trans component',
code: `const Foo = () => Translated text`,
filename,
},
{
name: 'Text in Trans component with whitespace/JSXText elements',
code: `const Foo = () =>
Translated text
`,
filename,
},
{
name: 'Empty component',
code: `
`,
filename,
},
{
name: 'Text using t() function',
code: `{t('translated.key', 'Translated text')}
`,
filename,
},
{
name: 'Prop using t() function',
code: ``,
filename,
},
{
name: 'Empty string prop',
code: ``,
filename,
},
{
name: 'Prop using boolean',
code: ``,
filename,
},
{
name: 'Prop using number',
code: ``,
filename,
},
{
name: 'Prop using null',
code: ``,
filename,
},
{
name: 'Prop using undefined',
code: ``,
filename,
},
{
name: 'Variable interpolation',
code: `{variable}
`,
filename,
},
{
name: 'Entirely non-alphanumeric text (prop)',
code: ``,
filename,
},
{
name: 'Entirely non-alphanumeric text',
code: `-
`,
filename,
},
{
name: 'Non-alphanumeric siblings',
code: `({variable})
`,
filename,
},
{
name: "Ternary in an attribute we don't care about",
code: ``,
filename,
},
{
name: 'Ternary with falsy strings',
code: ``,
},
{
name: 'Ternary with no strings',
code: ``,
filename,
},
{
name: 'Object property',
code: `const getThing = () => ({
label: t('test', 'Test'),
})`,
filename,
},
{
// Ideally we would catch this, but test case is to ensure that
// we aren't reporting an error
name: 'Object property using variable',
code: `
const getThing = () => {
const foo = 'test';
const thing = {
label: foo,
}
}`,
filename,
},
{
name: 'Object property using dynamic/other keys',
code: `
const getThing = () => {
const foo = 'label';
const label = 'not-a-label';
const thing = {
1: 'a',
// We can't easily check for computed keys, so for now don't worry about this case
[foo]: 'test',
// This is dumb, but we need to check that we don't confuse
// the name of the variable for the key name
[label]: 'test',
['title']: 'test',
}
}`,
filename,
},
{
name: 'Label reference inside `css` call',
code: `const getThing = () => {
const thing = css({
label: 'red',
});
}`,
options: [{ calleesToIgnore: ['somethingelse', '^css$'] }],
filename,
},
{
name: 'Object property value that is a boolean or number',
code: `const getThing = () => {
const thing = {
label: true,
title: 1
};
}`,
filename,
},
{
name: 'Object property at top level',
code: `const thing = { label: 'test' }`,
filename,
},
{
name: 'Object property in default props',
code: `const Foo = ({ foobar = {label: 'test'} }) => {foobar.label}
`,
filename,
},
],
invalid: [
/**
* FIXABLE CASES
*/
// Basic happy path case:
// untranslated text, in a component, in a file location where we can extract a prefix,
// and it can be fixed
{
name: 'Basic untranslated text in component',
code: `
const Foo = () => Untranslated text
`,
filename,
errors: [
{
messageId: 'noUntranslatedStrings',
suggestions: [
{
messageId: 'wrapWithTrans',
output: `
${TRANS_IMPORT}
const Foo = () => Untranslated text
`,
},
],
},
],
},
{
name: 'Text inside JSXElement, not in a function',
code: `
const thing = foo
`,
filename,
errors: [
{
messageId: 'noUntranslatedStrings',
suggestions: [
{
messageId: 'wrapWithTrans',
output: `
${TRANS_IMPORT}
const thing = foo
`,
},
],
},
],
},
{
name: 'Fixes medium length strings',
code: `
const Foo = () => This is a longer string that we will translate
`,
filename,
errors: [
{
messageId: 'noUntranslatedStrings',
suggestions: [
{
messageId: 'wrapWithTrans',
output: `
${TRANS_IMPORT}
const Foo = () => This is a longer string that we will translate
`,
},
],
},
],
},
{
name: 'Fixes short strings with many words',
code: `
const Foo = () => lots of sho rt word s to be filt ered
`,
filename,
errors: [
{
messageId: 'noUntranslatedStrings',
suggestions: [
{
messageId: 'wrapWithTrans',
output: `
${TRANS_IMPORT}
const Foo = () => lots of sho rt word s to be filt ered
`,
},
],
},
],
},
{
name: 'expression',
code: `
const foo = <>hello>`,
filename,
errors: [
{
messageId: 'noUntranslatedStrings',
suggestions: [
{
messageId: 'wrapWithTrans',
output: `
${TRANS_IMPORT}
const foo = <>hello>`,
},
],
},
],
},
{
name: 'Fixes strings in JSX in props',
code: `
const Foo = () => Test>} />
`,
filename,
errors: [
{
messageId: 'noUntranslatedStrings',
suggestions: [
{
messageId: 'wrapWithTrans',
output: `
${TRANS_IMPORT}
const Foo = () => Test>} />
`,
},
],
},
],
},
{
name: 'Fixes basic prop case and adds useTranslate',
code: `
const Foo = () => {
const fooBar = 'a';
return (
)
}`,
filename,
errors: [
{
messageId: 'noUntranslatedStringsProp',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${USE_TRANSLATE_IMPORT}
const Foo = () => {
const { t } = useTranslate();
const fooBar = 'a';
return (
)
}`,
},
],
},
],
},
{
name: 'Fixes using t when not inside something that looks like a React component',
code: `
function foo() {
return (
)
}`,
filename,
errors: [
{
messageId: 'noUntranslatedStringsProp',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${T_IMPORT}
function foo() {
return (
)
}`,
},
],
},
],
},
{
name: 'Fixes using t when not inside something that looks like a React component - anonymous function',
code: `
const foo = function() {
return ;
}`,
filename,
errors: [
{
messageId: 'noUntranslatedStringsProp',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${T_IMPORT}
const foo = function() {
return ;
}`,
},
],
},
],
},
{
name: 'Fixes when Trans import already exists',
code: `
${TRANS_IMPORT}
const Foo = () => {
return (
)
}`,
filename,
errors: [
{
messageId: 'noUntranslatedStringsProp',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${TRANS_AND_USE_TRANSLATE_IMPORT}
const Foo = () => {
const { t } = useTranslate();
return (
)
}`,
},
],
},
],
},
{
name: 'Fixes when looks in an upper cased function but does not return JSX',
code: `
const Foo = () => {
return {
foo:
}
}`,
filename,
errors: [
{
messageId: 'noUntranslatedStringsProp',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${T_IMPORT}
const Foo = () => {
return {
foo:
}
}`,
},
],
},
],
},
{
name: 'Fixes correctly when useTranslate already exists',
code: `
${USE_TRANSLATE_IMPORT}
const Foo = () => {
const { t } = useTranslate();
return (
)
}`,
filename,
errors: [
{
messageId: 'noUntranslatedStringsProp',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${USE_TRANSLATE_IMPORT}
const Foo = () => {
const { t } = useTranslate();
return (
)
}`,
},
],
},
],
},
{
name: 'Fixes and uses ID from attribute if exists',
code: `
${T_IMPORT}
const Foo = () => `,
filename,
errors: [
{
messageId: 'noUntranslatedStringsProp',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${T_IMPORT}
const Foo = () => `,
},
],
},
],
},
{
name: 'Fixes correctly when Trans import already exists',
code: `
${TRANS_IMPORT}
const Foo = () => Untranslated text
`,
filename,
errors: [
{
messageId: 'noUntranslatedStrings',
suggestions: [
{
messageId: 'wrapWithTrans',
output: `
${TRANS_IMPORT}
const Foo = () => Untranslated text
`,
},
],
},
],
},
{
name: 'Fixes correctly when t() import already exists',
code: `
${T_IMPORT}
const Foo = () => `,
filename,
errors: [
{
messageId: 'noUntranslatedStringsProp',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${T_IMPORT}
const Foo = () => `,
},
],
},
],
},
{
name: 'Fixes correctly when useTranslate import already exists',
code: `
${USE_TRANSLATE_IMPORT}
const Foo = () => {
const { t } = useTranslate();
return (<>
>)
}
`,
filename,
errors: [
{
messageId: 'noUntranslatedStringsProp',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${USE_TRANSLATE_IMPORT}
const Foo = () => {
const { t } = useTranslate();
return (<>
>)
}
`,
},
],
},
],
},
{
name: 'Fixes correctly when no return statement',
code: `
const Foo = () => {
const foo =
}
`,
filename,
errors: [
{
messageId: 'noUntranslatedStringsProp',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${T_IMPORT}
const Foo = () => {
const foo =
}
`,
},
],
},
],
},
{
name: 'Fixes correctly when import exists but needs to add t()',
code: `
${TRANS_IMPORT}
const Foo = () => `,
filename,
errors: [
{
messageId: 'noUntranslatedStringsProp',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${T_IMPORT}
${TRANS_IMPORT}
const Foo = () => `,
},
],
},
],
},
{
name: 'Fixes correctly with a Class component',
code: `
class Foo extends React.Component {
render() {
return untranslated text
;
}
}`,
filename,
errors: [
{
messageId: 'noUntranslatedStrings',
suggestions: [
{
messageId: 'wrapWithTrans',
output: `
${TRANS_IMPORT}
class Foo extends React.Component {
render() {
return untranslated text
;
}
}`,
},
],
},
],
},
{
name: 'Fixes basic prop case',
code: `
const Foo = () => `,
filename,
errors: [
{
messageId: 'noUntranslatedStringsProp',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${T_IMPORT}
const Foo = () => `,
},
],
},
],
},
{
name: 'Fixes prop case with string literal inside expression container',
code: `
const Foo = () => `,
filename,
errors: [
{
messageId: 'noUntranslatedStringsProp',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${T_IMPORT}
const Foo = () => `,
},
],
},
],
},
{
name: 'Fixes prop case with double quotes in value',
code: `
const Foo = () => `,
filename,
errors: [
{
messageId: 'noUntranslatedStringsProp',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${T_IMPORT}
const Foo = () => `,
},
],
},
],
},
{
name: 'Fixes case with nested functions/components',
code: `
${TRANS_IMPORT}
const Foo = () => {
const getSomething = () => {
return foo
;
}
return {getSomething()}
;
}
`,
filename,
errors: [
{
messageId: 'noUntranslatedStrings',
suggestions: [
{
messageId: 'wrapWithTrans',
output: `
${TRANS_IMPORT}
const Foo = () => {
const getSomething = () => {
return foo
;
}
return {getSomething()}
;
}
`,
},
],
},
],
},
{
name: 'Untranslated object property',
code: `
const Foo = () => {
const thing = {
label: 'test',
}
return {thing.label}
;
}`,
filename,
errors: [
{
messageId: 'noUntranslatedStringsProperties',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${USE_TRANSLATE_IMPORT}
const Foo = () => {
const { t } = useTranslate();
const thing = {
label: t(\"some-feature.foo.thing.label.test\", \"test\"),
}
return {thing.label}
;
}`,
},
],
},
],
},
{
name: 'Untranslated object property with existing import',
code: `
${T_IMPORT}
const Foo = () => {
const thing = {
label: 'test',
}
}`,
filename,
errors: [
{
messageId: 'noUntranslatedStringsProperties',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${T_IMPORT}
const Foo = () => {
const thing = {
label: t(\"some-feature.foo.thing.label.test\", \"test\"),
}
}`,
},
],
},
],
},
{
name: 'Untranslated object property with calleesToIgnore',
code: `
const Foo = () => {
const thing = doAThing({
label: 'test',
})
}`,
options: [{ calleesToIgnore: ['doSomethingElse'] }],
filename,
errors: [
{
messageId: 'noUntranslatedStringsProperties',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${T_IMPORT}
const Foo = () => {
const thing = doAThing({
label: t(\"some-feature.foo.thing.label.test\", \"test\"),
})
}`,
},
],
},
],
},
/**
* AUTO FIXES
*/
{
name: 'Auto fixes when options are configured',
code: `const Foo = () => test
`,
filename,
options: [{ forceFix: ['public/app/features/some-feature'] }],
output: `${TRANS_IMPORT}
const Foo = () => test
`,
errors: [
{
messageId: 'noUntranslatedStrings',
suggestions: [
{
messageId: 'wrapWithTrans',
output: `${TRANS_IMPORT}
const Foo = () => test
`,
},
],
},
],
},
{
name: 'Auto fixes when options are configured - prop',
code: `
const Foo = () => {
return
}`,
filename,
options: [{ forceFix: ['public/app/features/some-feature'] }],
output: `
${USE_TRANSLATE_IMPORT}
const Foo = () => {
const { t } = useTranslate();
return
}`,
errors: [
{
messageId: 'noUntranslatedStringsProp',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${USE_TRANSLATE_IMPORT}
const Foo = () => {
const { t } = useTranslate();
return
}`,
},
],
},
],
},
{
name: 'Auto fixes object property',
code: `
const Foo = () => {
return {
label: 'test',
}
}`,
filename,
options: [{ forceFix: ['public/app/features/some-feature'] }],
output: `
${T_IMPORT}
const Foo = () => {
return {
label: t("some-feature.foo.label.test", "test"),
}
}`,
errors: [
{
messageId: 'noUntranslatedStringsProperties',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${T_IMPORT}
const Foo = () => {
return {
label: t("some-feature.foo.label.test", "test"),
}
}`,
},
],
},
],
},
/**
* UNFIXABLE CASES
*/
{
name: 'Multiple untranslated strings in one element',
code: `const Foo = () => test {name} example
`,
filename,
errors: [
{
messageId: 'noUntranslatedStrings',
},
],
},
{
name: 'Cannot fix text with expression sibling',
code: `const Foo = () => {name} Hello
`,
filename,
errors: [{ messageId: 'noUntranslatedStrings' }],
},
{
name: 'Cannot fix text with expression sibling in fragment',
code: `
const Foo = () => {
const bar = {
baz: (<>Hello {name}>)
}
}`,
filename,
errors: [{ messageId: 'noUntranslatedStrings' }],
},
{
name: 'Cannot fix text containing HTML entities',
code: `const Foo = () => Something
`,
filename,
errors: [{ messageId: 'noUntranslatedStrings' }],
},
{
name: 'Cannot fix text that is too long',
code: `const Foo = () => This is something with lots of text that we don't want to translate automatically
`,
filename,
errors: [{ messageId: 'noUntranslatedStrings' }],
},
{
name: 'Cannot fix prop text that is too long',
code: `const Foo = () => `,
filename,
errors: [{ messageId: 'noUntranslatedStringsProp' }],
},
{
name: 'Cannot fix text with HTML sibling',
code: `const Foo = () => something foo bar
`,
filename,
errors: [{ messageId: 'noUntranslatedStrings' }],
},
{
name: 'JSXAttribute not in a function',
code: ``,
filename,
errors: [{ messageId: 'noUntranslatedStringsProp' }],
},
{
name: 'Cannot fix JSXExpression in attribute if it is template literal',
code: `const Foo = () => `,
filename,
errors: [{ messageId: 'noUntranslatedStringsProp' }],
},
{
name: 'Cannot fix text outside correct directory location',
code: `const Foo = () => Untranslated text
`,
filename: 'public/something-else/foo/SomeOtherFile.tsx',
errors: [{ messageId: 'noUntranslatedStrings' }],
},
{
name: 'Invalid when ternary with string literals - both',
code: `const Foo = () => {isAThing ? 'Foo' : 'Bar'}
`,
filename,
errors: [{ messageId: 'noUntranslatedStrings' }, { messageId: 'noUntranslatedStrings' }],
},
{
name: 'Invalid when ternary with string literals - alternate',
code: `const Foo = () => {isAThing ? 'Foo' : 1}
`,
filename,
errors: [{ messageId: 'noUntranslatedStrings' }],
},
{
name: 'Invalid when ternary with string literals - prop',
code: `const Foo = () => `,
filename,
errors: [{ messageId: 'noUntranslatedStringsProp' }, { messageId: 'noUntranslatedStringsProp' }],
},
{
name: 'Cannot fix if `t` already exists from somewhere else',
code: `
const Foo = () => {
const t = () => 'something else';
return (
)
}`,
filename,
errors: [{ messageId: 'noUntranslatedStringsProp' }],
},
// TODO: Enable test once all top-level issues have been fixed
// and rule is enabled again
// {
// name: 'Object property at top level scope',
// code: `
// const thing = {
// label: 'test',
// }`,
// filename,
// errors: [
// {
// messageId: 'noUntranslatedStringsProperties',
// },
// ],
// },
],
});