i18n: Use grafana/i18n to init the locale and manage the regionalFormat (#105281)

This commit is contained in:
Laura Fernández
2025-05-19 13:24:33 +02:00
committed by GitHub
parent 8b78b604ca
commit 23ecb9d904
12 changed files with 116 additions and 9 deletions
+3
View File
@@ -49,9 +49,12 @@
"postpack": "mv package.json.bak package.json"
},
"dependencies": {
"@formatjs/intl-durationformat": "^0.7.0",
"fast-deep-equal": "^3.1.3",
"i18next": "^24.0.0",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-pseudo": "^2.2.1",
"micro-memoize": "^4.1.2",
"react-i18next": "^15.0.0"
},
"devDependencies": {
+45
View File
@@ -0,0 +1,45 @@
import deepEqual from 'fast-deep-equal';
import memoize from 'micro-memoize';
const deepMemoize: typeof memoize = (fn) => memoize(fn, { isEqual: deepEqual });
let regionalFormat: string | undefined;
const createDateTimeFormatter = deepMemoize((locale: string | undefined, options: Intl.DateTimeFormatOptions) => {
return new Intl.DateTimeFormat(locale, options);
});
const createDurationFormatter = deepMemoize((locale: string | undefined, options: Intl.DurationFormatOptions) => {
return new Intl.DurationFormat(locale, options);
});
export const formatDate = deepMemoize(
(_value: number | Date | string, format: Intl.DateTimeFormatOptions = {}): string => {
const value = typeof _value === 'string' ? new Date(_value) : _value;
const dateFormatter = createDateTimeFormatter(regionalFormat, format);
return dateFormatter.format(value);
}
);
export const formatDuration = deepMemoize(
(duration: Intl.DurationInput, options: Intl.DurationFormatOptions = {}): string => {
const dateFormatter = createDurationFormatter(regionalFormat, options);
return dateFormatter.format(duration);
}
);
export const formatDateRange = (
_from: number | Date | string,
_to: number | Date | string,
format: Intl.DateTimeFormatOptions = {}
): string => {
const from = typeof _from === 'string' ? new Date(_from) : _from;
const to = typeof _to === 'string' ? new Date(_to) : _to;
const dateFormatter = createDateTimeFormatter(regionalFormat, format);
return dateFormatter.formatRange(from, to);
};
export const initRegionalFormat = (regionalFormatArg: string) => {
regionalFormat = regionalFormatArg;
};
+9
View File
@@ -4,6 +4,7 @@ import LanguageDetector, { DetectorOptions } from 'i18next-browser-languagedetec
import { initReactI18next, setDefaults, setI18n, Trans as I18NextTrans, getI18n } from 'react-i18next';
import { DEFAULT_LANGUAGE, PSEUDO_LOCALE } from './constants';
import { initRegionalFormat } from './dates';
import { LANGUAGES } from './languages';
import { TransProps, TransType } from './types';
@@ -123,6 +124,14 @@ export async function changeLanguage(language?: string) {
await getI18nInstance().changeLanguage(validLanguage);
}
export async function initializeI18n(
{ language, ns, module }: InitializeI18nOptions,
regionalFormat: string
): Promise<{ language: string | undefined }> {
initRegionalFormat(regionalFormat);
return initTranslations({ language, ns, module });
}
type ResourceKey = string;
type ResourceLanguage = Record<string, ResourceKey>;
type ResourceType = Record<string, ResourceLanguage>;
+1
View File
@@ -24,3 +24,4 @@ export {
} from './constants';
export { initPluginTranslations, Trans, useTranslate } from './i18n';
export type { TransProps } from './types';
export { formatDate, formatDuration, formatDateRange } from './dates';
+1
View File
@@ -69,6 +69,7 @@
"@grafana/data": "12.1.0-pre",
"@grafana/e2e-selectors": "12.1.0-pre",
"@grafana/faro-web-sdk": "^1.13.2",
"@grafana/i18n": "12.1.0-pre",
"@grafana/schema": "12.1.0-pre",
"@hello-pangea/dnd": "17.0.0",
"@leeoniya/ufuzzy": "1.0.18",
@@ -1,16 +1,40 @@
import { TimeOption, TimeRange, TimeZone, rangeUtil, dateTimeFormat } from '@grafana/data';
import { formatDateRange } from '@grafana/i18n';
import { getFeatureToggle } from '../../../utils/featureToggle';
export const mapOptionToTimeRange = (option: TimeOption, timeZone?: TimeZone): TimeRange => {
return rangeUtil.convertRawToRange({ from: option.from, to: option.to }, timeZone);
};
const rangeFormatShort: Intl.DateTimeFormatOptions = {
dateStyle: 'short',
timeStyle: 'short',
};
const rangeFormatFull: Intl.DateTimeFormatOptions = {
dateStyle: 'short',
timeStyle: 'medium',
};
export const mapRangeToTimeOption = (range: TimeRange, timeZone?: TimeZone): TimeOption => {
const from = dateTimeFormat(range.from, { timeZone });
const to = dateTimeFormat(range.to, { timeZone });
let display = `${from} to ${to}`;
if (getFeatureToggle('localeFormatPreference')) {
const fromDate = range.from.toDate();
const toDate = range.to.toDate();
// The short time format doesn't include seconds, so change the format
// if the range includes seconds
const hasSeconds = fromDate.getSeconds() !== 0 || toDate.getSeconds() !== 0;
display = formatDateRange(fromDate, toDate, hasSeconds ? rangeFormatFull : rangeFormatShort);
}
return {
from,
to,
display: `${from} to ${to}`,
display,
};
};
+1 -1
View File
@@ -15,7 +15,7 @@ type DB struct{}
// Stub out the QueryFrames method for ARM builds
// See github.com/dolthub/go-mysql-server/issues/2837
func (db *DB) QueryFrames(_ context.Context, _ tracing.Tracer, _, _ string, _ []*data.Frame, _...QueryOption) (*data.Frame, error) {
func (db *DB) QueryFrames(_ context.Context, _ tracing.Tracer, _, _ string, _ []*data.Frame, _ ...QueryOption) (*data.Frame, error) {
return nil, fmt.Errorf("sql expressions not supported in arm")
}
+14 -6
View File
@@ -1,6 +1,7 @@
import 'symbol-observable';
import 'regenerator-runtime/runtime';
import '@formatjs/intl-durationformat/polyfill';
import 'whatwg-fetch'; // fetch polyfill needed for PhantomJs rendering
import 'file-saver';
import 'jquery';
@@ -18,7 +19,6 @@ import {
standardFieldConfigEditorRegistry,
standardTransformersRegistry,
} from '@grafana/data';
import { initTranslations } from '@grafana/i18n/internal';
import {
locationService,
registerEchoBackend,
@@ -51,6 +51,7 @@ import {
import config, { updateConfig } from 'app/core/config';
import { getStandardTransformers } from 'app/features/transformers/standardTransformers';
import { initializeI18n } from '../../packages/grafana-i18n/src/i18n';
import getDefaultMonacoLanguages from '../lib/monaco-languages';
import { AppWrapper } from './AppWrapper';
@@ -125,15 +126,22 @@ export class GrafanaApp {
await preInitTasks();
// Let iframe container know grafana has started loading
window.parent.postMessage('GrafanaAppInit', '*');
const regionalFormat = config.featureToggles.localeFormatPreference
? config.locale
: config.bootData.user.language;
const initI18nPromise = initializeI18n(
{
language: config.bootData.user.language,
ns: NAMESPACES,
module: loadTranslations,
},
regionalFormat
);
// This is a placeholder so we can put a 'comment' in the message json files.
// Starts with an underscore so it's sorted to the top of the file. Even though it is in a comment the following line is still extracted
// t('_comment', 'The code is the source of truth for English phrases. They should be updated in the components directly, and additional plurals specified in this file.');
const initI18nPromise = initTranslations({
language: config.bootData.user.language,
ns: NAMESPACES,
module: loadTranslations,
});
initI18nPromise.then(({ language }) => updateConfig({ language }));
setBackendSrv(backendSrv);
@@ -1,4 +1,3 @@
import '@formatjs/intl-durationformat/polyfill';
import deepEqual from 'fast-deep-equal';
import memoize from 'micro-memoize';
+13
View File
@@ -0,0 +1,13 @@
import {
DurationFormatConstructor,
DurationFormatOptions as _DurationFormatOptions,
DurationInput as _DurationInput,
} from '@formatjs/intl-durationformat/src/types';
declare global {
namespace Intl {
const DurationFormat: DurationFormatConstructor;
type DurationFormatOptions = _DurationFormatOptions;
type DurationInput = _DurationInput;
}
}
+4
View File
@@ -3175,11 +3175,14 @@ __metadata:
version: 0.0.0-use.local
resolution: "@grafana/i18n@workspace:packages/grafana-i18n"
dependencies:
"@formatjs/intl-durationformat": "npm:^0.7.0"
"@grafana/tsconfig": "npm:^2.0.0"
"@types/react": "npm:18.3.18"
fast-deep-equal: "npm:^3.1.3"
i18next: "npm:^24.0.0"
i18next-browser-languagedetector: "npm:^8.0.0"
i18next-pseudo: "npm:^2.2.1"
micro-memoize: "npm:^4.1.2"
react-i18next: "npm:^15.0.0"
rollup: "npm:^4.22.4"
typescript: "npm:5.7.3"
@@ -3608,6 +3611,7 @@ __metadata:
"@grafana/data": "npm:12.1.0-pre"
"@grafana/e2e-selectors": "npm:12.1.0-pre"
"@grafana/faro-web-sdk": "npm:^1.13.2"
"@grafana/i18n": "npm:12.1.0-pre"
"@grafana/schema": "npm:12.1.0-pre"
"@grafana/tsconfig": "npm:^2.0.0"
"@hello-pangea/dnd": "npm:17.0.0"