diff --git a/packages/grafana-data/src/types/navModel.ts b/packages/grafana-data/src/types/navModel.ts index 63e3e7050f0..405924b4757 100644 --- a/packages/grafana-data/src/types/navModel.ts +++ b/packages/grafana-data/src/types/navModel.ts @@ -44,6 +44,7 @@ export interface NavModelItem extends NavLinkDTO { highlightId?: string; tabSuffix?: ComponentType<{ className?: string }>; hideFromBreadcrumbs?: boolean; + emptyMessage?: string; } export enum NavSection { diff --git a/public/app/app.ts b/public/app/app.ts index 8bbdd55eb1b..53c6e732dd1 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -123,6 +123,10 @@ export class GrafanaApp { setPanelDataErrorView(PanelDataErrorView); setLocationSrv(locationService); setTimeZoneResolver(() => config.bootData.user.timezone); + + // We must wait for translations to load because some preloaded store state requires translating + await initI18nPromise; + // Important that extension reducers are initialized before store addExtensionReducers(); configureStore(); @@ -177,12 +181,8 @@ export class GrafanaApp { const modalManager = new ModalManager(); modalManager.init(); - await Promise.all([ - initI18nPromise, - - // Preload selected app plugins - await preloadPlugins(config.apps), - ]); + // Preload selected app plugins + await preloadPlugins(config.apps); // initialize chrome service const queryParams = locationService.getSearchObject(); diff --git a/public/app/core/components/AppChrome/NavLandingPage.tsx b/public/app/core/components/AppChrome/NavLandingPage.tsx index 0143eebbaf0..75a069b5bb9 100644 --- a/public/app/core/components/AppChrome/NavLandingPage.tsx +++ b/public/app/core/components/AppChrome/NavLandingPage.tsx @@ -6,8 +6,6 @@ import { useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { useNavModel } from 'app/core/hooks/useNavModel'; -import { getNavTitle, getNavSubTitle } from '../NavBar/navBarItem-translations'; - import { NavLandingPageCard } from './NavLandingPageCard'; interface Props { @@ -28,8 +26,8 @@ export function NavLandingPage({ navId }: Props) { {children?.map((child) => ( ))} diff --git a/public/app/core/components/AppChrome/TopBar/TopNavBarMenu.tsx b/public/app/core/components/AppChrome/TopBar/TopNavBarMenu.tsx index 605e196f7ec..476251a9207 100644 --- a/public/app/core/components/AppChrome/TopBar/TopNavBarMenu.tsx +++ b/public/app/core/components/AppChrome/TopBar/TopNavBarMenu.tsx @@ -6,7 +6,6 @@ import { useLocation } from 'react-router-dom'; import { GrafanaTheme2, NavModelItem } from '@grafana/data'; import { Menu, MenuItem, useStyles2 } from '@grafana/ui'; -import { getNavTitle } from '../../NavBar/navBarItem-translations'; import { enrichConfigItems, enrichWithInteractionTracking } from '../../NavBar/utils'; export interface TopNavBarMenuProps { @@ -30,24 +29,23 @@ export function TopNavBarMenu({ node: nodePlain }: TopNavBarMenuProps) { // see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
e.stopPropagation()} className={styles.header}> -
{getNavTitle(node.id) ?? node.text}
+
{node.text}
{node.subTitle &&
{node.subTitle}
}
} > {node.children?.map((item) => { - const itemText = getNavTitle(item.id) ?? item.text; const showExternalLinkIcon = /^https?:\/\//.test(item.url || ''); return item.url ? ( ) : ( - + ); })} diff --git a/public/app/core/components/Breadcrumbs/utils.ts b/public/app/core/components/Breadcrumbs/utils.ts index 4fe8177a25f..ff3bd2ca806 100644 --- a/public/app/core/components/Breadcrumbs/utils.ts +++ b/public/app/core/components/Breadcrumbs/utils.ts @@ -1,7 +1,5 @@ import { NavModelItem } from '@grafana/data'; -import { getNavTitle } from '../NavBar/navBarItem-translations'; - import { Breadcrumb } from './types'; export function buildBreadcrumbs(sectionNav: NavModelItem, pageNav?: NavModelItem, homeNav?: NavModelItem) { @@ -19,10 +17,10 @@ export function buildBreadcrumbs(sectionNav: NavModelItem, pageNav?: NavModelIte if (!foundHome && !node.hideFromBreadcrumbs) { if (homeNav && urlToMatch === homeNav.url) { - crumbs.unshift({ text: getNavTitle(homeNav.id) ?? homeNav.text, href: node.url ?? '' }); + crumbs.unshift({ text: homeNav.text, href: node.url ?? '' }); foundHome = true; } else { - crumbs.unshift({ text: getNavTitle(node.id) ?? node.text, href: node.url ?? '' }); + crumbs.unshift({ text: node.text, href: node.url ?? '' }); } } diff --git a/public/app/core/components/MegaMenu/NavBarMenuItemWrapper.tsx b/public/app/core/components/MegaMenu/NavBarMenuItemWrapper.tsx index edfc7b8e4d1..7d76bb6be4c 100644 --- a/public/app/core/components/MegaMenu/NavBarMenuItemWrapper.tsx +++ b/public/app/core/components/MegaMenu/NavBarMenuItemWrapper.tsx @@ -4,7 +4,6 @@ import React from 'react'; import { GrafanaTheme2, NavModelItem } from '@grafana/data'; import { toIconName, useStyles2 } from '@grafana/ui'; -import { getNavTitle } from '../NavBar/navBarItem-translations'; import { isMatchOrChildMatch } from '../NavBar/utils'; import { NavBarMenuItem } from './NavBarMenuItem'; @@ -21,12 +20,11 @@ export function NavBarMenuItemWrapper({ }) { const styles = useStyles2(getStyles); - if (link.emptyMessageId && !linkHasChildren(link)) { - const emptyMessageTranslated = getNavTitle(link.emptyMessageId); + if (link.emptyMessage && !linkHasChildren(link)) { return (
    -
    {emptyMessageTranslated}
    +
    {link.emptyMessage}
); @@ -52,7 +50,7 @@ export function NavBarMenuItemWrapper({ target={childLink.target} url={childLink.url} > - {getNavTitle(childLink.id) ?? childLink.text} + {childLink.text} ) ); diff --git a/public/app/core/components/MegaMenu/NavBarMenuSection.tsx b/public/app/core/components/MegaMenu/NavBarMenuSection.tsx index 1e7e863c3b1..a5569873fc1 100644 --- a/public/app/core/components/MegaMenu/NavBarMenuSection.tsx +++ b/public/app/core/components/MegaMenu/NavBarMenuSection.tsx @@ -7,7 +7,6 @@ import { Button, Icon, useStyles2 } from '@grafana/ui'; import { NavBarItemIcon } from '../NavBar/NavBarItemIcon'; import { NavFeatureHighlight } from '../NavBar/NavFeatureHighlight'; -import { getNavTitle } from '../NavBar/navBarItem-translations'; import { hasChildMatch } from '../NavBar/utils'; import { NavBarMenuItem } from './NavBarMenuItem'; @@ -53,7 +52,7 @@ export function NavBarMenuSection({ - {getNavTitle(link.id) ?? link.text} + {link.text} {children && ( diff --git a/public/app/core/components/NavBar/NavBarItem.tsx b/public/app/core/components/NavBar/NavBarItem.tsx index c92405ed793..906840fb9d8 100644 --- a/public/app/core/components/NavBar/NavBarItem.tsx +++ b/public/app/core/components/NavBar/NavBarItem.tsx @@ -11,7 +11,6 @@ import { NavBarItemMenuTrigger } from './NavBarItemMenuTrigger'; import { getNavBarItemWithoutMenuStyles } from './NavBarItemWithoutMenu'; import { NavBarMenuItem } from './NavBarMenuItem'; import { useNavBarContext } from './context'; -import { getNavTitle } from './navBarItem-translations'; import { getNavModelItemKey } from './utils'; export interface Props { @@ -53,14 +52,12 @@ const NavBarItem = ({ isActive = false, className, reverseMenuDirection = false, } }; - const linkText = getNavTitle(link.id) ?? link.text; - return (
  • {(item: NavModelItem) => { - const itemText = getNavTitle(item.id) ?? item.text; const isSection = item.menuItemType === NavMenuItemType.Section; const iconName = item.icon ? toIconName(item.icon) : undefined; const icon = item.showIconInNavbar && !isSection ? iconName : undefined; @@ -83,7 +79,7 @@ const NavBarItem = ({ isActive = false, className, reverseMenuDirection = false, isDivider={!isSection && item.divider} icon={icon} target={item.target} - text={itemText} + text={item.text} url={item.url} onClick={item.onClick} styleOverrides={cx(styles.primaryText, { [styles.header]: isSection })} diff --git a/public/app/core/components/NavBar/NavBarItemMenu.tsx b/public/app/core/components/NavBar/NavBarItemMenu.tsx index 82a1d36e040..caeda7e2f8e 100644 --- a/public/app/core/components/NavBar/NavBarItemMenu.tsx +++ b/public/app/core/components/NavBar/NavBarItemMenu.tsx @@ -10,7 +10,6 @@ import { CustomScrollbar, useTheme2 } from '@grafana/ui'; import { NavBarItemMenuItem } from './NavBarItemMenuItem'; import { useNavBarItemMenuContext } from './context'; -import { getNavTitle } from './navBarItem-translations'; import { getNavModelItemKey } from './utils'; export interface NavBarItemMenuProps extends SpectrumMenuProps { @@ -58,11 +57,10 @@ export function NavBarItemMenu(props: NavBarItemMenuProps): ReactElement | null )); - if (itemComponents.length === 0 && section.value.emptyMessageId) { - const emptyMessageTranslated = getNavTitle(section.value.emptyMessageId); + if (itemComponents.length === 0 && section.value.emptyMessage) { itemComponents.push(
    - {emptyMessageTranslated} + {section.value.emptyMessage}
    ); } diff --git a/public/app/core/components/NavBar/NavBarMenu.tsx b/public/app/core/components/NavBar/NavBarMenu.tsx index ce3e58679c2..d39ab6a4680 100644 --- a/public/app/core/components/NavBar/NavBarMenu.tsx +++ b/public/app/core/components/NavBar/NavBarMenu.tsx @@ -15,7 +15,6 @@ import { NavBarItemWithoutMenu } from './NavBarItemWithoutMenu'; import { NavBarMenuItem } from './NavBarMenuItem'; import { NavBarToggle } from './NavBarToggle'; import { NavFeatureHighlight } from './NavFeatureHighlight'; -import { getNavTitle } from './navBarItem-translations'; import { isMatchOrChildMatch } from './utils'; const MENU_WIDTH = '350px'; @@ -256,7 +255,7 @@ export function NavItem({ }} styleOverrides={styles.item} target={childLink.target} - text={getNavTitle(childLink.id) ?? childLink.text} + text={childLink.text} url={childLink.url} isMobile={true} /> @@ -266,12 +265,11 @@ export function NavItem({ ); - } else if (link.emptyMessageId) { - const emptyMessageTranslated = getNavTitle(link.emptyMessageId); + } else if (link.emptyMessage) { return (
      -
      {emptyMessageTranslated}
      +
      {link.emptyMessage}
    ); @@ -297,7 +295,7 @@ export function NavItem({ - {getNavTitle(link.id) ?? link.text} + {link.text}
  • @@ -398,7 +396,7 @@ function CollapsibleNavItem({ contentClassName={styles.collapseContent} label={
    - {getNavTitle(link.id) ?? link.text} + {link.text}
    } > diff --git a/public/app/core/components/NavBar/navBarItem-translations.ts b/public/app/core/components/NavBar/navBarItem-translations.ts index c6fc386dce6..3ee3af1ef6b 100644 --- a/public/app/core/components/NavBar/navBarItem-translations.ts +++ b/public/app/core/components/NavBar/navBarItem-translations.ts @@ -1,10 +1,11 @@ +import { config } from '@grafana/runtime'; +import { t } from 'app/core/internationalization'; + // Maps the ID of the nav item to a translated phrase to later pass to // Because the navigation content is dynamic (defined in the backend), we can not use // the normal inline message definition method. -import { config } from '@grafana/runtime'; -import { t } from 'app/core/internationalization'; - +// see pkg/api/index.go export function getNavTitle(navId: string | undefined) { // the switch cases must match the ID of the navigation item, as defined in the backend nav model switch (navId) { @@ -38,6 +39,8 @@ export function getNavTitle(navId: string | undefined) { return t('nav.snapshots.title', 'Snapshots'); case 'dashboards/library-panels': return t('nav.library-panels.title', 'Library panels'); + case 'dashboards/public': + return t('nav.public.title', 'Public dashboards'); case 'dashboards/new': return t('nav.new-dashboard.title', 'New dashboard'); case 'dashboards/folder/new': diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index dcd2d188fe0..0b151e99f57 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -3,7 +3,6 @@ import React, { FC } from 'react'; import { NavModelItem, NavModelBreadcrumb, GrafanaTheme2 } from '@grafana/data'; import { Tab, TabsBar, Icon, useStyles2, toIconName } from '@grafana/ui'; -import { getNavTitle, getNavSubTitle } from 'app/core/components/NavBar/navBarItem-translations'; import { PanelHeaderMenuItem } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem'; import { PageInfoItem } from '../Page/types'; @@ -72,7 +71,7 @@ const Navigation = ({ children }: { children: NavModelItem[] }) => { return ( !child.hideFromTabs && ( = ({ navItem: model, renderTitle, actions, in const renderHeader = (main: NavModelItem) => { const marginTop = main.icon === 'grafana' ? 12 : 14; const icon = main.icon && toIconName(main.icon); - const sub = subTitle ?? getNavSubTitle(main.id) ?? main.subTitle; - const text = getNavTitle(main.id) ?? main.text; + const sub = subTitle ?? main.subTitle; return (
    {icon && } - {main.img && {`logo} + {main.img && }
    - {renderTitle ? renderTitle(text) : renderHeaderTitle(text, main.breadcrumbs ?? [], main.highlightText)} + {renderTitle + ? renderTitle(main.text) + : renderHeaderTitle(main.text, main.breadcrumbs ?? [], main.highlightText)} {info && } {sub &&
    {sub}
    } {actions &&
    {actions}
    } diff --git a/public/app/core/components/PageNew/PageHeader.tsx b/public/app/core/components/PageNew/PageHeader.tsx index beaabaac0ce..55fcdd552d4 100644 --- a/public/app/core/components/PageNew/PageHeader.tsx +++ b/public/app/core/components/PageNew/PageHeader.tsx @@ -4,7 +4,6 @@ import React from 'react'; import { NavModelItem, GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; -import { getNavSubTitle, getNavTitle } from '../NavBar/navBarItem-translations'; import { PageInfoItem } from '../Page/types'; import { PageInfo } from '../PageInfo/PageInfo'; @@ -18,10 +17,9 @@ export interface Props { export function PageHeader({ navItem, renderTitle, actions, info, subTitle }: Props) { const styles = useStyles2(getStyles); - const sub = subTitle ?? getNavSubTitle(navItem.id) ?? navItem.subTitle; + const sub = subTitle ?? navItem.subTitle; - const title = getNavTitle(navItem.id) ?? navItem.text; - const titleElement = renderTitle ? renderTitle(title) :

    {title}

    ; + const titleElement = renderTitle ? renderTitle(navItem.text) :

    {navItem.text}

    ; return (
    diff --git a/public/app/core/components/PageNew/SectionNavItem.tsx b/public/app/core/components/PageNew/SectionNavItem.tsx index 0a54d12dd3e..7abf0634e6a 100644 --- a/public/app/core/components/PageNew/SectionNavItem.tsx +++ b/public/app/core/components/PageNew/SectionNavItem.tsx @@ -6,8 +6,6 @@ import { selectors } from '@grafana/e2e-selectors'; import { reportInteraction } from '@grafana/runtime'; import { useStyles2, Icon } from '@grafana/ui'; -import { getNavTitle } from '../NavBar/navBarItem-translations'; - export interface Props { item: NavModelItem; isSectionRoot?: boolean; @@ -55,7 +53,7 @@ export function SectionNavItem({ item, isSectionRoot = false }: Props) { aria-selected={item.active} > {isSectionRoot && icon} - {getNavTitle(item.id) ?? item.text} + {item.text} {item.tabSuffix && } {children?.map((child, index) => ( diff --git a/public/app/core/internationalization/index.tsx b/public/app/core/internationalization/index.tsx index fa9c0d2988c..87f7c06fa37 100644 --- a/public/app/core/internationalization/index.tsx +++ b/public/app/core/internationalization/index.tsx @@ -22,7 +22,14 @@ const loadTranslations: BackendModule = { export function initializeI18n(language: string) { const validLocale = VALID_LANGUAGES.includes(language) ? language : DEFAULT_LANGUAGE; - i18n + // 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 + t( + '_comment', + 'Do not manually edit this file, or update these source phrases in Crowdin. The source of truth for English strings are in the code source' + ); + + return i18n .use(loadTranslations) .use(initReactI18next) // passes i18n down to react-i18next .init({ @@ -37,13 +44,6 @@ export function initializeI18n(language: string) { pluralSeparator: '__', }); - - // 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 - t( - '_comment', - 'Do not manually edit this file, or update these source phrases in Crowdin. The source of truth for English strings are in the code source' - ); } export function changeLanguage(locale: string) { diff --git a/public/app/core/reducers/navBarTree.ts b/public/app/core/reducers/navBarTree.ts index bd47059bf3c..49951bd181b 100644 --- a/public/app/core/reducers/navBarTree.ts +++ b/public/app/core/reducers/navBarTree.ts @@ -3,14 +3,30 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { NavModelItem } from '@grafana/data'; import { config } from '@grafana/runtime'; +import { getNavSubTitle, getNavTitle } from '../components/NavBar/navBarItem-translations'; + export const initialState: NavModelItem[] = config.bootData?.navTree ?? []; +function translateNav(navTree: NavModelItem[]): NavModelItem[] { + return navTree.map((navItem) => { + const children = navItem.children && translateNav(navItem.children); + + return { + ...navItem, + children: children, + text: getNavTitle(navItem.id) ?? navItem.text, + subTitle: getNavSubTitle(navItem.id) ?? navItem.subTitle, + emptyMessage: getNavTitle(navItem.emptyMessageId), + }; + }); +} + // this matches the prefix set in the backend navtree export const ID_PREFIX = 'starred/'; const navTreeSlice = createSlice({ name: 'navBarTree', - initialState, + initialState: () => translateNav(config.bootData?.navTree ?? []), reducers: { setStarred: (state, action: PayloadAction<{ id: string; title: string; url: string; isStarred: boolean }>) => { const starredItems = state.find((navItem) => navItem.id === 'starred'); diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index 5110af5c7af..18f0699d461 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -4,6 +4,8 @@ import { cloneDeep } from 'lodash'; import { NavIndex, NavModel, NavModelItem } from '@grafana/data'; import config from 'app/core/config'; +import { getNavSubTitle, getNavTitle } from '../components/NavBar/navBarItem-translations'; + export const HOME_NAV_ID = 'home'; export function buildInitialState(): NavIndex { @@ -16,22 +18,37 @@ export function buildInitialState(): NavIndex { buildNavIndex(navIndex, [homeNav]); } // set home as parent for the other rootNodes - buildNavIndex(navIndex, otherRootNodes, homeNav); + // need to use the translated home node from the navIndex + buildNavIndex(navIndex, otherRootNodes, navIndex[HOME_NAV_ID]); return navIndex; } function buildNavIndex(navIndex: NavIndex, children: NavModelItem[], parentItem?: NavModelItem) { + const translatedChildren: NavModelItem[] = []; + for (const node of children) { - node.parentItem = parentItem; + const translatedNode: NavModelItem = { + ...node, + text: getNavTitle(node.id) ?? node.text, + subTitle: getNavSubTitle(node.id) ?? node.subTitle, + emptyMessage: getNavTitle(node.emptyMessageId), + parentItem: parentItem, + }; - if (node.id) { - navIndex[node.id] = node; + if (translatedNode.id) { + navIndex[translatedNode.id] = translatedNode; } - if (node.children) { - buildNavIndex(navIndex, node.children, node); + if (translatedNode.children) { + buildNavIndex(navIndex, translatedNode.children, translatedNode); } + translatedChildren.push(translatedNode); + } + + // need to update the parentItem children with the new translated children + if (parentItem) { + parentItem.children = translatedChildren; } navIndex['not-found'] = { ...buildWarningNav('Page not found', '404 Error').node }; diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index c0625a4f533..63ae38fc535 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -287,6 +287,9 @@ "title": "Einstellungen" }, "profile/switch-org": "Organisation wechseln", + "public": { + "title": "" + }, "scenes": { "title": "Szenen" }, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 19c2e183523..719ab734af0 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -287,6 +287,9 @@ "title": "Profile" }, "profile/switch-org": "Switch organization", + "public": { + "title": "Public dashboards" + }, "scenes": { "title": "Scenes" }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index ed9234f94f3..10d633316f6 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -287,6 +287,9 @@ "title": "Preferencias" }, "profile/switch-org": "Cambiar de organización", + "public": { + "title": "" + }, "scenes": { "title": "Escenas" }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index baf901bae6f..419493a9ce8 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -287,6 +287,9 @@ "title": "Préférences" }, "profile/switch-org": "Passer à une autre organisation", + "public": { + "title": "" + }, "scenes": { "title": "Scènes" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 1c18fcd1207..14c6f1cc369 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -287,6 +287,9 @@ "title": "Přőƒįľę" }, "profile/switch-org": "Ŝŵįŧčĥ őřģäʼnįžäŧįőʼn", + "public": { + "title": "Pūþľįč đäşĥþőäřđş" + }, "scenes": { "title": "Ŝčęʼnęş" }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 5b37684294f..30b70a7c70d 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -287,6 +287,9 @@ "title": "首选项" }, "profile/switch-org": "切换组织", + "public": { + "title": "" + }, "scenes": { "title": "场景" },