# Translate your plugin before Grafana 12.1.0

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

This is for plugins that need to support Grafana versions >= 11.0.0 and use translations. If your plugin only needs to support Grafana 12.1.0 and later, then follow the steps in [Translate your plugin](https://grafana.com/developers/plugin-tools/how-to-guides/plugin-internationalization.md) instead. If you're using older versions of Grafana, the plugin will not work.

## 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`
* `loadResources.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
│   ├── loadResources.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. Your `loadResources` function is not invoked for `en-US` either, but it continues to be called for every other language listed in `plugin.json#languages`. 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.

### Make Grafana 11.0.0 your default image version[​](#make-grafana-1100-your-default-image-version "Direct link to Make Grafana 11.0.0 your default image version")

To do so, update `docker-compose.yaml` in your plugin with the correct `grafana_version`:

docker-compose.yaml

```yaml
services:
  grafana:
    extends:
      file: .config/docker-compose-base.yaml
      service: grafana
    build:
      args:
        grafana_version: ${GRAFANA_VERSION:-11.0.0}

```

### 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": ">=11.0.0",
    "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

```

### Update `semver` to a regular dependency[​](#update-semver-to-a-regular-dependency "Direct link to update-semver-to-a-regular-dependency")

Update the semver package to enable version-based behavior toggling:

* npm
* Yarn
* pnpm

```shell
npm uninstall semver
npm install --save semver
npm install --save-dev @types/semver

```

```shell
yarn remove semver
yarn add semver
yarn add --dev @types/semver

```

```shell
pnpm remove semver
pnpm add semver
pnpm add --save-dev @types/semver

```

### Add `loadResources` file[​](#add-loadresources-file "Direct link to add-loadresources-file")

To handle translation resource loading let's add `src/loadResources.ts`

src/loadResources.ts

```ts
import { LANGUAGES, ResourceLoader, Resources } from '@grafana/i18n';
import pluginJson from 'plugin.json';

const resources = LANGUAGES.reduce<Record<string, () => Promise<{ default: Resources }>>>((acc, lang) => {
  acc[lang.code] = async () => await import(`./locales/${lang.code}/${pluginJson.id}.json`);
  return acc;
}, {});

export const loadResources: ResourceLoader = async (resolvedLanguage: string) => {
  try {
    const translation = await resources[resolvedLanguage]();
    return translation.default;
  } catch (error) {
    // This makes sure that the plugin doesn't crash when the resolved language in Grafana isn't supported by the plugin
    console.error(`The plugin '${pluginJson.id}' doesn't support the language '${resolvedLanguage}'`, error);
    return {};
  }
};

```

### 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 and loaders logic to `module.ts`:

module.ts

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

// Before Grafana version 12.1.0 the plugin is responsible for loading translation resources
// In Grafana version 12.1.0 and later Grafana is responsible for loading translation resources
const loaders = semver.lt(config?.buildInfo?.version, '12.1.0') ? [loadResources] : [];

await initPluginTranslations(pluginJson.id, loaders);

```

### 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 { config } from '@grafana/runtime';
import semver from 'semver';
import { loadResources } from './loadResources';
import { loadResources as ScenesResources } from '@grafana/scenes';

// Before Grafana version 12.1.0 the plugin is responsible for loading translation resources
// In Grafana version 12.1.0 and later Grafana is responsible for loading translation resources
const loaders = semver.lt(config?.buildInfo?.version, '12.1.0') ? [loadResources, ScenesResources] : [ScenesResources];

await initPluginTranslations(pluginJson.id, loaders);

```

## 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).
