I18n: Add utility for locale-aware duration formatting (#89351)
* I18n: Add utility for locale-aware duration formatting * fix message * add fallback
This commit is contained in:
@@ -244,6 +244,7 @@
|
||||
"@emotion/react": "11.11.4",
|
||||
"@fingerprintjs/fingerprintjs": "^3.4.2",
|
||||
"@floating-ui/react": "0.26.16",
|
||||
"@formatjs/intl-durationformat": "^0.2.4",
|
||||
"@glideapps/glide-data-grid": "^6.0.0",
|
||||
"@grafana/aws-sdk": "0.3.3",
|
||||
"@grafana/azure-sdk": "0.0.3",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import '@formatjs/intl-durationformat/polyfill';
|
||||
|
||||
import { getI18next } from './index';
|
||||
|
||||
export function formatDate(value: number | Date | string, format: Intl.DateTimeFormatOptions = {}): string {
|
||||
if (typeof value === 'string') {
|
||||
return formatDate(new Date(value), format);
|
||||
}
|
||||
|
||||
const i18n = getI18next();
|
||||
const dateFormatter = new Intl.DateTimeFormat(i18n.language, format);
|
||||
return dateFormatter.format(value);
|
||||
}
|
||||
|
||||
export function formatDuration(duration: Intl.DurationInput, options: Intl.DurationFormatOptions = {}) {
|
||||
const i18n = getI18next();
|
||||
|
||||
const dateFormatter = new Intl.DurationFormat(i18n.language, options);
|
||||
return dateFormatter.format(duration);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { DEFAULT_LANGUAGE, NAMESPACES, VALID_LANGUAGES } from './constants';
|
||||
import { loadTranslations } from './loadTranslations';
|
||||
|
||||
let tFunc: TFunction<string[], undefined> | undefined;
|
||||
let i18nInstance: typeof i18n;
|
||||
|
||||
export async function initializeI18n(language: string): Promise<{ language: string | undefined }> {
|
||||
// This is a placeholder so we can put a 'comment' in the message json files.
|
||||
@@ -28,7 +29,7 @@ export async function initializeI18n(language: string): Promise<{ language: stri
|
||||
ns: NAMESPACES,
|
||||
};
|
||||
|
||||
let i18nInstance = i18n;
|
||||
i18nInstance = i18n;
|
||||
if (language === 'detect') {
|
||||
i18nInstance = i18nInstance.use(LanguageDetector);
|
||||
const detection: DetectorOptions = { order: ['navigator'], caches: [] };
|
||||
@@ -79,10 +80,20 @@ export const t = (id: string, defaultMessage: string, values?: Record<string, un
|
||||
return tFunc(id, defaultMessage, values);
|
||||
};
|
||||
|
||||
export const i18nDate = (value: number | Date | string, format: Intl.DateTimeFormatOptions = {}): string => {
|
||||
if (typeof value === 'string') {
|
||||
return i18nDate(new Date(value), format);
|
||||
export function getI18next() {
|
||||
if (!tFunc) {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.warn(
|
||||
'An attempt to internationalize was made before it was initialized. This was probably caused by calling a locale-aware function in the root module scope, instead of in render'
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
throw new Error('getI18next was called before i18n was initialized');
|
||||
}
|
||||
|
||||
return i18n;
|
||||
}
|
||||
const dateFormatter = new Intl.DateTimeFormat(i18n.language, format);
|
||||
return dateFormatter.format(value);
|
||||
};
|
||||
|
||||
return i18nInstance || i18n;
|
||||
}
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
|
||||
|
||||
import { ConfirmButton, ConfirmModal, Button, Stack } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { i18nDate } from 'app/core/internationalization';
|
||||
import { formatDate } from 'app/core/internationalization/dates';
|
||||
import { AccessControlAction, UserSession } from 'app/types';
|
||||
|
||||
interface Props {
|
||||
@@ -68,7 +68,7 @@ class BaseUserSessions extends PureComponent<Props, State> {
|
||||
sessions.map((session, index) => (
|
||||
<tr key={`${session.id}-${index}`}>
|
||||
<td>{session.isActive ? 'Now' : session.seenAt}</td>
|
||||
<td>{i18nDate(session.createdAt, { dateStyle: 'long' })}</td>
|
||||
<td>{formatDate(session.createdAt, { dateStyle: 'long' })}</td>
|
||||
<td>{session.clientIp}</td>
|
||||
<td>{`${session.browser} on ${session.os} ${session.osVersion}`}</td>
|
||||
<td>
|
||||
|
||||
@@ -4,7 +4,8 @@ import React, { PureComponent } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Button, Icon, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { i18nDate, Trans } from 'app/core/internationalization';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { formatDate } from 'app/core/internationalization/dates';
|
||||
import { UserSession } from 'app/types';
|
||||
|
||||
interface Props {
|
||||
@@ -50,7 +51,7 @@ class UserSessions extends PureComponent<Props> {
|
||||
{sessions.map((session: UserSession, index) => (
|
||||
<tr key={index}>
|
||||
{session.isActive ? <td>Now</td> : <td>{session.seenAt}</td>}
|
||||
<td>{i18nDate(session.createdAt, { dateStyle: 'long' })}</td>
|
||||
<td>{formatDate(session.createdAt, { dateStyle: 'long' })}</td>
|
||||
<td>{session.clientIp}</td>
|
||||
<td>
|
||||
{session.browser} on {session.os} {session.osVersion}
|
||||
|
||||
@@ -2503,6 +2503,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/ecma402-abstract@npm:2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "@formatjs/ecma402-abstract@npm:2.0.0"
|
||||
dependencies:
|
||||
"@formatjs/intl-localematcher": "npm:0.5.4"
|
||||
tslib: "npm:^2.4.0"
|
||||
checksum: 10/41543ba509ea3c7d6530d57b888115f7ca242f13462a951fae4d1d1f28bae10c999f4dea28a71d2f08366d4889a3f5276cae3a16c6f6417b841a84fd314c2234
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/fast-memoize@npm:1.2.6":
|
||||
version: 1.2.6
|
||||
resolution: "@formatjs/fast-memoize@npm:1.2.6"
|
||||
@@ -2533,6 +2543,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-durationformat@npm:^0.2.4":
|
||||
version: 0.2.4
|
||||
resolution: "@formatjs/intl-durationformat@npm:0.2.4"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:2.0.0"
|
||||
"@formatjs/intl-localematcher": "npm:0.5.4"
|
||||
tslib: "npm:^2.4.0"
|
||||
checksum: 10/5f500409a20d18967e17ffbc222f9b4c4bf7ef08cce20023c33f06d1989c2bc4cf700d1dd1d048748d0a36c882109d5375896a4964d6700f73ec18914c6de4ba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-localematcher@npm:0.2.31":
|
||||
version: 0.2.31
|
||||
resolution: "@formatjs/intl-localematcher@npm:0.2.31"
|
||||
@@ -2542,6 +2563,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-localematcher@npm:0.5.4":
|
||||
version: 0.5.4
|
||||
resolution: "@formatjs/intl-localematcher@npm:0.5.4"
|
||||
dependencies:
|
||||
tslib: "npm:^2.4.0"
|
||||
checksum: 10/780cb29b42e1ea87f2eb5db268577fcdc53da52d9f096871f3a1bb78603b4ba81d208ea0b0b9bc21548797c941ce435321f62d2522795b83b740f90b0ceb5778
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@gar/promisify@npm:^1.0.1":
|
||||
version: 1.1.3
|
||||
resolution: "@gar/promisify@npm:1.1.3"
|
||||
@@ -17036,6 +17066,7 @@ __metadata:
|
||||
"@emotion/react": "npm:11.11.4"
|
||||
"@fingerprintjs/fingerprintjs": "npm:^3.4.2"
|
||||
"@floating-ui/react": "npm:0.26.16"
|
||||
"@formatjs/intl-durationformat": "npm:^0.2.4"
|
||||
"@glideapps/glide-data-grid": "npm:^6.0.0"
|
||||
"@grafana/aws-sdk": "npm:0.3.3"
|
||||
"@grafana/azure-sdk": "npm:0.0.3"
|
||||
|
||||
Reference in New Issue
Block a user