# Grafana Internationalization ESLint Rules

This package also contains custom i18n eslint rules for use within the Grafana codebase and plugins.

## Rules

### `no-untranslated-strings`

Check if strings are marked for translation inside JSX Elements, in certain JSX props, and in certain object properties.

### Options

#### `basePaths`

Allows specifying base paths that should be stripped when generating i18n keys. Defaults to `['src']`.

#### Example

```tsx
// For a file located at public/app/features/search/EmptyState.tsx

// Specifying basePaths:
// {
//   '@grafana/i18n/no-untranslated-strings': ['error', { basePaths: ['public/app/features'] }],
// }

<Trans i18nKey="search.empty-state.no-results-found">No results found</Trans>

// Without basePaths:
// {
//   '@grafana/i18n/no-untranslated-strings': ['error'],
// }

<Trans i18nKey="public.app.features.search.empty-state.no-results-found">No results found</Trans>
```

#### `forceFix`

Allows specifying directories that, if the file is present within, then the rule will automatically fix the errors. This is primarily a workaround to allow for automatic mark up of new violations as the rule evolves.

#### Example:

```ts
{
  '@grafana/i18n/no-untranslated-strings': ['error', { forceFix: ['app/features/some-feature'] }],
}
```

#### `calleesToIgnore`

Allows specifying regexes for methods that should be ignored when checking if object properties are untranslated.

This is particularly useful to exclude references to properties such as `label` inside `css()` calls.

#### Example:

```ts
{
  '@grafana/i18n/no-untranslated-strings': ['error', { calleesToIgnore: ['^css$'] }],
}

// The below would not be reported as an error
const foo = css({
  label: 'test',
});

// The below would still be reported as an error
const bar = {
  label: 'test',
};
```

#### `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
// Bad ❌
<InlineToast placement="top" referenceElement={buttonRef.current}>
  Copied
</InlineToast>

// Good ✅
<InlineToast placement="top" referenceElement={buttonRef.current}>
  <Trans i18nKey="clipboard-button.inline-toast.success">Copied</Trans>
</InlineToast>
```

#### JSXAttributes

```tsx
// Bad ❌
<div title="foo bar" />

// Good ✅
<div title={t('some.key.foo-bar', 'foo bar')} />
```

#### Object properties

```tsx
// Bad ❌
const someConfig = {
  label: 'Some label',
};

// Good ✅
const getSomeConfig = () => ({
  label: t('some.key.label', 'Some label'),
});
```

#### Passing variables to translations

```tsx
// Bad ❌
const SearchTitle = ({ term }) => <div>Results for {term}</div>;

// Good ✅
const SearchTitle = ({ term }) => <Trans i18nKey="search-page.results-title">Results for {{ term }}</Trans>;

// Good ✅ (if you need to interpolate variables inside nested components)
const SearchTerm = ({ term }) => <Text color="success">{term}</Text>;
const SearchTitle = ({ term }) => (
  <Trans i18nKey="search-page.results-title">
    Results for <SearchTerm term={term} />
  </Trans>
);

// Good ✅ (if you need to interpolate variables and additional translated strings inside nested components)
const SearchTitle = ({ term }) => (
  <Trans i18nKey="search-page.results-title" values={{ myVariable: term }}>
    Results for <Text color="success">{'{{ myVariable }}'} and this translated text is also in green</Text>
  </Trans>
);
```

#### How to translate props or attributes

This rule checks if a string is wrapped up by the `Trans` tag, or if certain props contain untranslated strings.
We ask for such props to be translated with the `t()` function.

The below props are checked for untranslated strings:

- `label`
- `description`
- `placeholder`
- `aria-label`
- `title`
- `subTitle`
- `text`
- `tooltip`
- `message`
- `name`

```tsx
// Bad ❌
<input type="value" placeholder={'Username'} />;

// Good ✅
const placeholder = t('form.username-placeholder', 'Username');
return <input type="value" placeholder={placeholder} />;
```

Check more info about how translations work in Grafana in [Internationalization.md](https://github.com/grafana/grafana/blob/main/contribute/internationalization.md)

### `no-translation-top-level`

Ensure that `t()` translation method is not used at the top level of a file, outside of a component of method.
This is to prevent calling the translation method before it's been instantiated.

This does not cause an error if a file is lazily loaded, but refactors can cause errors, and it can cause problems in tests.

```tsx
// Bad ❌
const someTranslatedText = t('some.key', 'Some text');
const SomeComponent = () => {
  return <div title={someTranslatedText} />;
};

// Good ✅
const SomeComponent = () => {
  const someTranslatedText = t('some.key', 'Some text');
  return <div title={someTranslatedText} />;
};

// Bad ❌
const someConfigThatHasToBeShared = [{ foo: t('some.key', 'Some text') }];
const SomeComponent = () => {
  return (
    <div>
      {someConfigThatHasToBeShared.map((cfg) => {
        return <div>{cfg.foo}</div>;
      })}
    </div>
  );
};

// Good ✅
const getSomeConfigThatHasToBeShared = () => [{ foo: t('some.key', 'Some text') }];
const SomeComponent = () => {
  const configs = getSomeConfigThatHasToBeShared();
  return (
    <div>
      {configs.map((cfg) => {
        return <div>{cfg.foo}</div>;
      })}
    </div>
  );
};
```
