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', // }, // ], // }, ], });