# Translate your plugin

[View as Markdown](https://grafana.com/developers/plugin-tools/how-to-guides/plugin-internationalization)

By default, plugins are available in English only and are not translated when you change your language settings in the [Grafana UI](https://grafana.com/docs/grafana/latest/administration/organization-preferences/#change-grafana-language).

If you want your plugin to be translatable to other languages you need to perform the changes described in this document. You can find the [list of available languages](https://github.com/grafana/grafana/blob/main/packages/grafana-i18n/src/constants.ts) in GitHub.

note

While this example is based on a panel plugin, the process is the same for data source and app plugins.

## Before you begin[​](#before-you-begin "Direct link to Before you begin")

info

Translation is available starting from Grafana 12.1.0. If you're using Grafana 11.0.0 and later follow the steps in [Translate your plugin before Grafana 12.1.0](https://grafana.com/developers/plugin-tools/how-to-guides/plugin-internationalization-grafana-11.md). If you're using older versions of Grafana the plugin will not work.

The following is recommended:

* Basic knowledge of Grafana plugin development
* Basic understanding of the [`t` function](https://www.i18next.com/overview/api#t)
* Basic understanding of the [`Trans` component](https://react.i18next.com/latest/trans-component)

## Overview of the files affected by translation[​](#overview-of-the-files-affected-by-translation "Direct link to Overview of the files affected by translation")

If you create your plugin running the `create-plugin` scaffolding tool, enabling plugin translation involves updating the following files:

* `docker-compose.yaml`
* `plugin.json`
* `module.ts`
* `eslint.config.mjs`
* `package.json`

By the end of the translation process you'll have a file structure like this:

```text
myorg-myplugin-plugintype/
├── src/
│   ├── locales
│   │  ├── en-US
│   │  │  └── myorg-myplugin-plugintype.json
│   │  └── es-ES
│   │     └── myorg-myplugin-plugintype.json
│   ├── module.ts
│   └── plugin.json
├── tests/
├── docker-compose.yaml
├── eslint.config.mjs
└── package.json

```

note

The `src/locales/en-US/<plugin-id>.json` file is generated by translation tooling and reviewed by translators. Starting with Grafana 13.1.0, Grafana doesn't load it at runtime. `en-US` text comes from the defaults in your source code, so the browser doesn't need to fetch a translation file the bundle already contains. Refer to [Determine the text to translate](#determine-the-text-to-translate) section to see how to declare those defaults, including the special handling required for plurals.

## Set up your plugin for translation[​](#set-up-your-plugin-for-translation "Direct link to Set up your plugin for translation")

Follow these steps to update your plugin and set it up for translation.

### Enable translation in your Grafana instance (12.1.0 only)[​](#enable-translation-in-your-grafana-instance-1210-only "Direct link to Enable translation in your Grafana instance (12.1.0 only)")

To translate your plugin, you need to [enable the feature toggle](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/) `localizationForPlugins` in your Grafana instance.

To do so, update `docker-compose.yaml` in your plugin with the feature toggle `localizationForPlugins`:

docker-compose.yaml

```yaml
services:
  grafana:
    extends:
      file: .config/docker-compose-base.yaml
      service: grafana
    environment:
      GF_FEATURE_TOGGLES_ENABLE: localizationForPlugins

```

### Define the languages and Grafana dependencies[​](#define-the-languages-and-grafana-dependencies "Direct link to Define the languages and Grafana dependencies")

Set up the translation languages for your plugin and the Grafana dependencies for translation.

To do so, add the relevant `grafanaDependency` and `languages` you want to translate to in the `plugin.json` file. For example, if you want to add English (US) and Spanish (Spain):

plugin.json

```json
"dependencies": {
    "grafanaDependency": ">=12.1.0", // @grafana/i18n works from version 11.0.0 and higher
    "plugins": []
  },
"languages": ["en-US", "es-ES"] // the languages that the plugin supports

```

### Update to the latest version of `create-plugin`[​](#update-to-the-latest-version-of-create-plugin "Direct link to update-to-the-latest-version-of-create-plugin")

Update your `create-plugin` configs to the latest version using the following command:

* npm
* Yarn
* pnpm

```shell
npx @grafana/create-plugin@latest update

```

```shell
yarn dlx @grafana/create-plugin@latest update

```

```shell
pnpm dlx @grafana/create-plugin@latest update

```

### Initialize translations in `module.ts` for a plugin without `@grafana/scenes`[​](#initialize-translations-in-modulets-for-a-plugin-without-grafanascenes "Direct link to initialize-translations-in-modulets-for-a-plugin-without-grafanascenes")

Add plugin translation to `module.ts`:

module.ts

```ts
import { initPluginTranslations } from '@grafana/i18n';
import pluginJson from 'plugin.json';

await initPluginTranslations(pluginJson.id);

```

### Initialize translations in `module.ts` for a plugin that uses `@grafana/scenes`[​](#initialize-translations-in-modulets-for-a-plugin-that-uses-grafanascenes "Direct link to initialize-translations-in-modulets-for-a-plugin-that-uses-grafanascenes")

module.ts

```ts
import { initPluginTranslations } from '@grafana/i18n';
import pluginJson from 'plugin.json';
import { loadResources } from '@grafana/scenes';

await initPluginTranslations(pluginJson.id, [loadResources]);

```

## Determine the text to translate[​](#determine-the-text-to-translate "Direct link to Determine the text to translate")

After you've configured your plugin for translation, you can proceed to mark up the language strings you want to translate. Each translatable string is assigned a unique key that ends up in each translation file under `locales/<locale>/<plugin id>.json`.

note

The string you pass as the default (the second argument of `t()` or the children of `<Trans>`) is what Grafana renders for `en-US`. Starting with Grafana 13.1.0, translation resources for `en-US` are **not** loaded at runtime. Instead, they're read directly from your source code, so both the plugin and Grafana don't need to fetch a translation file the bundle already contains, saving hundreds of kilobytes on first paint. The `src/locales/en-US/<plugin-id>.json` file is still generated by `i18n-extract` and used as the source of truth for translators, but it's not fetched by the browser. Keep your in-source defaults meaningful and up to date.

The following example uses the `t` function:

```diff
export const plugin = new PanelPlugin<SimpleOptions>(SimplePanel).setPanelOption
   return builder
     .addTextInput({
       path: 'text',
-      name: 'Simple text option',
-      description: 'Description of panel option',
-      defaultValue: 'Default value of text input option',
+      name: t('panel.options.text.name', 'Simple text option'),
+      description: t('panel.options.text.description', 'Description of panel option'),
+      defaultValue: t('panel.options.text.defaultValue', 'Default value of text input option'),
     })
     .addBooleanSwitch({
       path: 'showSeriesCount',
-      name: 'Show series counter',
+      name: t('panel.options.showSeriesCount.name', 'Show series counter'),
       defaultValue: false,
     })
     .addRadio({
       path: 'seriesCountSize',
       defaultValue: 'sm',
-      name: 'Series counter size',
+      name: t('panel.options.seriesCountSize.name', 'Series counter size'),
       settings: {
         options: [
           {
             value: 'sm',
-            label: 'Small',
+            label: t('panel.options.seriesCountSize.options.sm', 'Small'),
           },
           {
             value: 'md',
-            label: 'Medium',
+            label: t('panel.options.seriesCountSize.options.md', 'Medium'),
           },
           {
             value: 'lg',
-            label: 'Large',
+            label: t('panel.options.seriesCountSize.options.lg', 'Large'),
           },
         ],
       },

```

### Example using the `Trans` component:[​](#example-using-the-trans-component "Direct link to example-using-the-trans-component")

```diff
 import { SimpleOptions } from 'types';
 import { css, cx } from '@emotion/css';
 import { useStyles2, useTheme2 } from '@grafana/ui';
 import { PanelDataErrorView } from '@grafana/runtime';
+import { Trans } from '@grafana/i18n';

 interface Props extends PanelProps<SimpleOptions> {}

@@ -60,9 +61,15 @@ export const SimplePanel: React.FC<Props> = ({ options, data, width, height, fie

       <div className={styles.textBox}>
         {options.showSeriesCount && (
-          <div data-testid="simple-panel-series-counter">Number of series: {data.series.length}</div>
+          <div data-testid="simple-panel-series-counter">
+            <Trans i18nKey="components.simplePanel.options.showSeriesCount">
+              Number of series: {{ numberOfSeries: data.series.length }}
+            </Trans>
+          </div>
         )}
-        <div>Text option value: {options.text}</div>
+        <Trans i18nKey="components.simplePanel.options.textOptionValue">
+          Text option value: {{ optionValue: options.text }}
+        </Trans>
       </div>
     </div>
   );

```

### Handle pluralization[​](#handle-pluralization "Direct link to Handle pluralization")

Be extra careful with plurals. Starting with Grafana 13.1.0, `en-US` resources aren't loaded at runtime, so every plural form has to be declared as a default in source as well, because the JSON file isn't there to fill them in. A missing `_one` or `_other` default is a missing string at runtime, not a graceful fallback. Pass `count` together with `defaultValue_one` and `defaultValue_other` (and any other [CLDR plural categories](https://cldr.unicode.org/index/cldr-spec/plural-rules) you need); `i18next` picks the right suffix at runtime and `i18next-cli` emits the matching `<key>_<suffix>` entries into `src/locales/en-US/<plugin-id>.json` on extract.

**Using `t()`**

```ts
t('panel.counts.folder', '', {
  count: folderCount,
  defaultValue_one: '{{count}} folder',
  defaultValue_other: '{{count}} folders',
});

```

The positional default (second argument) is intentionally empty, since the plural-aware defaults come from the options object.

**Using `<Trans>`**

```tsx
<Trans
  i18nKey="panel.routes.view"
  count={routes.length}
  tOptions={{
    defaultValue_one: 'View route',
    defaultValue_other: 'View routes',
  }}
>
  View route
</Trans>

```

Children stay in place because they carry the JSX structure (for example `<strong>`, `<br/>`, `<Icon />`) that `<n>` placeholders in the JSON values map back to at render time.

For reference, [grafana/grafana#125312](https://github.com/grafana/grafana/pull/125312) and [grafana/grafana#125316](https://github.com/grafana/grafana/pull/125316) migrated every plural `t()` and `<Trans>` in Grafana itself to this shape.

This convention is enforced by the `@grafana/i18n/t-plural-defaults` and `@grafana/i18n/trans-plural-defaults` ESLint rules. See [Configure ESLint rules for translations](#configure-eslint-rules-for-translations) to enable them.

## Obtain the translated text[​](#obtain-the-translated-text "Direct link to Obtain the translated text")

Use the [`i18next-cli`](https://github.com/i18next/i18next-cli#readme) and `i18n-extract` to sweep all input files, extract tagged `i18n` keys, and save the translations.

### Parse for translations[​](#parse-for-translations "Direct link to Parse for translations")

Install the `i18next-cli`:

* npm
* Yarn
* pnpm

```shell
npm install --save-dev i18next-cli

```

```shell
yarn add --dev i18next-cli

```

```shell
pnpm add --save-dev i18next-cli

```

Next, create a configuration file `i18next.config.ts` and configure it so the CLI sweeps your plugin and extracts the translations into the `src/locales/[$LOCALE]/[your-plugin].json`:

warning

The path `src/locales/[$LOCALE]/[your-plugin-id].json` is mandatory. If you modify it translations won't work.

i18next.config.ts

```ts
import { defineConfig } from 'i18next-cli';
import pluginJson from './src/plugin.json';

export default defineConfig({
  locales: pluginJson.languages,
  extract: {
    input: ['src/**/*.{tsx,ts}'],
    output: 'src/locales/{{language}}/{{namespace}}.json',
    defaultNS: pluginJson.id,
    functions: ['t', '*.t'],
    transComponents: ['Trans'],
  },
});

```

### Obtain your translation file[​](#obtain-your-translation-file "Direct link to Obtain your translation file")

Add the translation script `i18n-extract` to `package.json`:

package.json

```json
  "scripts": {
    "i18n-extract": "i18next-cli extract --sync-primary"
  },

```

Run the script to translate the files:

* npm
* Yarn
* pnpm

```shell
npm run i18n-extract

```

```shell
yarn i18n-extract

```

```shell
pnpm run i18n-extract

```

The translation file will look similar to this:

src/locales/en-US/\[your-plugin-id].json

```json
{
  "components": {
    "simplePanel": {
      "options": {
        "showSeriesCount": "Number of series: {{numberOfSeries}}",
        "textOptionValue": "Text option value: {{optionValue}}"
      }
    }
  },
  "panel": {
    "options": {
      "seriesCountSize": {
        "name": "Series counter size",
        "options": {
          "lg": "Large",
          "md": "Medium",
          "sm": "Small"
        }
      },
      "showSeriesCount": {
        "name": "Show series counter"
      },
      "text": {
        "defaultValue": "Default value of text input option",
        "description": "Description of panel option",
        "name": "Simple text option"
      }
    }
  }
}

```

## Test the translated plugin[​](#test-the-translated-plugin "Direct link to Test the translated plugin")

To test the plugin follow the steps in [Set up your development environment](https://grafana.com/developers/plugin-tools/set-up.md) to run your plugin locally.

You can then verify your plugin is displaying the appropriate text as you [change the language](https://grafana.com/docs/grafana/latest/administration/organization-preferences/#change-grafana-language).

## Configure ESLint rules for translations[​](#configure-eslint-rules-for-translations "Direct link to Configure ESLint rules for translations")

Add the `@grafana/i18n` rules in `eslint.config.mjs`:

eslint.config.mjs

```js
/* existing imports */
import grafanaI18nPlugin from '@grafana/i18n/eslint-plugin';

export default defineConfig([
  /* existing config */
  {
    name: 'grafana/i18n-rules',
    plugins: { '@grafana/i18n': grafanaI18nPlugin },
    rules: {
      '@grafana/i18n/no-untranslated-strings': ['error', { calleesToIgnore: ['^css$', 'use[A-Z].*'] }],
      '@grafana/i18n/no-translation-top-level': 'error',
      '@grafana/i18n/t-plural-defaults': 'error',
      '@grafana/i18n/trans-plural-defaults': 'error',
    },
  },
]);

```

You can find more detailed description of the rules [here](https://github.com/grafana/grafana/blob/main/packages/grafana-i18n/src/eslint/README.md).
