I18n: Add new language options (#101899)

* Add newLanguages feature toggle

* add additional languages behind feature toggle

* be more forgiving of what config looks like in tests

* tweak regex

* put pt-br back

* restore order of pt-BR and cn-Hans, rename EXTRA_LANGUAGES to NEW_LANGUAGES

* stricter test regex
pull/101723/head
Josh Hunt 4 months ago committed by GitHub
parent da25d97ffd
commit 210c886bb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
  2. 1
      packages/grafana-data/src/types/featureToggles.gen.ts
  3. 7
      pkg/services/featuremgmt/registry.go
  4. 1
      pkg/services/featuremgmt/toggles_gen.csv
  5. 4
      pkg/services/featuremgmt/toggles_gen.go
  6. 13
      pkg/services/featuremgmt/toggles_gen.json
  7. 30
      public/app/core/internationalization/constants.test.ts
  8. 127
      public/app/core/internationalization/constants.ts

@ -227,6 +227,7 @@ Experimental features might be changed or removed without prior notice.
| `newLogsPanel` | Enables the new logs panel in Explore |
| `pluginsCDNSyncLoader` | Load plugins from CDN synchronously |
| `assetSriChecks` | Enables SRI checks for Grafana JavaScript assets |
| `extraLanguages` | Enables additional languages |
## Development feature toggles

@ -258,4 +258,5 @@ export interface FeatureToggles {
alertRuleRestore?: boolean;
grafanaManagedRecordingRulesDatasources?: boolean;
inviteUserExperimental?: boolean;
extraLanguages?: boolean;
}

@ -1812,6 +1812,13 @@ var (
HideFromDocs: true,
FrontendOnly: true,
},
{
Name: "extraLanguages",
Description: "Enables additional languages",
Stage: FeatureStageExperimental,
Owner: grafanaFrontendPlatformSquad,
FrontendOnly: true,
},
}
)

@ -239,3 +239,4 @@ assetSriChecks,experimental,@grafana/frontend-ops,false,false,true
alertRuleRestore,preview,@grafana/alerting-squad,false,false,false
grafanaManagedRecordingRulesDatasources,experimental,@grafana/alerting-squad,false,false,false
inviteUserExperimental,experimental,@grafana/sharing-squad,false,false,true
extraLanguages,experimental,@grafana/grafana-frontend-platform,false,false,true

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
239 alertRuleRestore preview @grafana/alerting-squad false false false
240 grafanaManagedRecordingRulesDatasources experimental @grafana/alerting-squad false false false
241 inviteUserExperimental experimental @grafana/sharing-squad false false true
242 extraLanguages experimental @grafana/grafana-frontend-platform false false true

@ -966,4 +966,8 @@ const (
// FlagInviteUserExperimental
// Renders invite user button along the app
FlagInviteUserExperimental = "inviteUserExperimental"
// FlagExtraLanguages
// Enables additional languages
FlagExtraLanguages = "extraLanguages"
)

@ -1713,6 +1713,19 @@
"hideFromAdminPage": true
}
},
{
"metadata": {
"name": "extraLanguages",
"resourceVersion": "1741626708204",
"creationTimestamp": "2025-03-10T17:11:48Z"
},
"spec": {
"description": "Enables additional languages",
"stage": "experimental",
"codeowner": "@grafana/grafana-frontend-platform",
"frontend": true
}
},
{
"metadata": {
"name": "extraThemes",

@ -1,30 +1,8 @@
import { uniqBy } from 'lodash';
import {
BRAZILIAN_PORTUGUESE,
CHINESE_SIMPLIFIED,
DEFAULT_LANGUAGE,
ENGLISH_US,
FRENCH_FRANCE,
GERMAN_GERMANY,
LANGUAGES,
PSEUDO_LOCALE,
SPANISH_SPAIN,
VALID_LANGUAGES,
} from './constants';
import { LANGUAGES, VALID_LANGUAGES } from './constants';
describe('internationalization constants', () => {
it('should have set the constants correctly', () => {
expect(ENGLISH_US).toBe('en-US');
expect(FRENCH_FRANCE).toBe('fr-FR');
expect(SPANISH_SPAIN).toBe('es-ES');
expect(GERMAN_GERMANY).toBe('de-DE');
expect(BRAZILIAN_PORTUGUESE).toBe('pt-BR');
expect(CHINESE_SIMPLIFIED).toBe('zh-Hans');
expect(PSEUDO_LOCALE).toBe('pseudo');
expect(DEFAULT_LANGUAGE).toBe(ENGLISH_US);
});
it('should match a canonical locale definition', () => {
for (const lang of LANGUAGES) {
const resolved = Intl.getCanonicalLocales(lang.code);
@ -32,6 +10,12 @@ describe('internationalization constants', () => {
}
});
it('should have locale codes including the country code', () => {
for (const lang of LANGUAGES) {
expect(lang.code).toMatch(/^[a-z]{2}-[a-zA-Z]+$/);
}
});
it('should not have duplicate languages codes', () => {
const uniqLocales = uniqBy(LANGUAGES, (v) => v.code);
expect(LANGUAGES).toHaveLength(uniqLocales.length);

@ -1,12 +1,28 @@
import { ResourceKey } from 'i18next';
import { uniq } from 'lodash';
import { config } from '@grafana/runtime';
export const ENGLISH_US = 'en-US';
export const FRENCH_FRANCE = 'fr-FR';
export const SPANISH_SPAIN = 'es-ES';
export const GERMAN_GERMANY = 'de-DE';
export const BRAZILIAN_PORTUGUESE = 'pt-BR';
export const CHINESE_SIMPLIFIED = 'zh-Hans';
export const ITALIAN_ITALY = 'it-IT';
export const JAPANESE_JAPAN = 'ja-JP';
export const INDONESIAN_INDONESIA = 'id-ID';
export const KOREAN_KOREA = 'ko-KR';
export const RUSSIAN_RUSSIA = 'ru-RU';
export const CZECH_CZECHIA = 'cs-CZ';
export const DUTCH_NETHERLANDS = 'nl-NL';
export const HUNGARIAN_HUNGARY = 'hu-HU';
export const PORTUGUESE_PORTUGAL = 'pt-PT';
export const POLISH_POLAND = 'pl-PL';
export const SWEDISH_SWEDEN = 'sv-SE';
export const TURKISH_TURKEY = 'tr-TR';
export const CHINESE_TRADITIONAL = 'zh-Hant';
export const PSEUDO_LOCALE = 'pseudo';
export const DEFAULT_LANGUAGE = ENGLISH_US;
@ -24,6 +40,113 @@ export interface LanguageDefinition<Namespace extends string = string> {
loader: Record<Namespace, LocaleFileLoader>;
}
// New languages added recently without translations available yet
const NEW_LANGUAGES: LanguageDefinition[] = [
{
code: CHINESE_TRADITIONAL,
name: '中文(繁體)',
loader: {
grafana: () => import('../../../locales/zh-Hant/grafana.json'),
},
},
{
code: ITALIAN_ITALY,
name: 'Italiano',
loader: {
grafana: () => import('../../../locales/it-IT/grafana.json'),
},
},
{
code: JAPANESE_JAPAN,
name: '日本語',
loader: {
grafana: () => import('../../../locales/ja-JP/grafana.json'),
},
},
{
code: INDONESIAN_INDONESIA,
name: 'Bahasa Indonesia',
loader: {
grafana: () => import('../../../locales/id-ID/grafana.json'),
},
},
{
code: KOREAN_KOREA,
name: '한국어',
loader: {
grafana: () => import('../../../locales/ko-KR/grafana.json'),
},
},
{
code: RUSSIAN_RUSSIA,
name: 'Русский',
loader: {
grafana: () => import('../../../locales/ru-RU/grafana.json'),
},
},
{
code: CZECH_CZECHIA,
name: 'Čeština',
loader: {
grafana: () => import('../../../locales/cs-CZ/grafana.json'),
},
},
{
code: DUTCH_NETHERLANDS,
name: 'Nederlands',
loader: {
grafana: () => import('../../../locales/nl-NL/grafana.json'),
},
},
{
code: HUNGARIAN_HUNGARY,
name: 'Magyar',
loader: {
grafana: () => import('../../../locales/hu-HU/grafana.json'),
},
},
{
code: PORTUGUESE_PORTUGAL,
name: 'Português',
loader: {
grafana: () => import('../../../locales/pt-PT/grafana.json'),
},
},
{
code: POLISH_POLAND,
name: 'Polski',
loader: {
grafana: () => import('../../../locales/pl-PL/grafana.json'),
},
},
{
code: SWEDISH_SWEDEN,
name: 'Svenska',
loader: {
grafana: () => import('../../../locales/sv-SE/grafana.json'),
},
},
{
code: TURKISH_TURKEY,
name: 'Türkçe',
loader: {
grafana: () => import('../../../locales/tr-TR/grafana.json'),
},
},
];
export const LANGUAGES: LanguageDefinition[] = [
{
code: ENGLISH_US,
@ -74,6 +197,10 @@ export const LANGUAGES: LanguageDefinition[] = [
},
] satisfies Array<LanguageDefinition<'grafana'>>;
if (config.featureToggles?.extraLanguages) {
LANGUAGES.push(...NEW_LANGUAGES);
}
if (process.env.NODE_ENV === 'development') {
LANGUAGES.push({
code: PSEUDO_LOCALE,

Loading…
Cancel
Save