-
Notifications
You must be signed in to change notification settings - Fork 21
feat: add i18n support (Phase 1) #1589
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
feb944b
badf150
cad3b07
2c79125
394df11
bf3813c
62e5f3c
3bd36b9
652e5aa
cdef282
8945144
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| { | ||
| "version": "6.7.8", | ||
| "version": "6.8.4", | ||
| "features": {} | ||
| } | ||
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,4 +75,152 @@ We use the file nesting feature to help manage the growing number of files in th | |
| } | ||
| ``` | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this the best place to put this documentation?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that's a good location for it. |
||
| ## 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 | ||
|
|
||
| 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'], | ||
| }, | ||
| }); |
This file was deleted.
| 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" | ||
| } | ||
| } |
| 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" | ||
| } | ||
| } |
There was a problem hiding this comment.
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