i18n: Use grafana/i18n to init the locale and manage the regionalFormat (#105281)
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>;
|
||||
|
||||
@@ -24,3 +24,4 @@ export {
|
||||
} from './constants';
|
||||
export { initPluginTranslations, Trans, useTranslate } from './i18n';
|
||||
export type { TransProps } from './types';
|
||||
export { formatDate, formatDuration, formatDateRange } from './dates';
|
||||
|
||||
Vendored
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
|
||||
Vendored
+13
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user