From 50ff1ae69b354d0695f6cc51eefe27ab791bd8cf Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 13 Mar 2025 11:54:57 +0000 Subject: [PATCH] I18n: Add useT hook for plugin translation API (#102033) * Create placeholder useT function * expose useT() in @grafana/runtime * fix setting useT correctly * example usage of useT * rename hook to useTranslate * fix rename * remove comment --- .betterer.results | 6 +++++- packages/grafana-runtime/src/unstable.ts | 3 +-- packages/grafana-runtime/src/utils/i18n.ts | 21 +++++++++++++++++++ public/app/app.ts | 4 +++- .../app/core/internationalization/index.tsx | 10 +++++++++ .../app/features/bookmarks/BookmarksPage.tsx | 4 +++- 6 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 packages/grafana-runtime/src/utils/i18n.ts diff --git a/.betterer.results b/.betterer.results index 4358fc331ee..fad77bbf86c 100644 --- a/.betterer.results +++ b/.betterer.results @@ -887,7 +887,8 @@ exports[`better eslint`] = { "public/app/app.ts:5381": [ [0, 0, 0, "\'@grafana/runtime/src/components/PanelDataErrorView\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], [0, 0, 0, "\'@grafana/runtime/src/components/PanelRenderer\' import is restricted from being used by a pattern. Import from the public export instead.", "1"], - [0, 0, 0, "\'@grafana/runtime/src/components/PluginPage\' import is restricted from being used by a pattern. Import from the public export instead.", "2"] + [0, 0, 0, "\'@grafana/runtime/src/components/PluginPage\' import is restricted from being used by a pattern. Import from the public export instead.", "2"], + [0, 0, 0, "\'@grafana/runtime/src/unstable\' import is restricted from being used by a pattern. Import from the public export instead.", "3"] ], "public/app/core/TableModel.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -2883,6 +2884,9 @@ exports[`better eslint`] = { "public/app/features/auth-config/utils/data.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/app/features/bookmarks/BookmarksPage.tsx:5381": [ + [0, 0, 0, "\'@grafana/runtime/src/unstable\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] + ], "public/app/features/browse-dashboards/api/browseDashboardsAPI.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`@reduxjs/toolkit/query/react\`)", "0"] ], diff --git a/packages/grafana-runtime/src/unstable.ts b/packages/grafana-runtime/src/unstable.ts index 5cf9253a7c4..bc7633502aa 100644 --- a/packages/grafana-runtime/src/unstable.ts +++ b/packages/grafana-runtime/src/unstable.ts @@ -9,5 +9,4 @@ * and be subject to the standard policies */ -// Dummy export to make it a valid module. Remove when we have real exports. -export const unstable = {}; +export { useTranslate as useT, setUseTranslateHook } from './utils/i18n'; diff --git a/packages/grafana-runtime/src/utils/i18n.ts b/packages/grafana-runtime/src/utils/i18n.ts new file mode 100644 index 00000000000..eb1160197ab --- /dev/null +++ b/packages/grafana-runtime/src/utils/i18n.ts @@ -0,0 +1,21 @@ +type UseTranslateHook = () => (id: string, defaultMessage: string, values?: Record) => string; + +/** + * Provides a i18next-compatible translation function. + */ +export let useTranslate: UseTranslateHook = () => { + // Fallback implementation that should be overridden by setUseT + const errorMessage = 'useT is not set. useT must not be called before Grafana is initialized.'; + if (process.env.NODE_ENV === 'development') { + throw new Error(errorMessage); + } + + console.error(errorMessage); + return (id: string, defaultMessage: string) => { + return defaultMessage; + }; +}; + +export function setUseTranslateHook(useTParam: UseTranslateHook) { + useTranslate = useTParam; +} diff --git a/public/app/app.ts b/public/app/app.ts index f576ba415a0..f9e65818a3d 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -45,6 +45,7 @@ import { import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView'; import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer'; import { setPluginPage } from '@grafana/runtime/src/components/PluginPage'; +import { setUseTranslateHook } from '@grafana/runtime/src/unstable'; import config, { updateConfig } from 'app/core/config'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; @@ -58,7 +59,7 @@ import { getAllOptionEditors, getAllStandardFieldConfigs } from './core/componen import { PluginPage } from './core/components/Page/PluginPage'; import { GrafanaContextType, useChromeHeaderHeight, useReturnToPreviousInternal } from './core/context/GrafanaContext'; import { initializeCrashDetection } from './core/crash'; -import { initializeI18n } from './core/internationalization'; +import { initializeI18n, useTranslateInternal } from './core/internationalization'; import { setMonacoEnv } from './core/monacoEnv'; import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks'; import { CorrelationsService } from './core/services/CorrelationsService'; @@ -253,6 +254,7 @@ export class GrafanaApp { setReturnToPreviousHook(useReturnToPreviousInternal); setChromeHeaderHeightHook(useChromeHeaderHeight); + setUseTranslateHook(useTranslateInternal); if (config.featureToggles.crashDetection) { initializeCrashDetection(); diff --git a/public/app/core/internationalization/index.tsx b/public/app/core/internationalization/index.tsx index e3dbfb28270..538a643b9af 100644 --- a/public/app/core/internationalization/index.tsx +++ b/public/app/core/internationalization/index.tsx @@ -72,6 +72,8 @@ export const Trans = (props: TransProps): ReactElement => { * This is a simple wrapper over i18n.t() to provide default namespaces and enforce a consistent API. * Note: Don't use this in the top level module scope. This wrapper needs initialization, which is done during Grafana * startup, and it will throw if used before. + * + * This will soon be deprecated in favor of useT() * @param id ID of the translation string * @param defaultMessage Default message to use if the translation is missing * @param values Values to be interpolated into the string @@ -111,3 +113,11 @@ export function getI18next() { return i18nInstance || i18n; } + +// We want to move to a react-only API for translations. +// This hook doesn't do much now, but we want it to define the API for plugins. +// Perhaps in the future this will use useTranslation from react-i18next or something else +// from context +export function useTranslateInternal() { + return t; +} diff --git a/public/app/features/bookmarks/BookmarksPage.tsx b/public/app/features/bookmarks/BookmarksPage.tsx index 810baa17ed9..7d172b66d67 100644 --- a/public/app/features/bookmarks/BookmarksPage.tsx +++ b/public/app/features/bookmarks/BookmarksPage.tsx @@ -1,15 +1,17 @@ import { css } from '@emotion/css'; import { GrafanaTheme2, NavModelItem } from '@grafana/data'; +import { useT } from '@grafana/runtime/src/unstable'; import { EmptyState, useStyles2 } from '@grafana/ui'; import { usePinnedItems } from 'app/core/components/AppChrome/MegaMenu/hooks'; import { findByUrl } from 'app/core/components/AppChrome/MegaMenu/utils'; import { NavLandingPageCard } from 'app/core/components/NavLandingPage/NavLandingPageCard'; import { Page } from 'app/core/components/Page/Page'; -import { t, Trans } from 'app/core/internationalization'; +import { Trans } from 'app/core/internationalization'; import { useSelector } from 'app/types'; export function BookmarksPage() { + const t = useT(); const styles = useStyles2(getStyles); const pinnedItems = usePinnedItems(); const navTree = useSelector((state) => state.navBarTree);