Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .config/.cprc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"version": "6.7.8",
"version": "6.8.4",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auto updated when running npx @grafana/create-plugin@latest update

"features": {}
}
33 changes: 33 additions & 0 deletions .github/workflows/call_verify-i18n.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Verify i18n Translations

on:
workflow_call:

permissions:
contents: read

jobs:
verify-i18n:
name: Verify i18n Translations
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5

- name: Setup Plugin Environment
uses: ./.github/actions/setup-env

- name: Extract translation keys
run: yarn i18n-extract

- name: Check locale files are in sync
run: |
echo "🔍 Checking if locale files are in sync..."
if ! git diff --exit-code src/locales/; then
echo ""
echo "❌ Locale files are out of sync."
echo "Run 'yarn i18n-extract' locally and commit the changes."
exit 1
else
echo "✅ Locale files are in sync."
fi
7 changes: 7 additions & 0 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ jobs:
pull-requests: write
uses: ./.github/workflows/call_verify-probe-api-server-mappings.yml

# i18n translation files sync check
i18n-validation:
name: i18n Validation
permissions:
contents: read
uses: ./.github/workflows/call_verify-i18n.yml

# Grafana API compatibility check
grafana-compat:
name: Grafana API Compatibility
Expand Down
148 changes: 148 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,152 @@ We use the file nesting feature to help manage the growing number of files in th
}
```

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the best place to put this documentation?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a good location for it.
Just a heads up that the developer-facing stuff here is solid and will stick around (how to use t()/Trans, key naming, best practices). But the workflow sections (extraction, adding languages, file structure) will probably need a rewrite once we get Crowdin set up in Phase 2.

## Contributing to translations

This project uses [@grafana/i18n](https://www.npmjs.com/package/@grafana/i18n) for internationalization support. We use [i18next-cli](https://www.npmjs.com/package/i18next-cli) to manage translations across multiple languages.

### Supported languages

The list of supported languages is configured in `src/plugin.json` under the `languages` array. Currently supported languages:

- `en-US` (English - United States)
- `es-ES` (Spanish - Spain)

### Adding translatable text to components

There are two ways to add translatable text depending on your use case:

#### Using the `t` function for simple strings

Import the `t` function from `@grafana/i18n` and use it for simple text strings:

```tsx
import { t } from '@grafana/i18n';

const buttonLabel = t('componentName.buttonLabel', 'Default text');
```

**Example:**

```tsx
const activeFiltersText = t('checkFilterGroup.activeFiltersText', '({{activeFilters}} active)', {
activeFilters: activeFilters.length
});
```

#### Using the `Trans` component for JSX content

Import the `Trans` component from `@grafana/i18n` when you need to translate JSX elements:

```tsx
import { Trans } from '@grafana/i18n';

return (
<button>
<Trans i18nKey="componentName.buttonText">
Default button text
</Trans>
</button>
);
```

**Example:**

```tsx
<LinkButton>
<Trans i18nKey="checks.numberOfChecks">
Number of checks: {{ numOfChecks: checks.length }}
</Trans>
</LinkButton>
```

### Translation key naming conventions

Translation keys follow a hierarchical structure:

```
componentName.specificKey
```

- Use camelCase for all parts of the key
- The first part should be the component name (e.g., `checkFilterGroup`, `addNewCheckButton`)
- The second part should describe the specific text (e.g., `createNewCheck`, `activeFiltersText`)
- For nested UI elements, you can add more levels (e.g., `checkList.header.sortOptions.ascExecutions`)

### Extracting translations

After adding new translatable text to the codebase, you need to extract the translation keys to the locale files:

```bash
yarn i18n-extract
```

This command:

1. Scans all TypeScript/TSX files in the `src` directory
2. Extracts translation keys from `t()` function calls and `Trans` components
3. Updates all language files in `src/locales/[language]/grafana-synthetic-monitoring-app.json`
4. Synchronizes the primary language (en-US) and ensures all languages have the same keys

### Translation file structure

Translation files are organized by language code:

```
src/locales/
├── en-US/
│ └── grafana-synthetic-monitoring-app.json
└── es-ES/
└── grafana-synthetic-monitoring-app.json
```

Each translation file uses a nested JSON structure organized by component:

```json
{
"componentName": {
"keyName": "Translated text",
"keyWithVariable": "Text with {{variable}}"
}
}
```

### Adding a new language

To add support for a new language:

1. Add the language code to the `languages` array in `src/plugin.json`:

```json
"languages": ["en-US", "es-ES", "fr-FR"]
```

2. Run the extraction command to generate the translation file:

```bash
yarn i18n-extract
```

3. A new directory will be created at `src/locales/fr-FR/` with a JSON file containing all the translation keys
4. Translate the values in the new language file

### Configuration

The i18next configuration is defined in `i18next.config.ts`:

- **locales**: Reads the language list from `src/plugin.json`
- **input**: Scans `src/**/*.{tsx,ts}` for translation keys
- **output**: Generates files in `src/locales/{{language}}/{{namespace}}.json`
- **defaultNS**: Uses the plugin ID as the namespace
- **functions**: Extracts from `t` and `*.t` function calls
- **transComponents**: Extracts from `Trans` components

### Best practices

- Always provide a default English text as the second parameter to the `t()` function
- Use descriptive, hierarchical keys in camelCase that reflect the component structure
- Run `yarn i18n-extract` after adding or modifying translatable text
- Keep translation keys focused and specific to avoid reuse conflicts
- For text with variables, use clear variable names (e.g., `{{activeFilters}}` not `{{count}}`)
- Test your changes with different languages enabled in Grafana

2 changes: 2 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ services:
- ./dev/custom.ini:/etc/grafana/grafana.ini
- ./dev/license.jwt:/var/lib/grafana/license.jwt
- grafana-storage:/var/lib/grafana
environment:
GF_FEATURE_TOGGLES_ENABLE: localizationForPlugins

volumes:
grafana-storage:
13 changes: 13 additions & 0 deletions i18next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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'],
},
});
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"dev": "webpack -w -c webpack.config.ts --env development",
"e2e:update": "yarn exec cypress install && yarn exec grafana-e2e run --update-screenshots",
"e2e": "yarn exec cypress install && yarn exec grafana-e2e run",
"i18n-extract": "i18next-cli extract --sync-primary",
"lint:dashboards": "./scripts/lint-dashboards.sh",
"lint:fix": "yarn run lint --fix",
"lint": "eslint --cache src/",
Expand Down Expand Up @@ -76,6 +77,7 @@
"fork-ts-checker-webpack-plugin": "9.1.0",
"glob": "12.0.0",
"husky": "9.1.7",
"i18next-cli": "^1.42.8",
"identity-obj-proxy": "3.0.0",
"imports-loader": "5.0.0",
"jest": "30.2.0",
Expand Down
5 changes: 4 additions & 1 deletion src/components/AddNewCheckButton/AddNewCheckButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useCallback } from 'react';
import { Trans } from '@grafana/i18n';
import { LinkButton } from '@grafana/ui';
import { trackAddNewCheckButtonClicked } from 'features/tracking/checkCreationEvents';
import { ACTIONS_TEST_ID } from 'test/dataTestIds';
Expand Down Expand Up @@ -27,7 +28,9 @@ export function AddNewCheckButton({ source }: AddNewCheckButtonProps) {
onClick={handleClick}
variant="primary"
>
Create new check
<Trans i18nKey="addNewCheckButton.createNewCheck">
Create new check
</Trans>
</LinkButton>
);
}
25 changes: 0 additions & 25 deletions src/components/ScenesProvider.tsx

This file was deleted.

41 changes: 41 additions & 0 deletions src/locales/en-US/grafana-synthetic-monitoring-app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"addNewCheckButton": {
"createNewCheck": "Create new check"
},
"checkFilterGroup": {
"activeFiltersText": "({{activeFilters}} active)",
"filterTitle": "Additional filters {{activeFiltersText}}"
},
"checkFilters": {
"alerts": "Alerts",
"all": "All",
"allProbes": "All probes",
"filterByAlertsAriaLabel": "Filter by alerts",
"filterByStatusAriaLabel": "Filter by status",
"filterByTypeAriaLabel": "Filter by type",
"probes": "Probes",
"searchChecksAriaLabel": "Search checks",
"searchPlaceholder": "Search by job name, endpoint, or label",
"type": "Type",
"withAlerts": "With alerts",
"withoutAlerts": "Without alerts"
},
"checkList": {
"header": {
"currentlyShowing": "Currently showing {{currentPageChecksLength}} of {{checksLength}} total checks",
"deselectAll": "Deselect all",
"selectAll": "Select all",
"selectAllAriaLabel": "Select all",
"setThresholds": "Set Thresholds",
"sortOptions": {
"ascExecutions": "Asc. Executions",
"ascReachability": "Asc. Reachability",
"descExecutions": "Desc. Executions",
"descReachability": "Desc. Reachability"
}
}
},
"labelFilterInput": {
"buttonCascaderLabel": "Labels"
}
}
41 changes: 41 additions & 0 deletions src/locales/es-ES/grafana-synthetic-monitoring-app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"addNewCheckButton": {
"createNewCheck": "Crear nueva prueba"
},
"checkFilterGroup": {
"activeFiltersText": "({{activeFilters}} activo)",
"filterTitle": "Filtros adicionales {{activeFiltersText}}"
},
"checkFilters": {
"alerts": "Alertas",
"all": "Todos",
"allProbes": "Todos los agentes",
"filterByAlertsAriaLabel": "Filtrar por alertas",
"filterByStatusAriaLabel": "Filtrar por estado",
"filterByTypeAriaLabel": "Filtrar por tipo",
"probes": "Agentes",
"searchChecksAriaLabel": "Buscar pruebas",
"searchPlaceholder": "Buscar por nombre del job, endpoint o etiqueta",
"type": "Tipo",
"withAlerts": "Con alertas",
"withoutAlerts": "Sin alertas"
},
"checkList": {
"header": {
"currentlyShowing": "Mostrando {{currentPageChecksLength}} de {{checksLength}} pruebas",
"deselectAll": "Deseleccionar todos",
"selectAll": "Seleccionar todos",
"selectAllAriaLabel": "Seleccionar todos",
"setThresholds": "Definir umbrales",
"sortOptions": {
"ascExecutions": "Ejecuciones asc.",
"ascReachability": "Accesibilidad asc.",
"descExecutions": "Ejecuciones desc.",
"descReachability": "Accesibilidad desc."
}
}
},
"labelFilterInput": {
"buttonCascaderLabel": "Etiquetas"
}
}
Loading