Navigation: Ensure command palette is correctly translated (#61103)

* Navigation: Translate nav tree in store

* Remove usage of getNavTitle

* properly translate children

* add pubdash translation

* remove extraneous calls to translation in PageHeader

* empty commit

* correctly allow subtitle to be overrwitten (thank you tests!)

---------

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
Josh Hunt 2023-02-08 13:15:37 +00:00 committed by GitHub
parent bc67edbff8
commit 504eabbe80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 108 additions and 74 deletions

View File

@ -44,6 +44,7 @@ export interface NavModelItem extends NavLinkDTO {
highlightId?: string;
tabSuffix?: ComponentType<{ className?: string }>;
hideFromBreadcrumbs?: boolean;
emptyMessage?: string;
}
export enum NavSection {

View File

@ -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();

View File

@ -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) => (
<NavLandingPageCard
key={child.id}
description={getNavSubTitle(child.id) ?? child.subTitle}
text={getNavTitle(child.id) ?? child.text}
description={child.subTitle}
text={child.text}
url={child.url ?? ''}
/>
))}

View File

@ -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
<div onClick={(e) => e.stopPropagation()} className={styles.header}>
<div>{getNavTitle(node.id) ?? node.text}</div>
<div>{node.text}</div>
{node.subTitle && <div className={styles.subTitle}>{node.subTitle}</div>}
</div>
}
>
{node.children?.map((item) => {
const itemText = getNavTitle(item.id) ?? item.text;
const showExternalLinkIcon = /^https?:\/\//.test(item.url || '');
return item.url ? (
<MenuItem
url={item.url}
label={itemText}
label={item.text}
icon={showExternalLinkIcon ? 'external-link-alt' : undefined}
target={item.target}
key={item.id}
/>
) : (
<MenuItem icon={item.icon} onClick={item.onClick} label={itemText} key={item.id} />
<MenuItem icon={item.icon} onClick={item.onClick} label={item.text} key={item.id} />
);
})}
</Menu>

View File

@ -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 ?? '' });
}
}

View File

@ -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 (
<NavBarMenuSection link={link}>
<ul className={styles.children}>
<div className={styles.emptyMessage}>{emptyMessageTranslated}</div>
<div className={styles.emptyMessage}>{link.emptyMessage}</div>
</ul>
</NavBarMenuSection>
);
@ -52,7 +50,7 @@ export function NavBarMenuItemWrapper({
target={childLink.target}
url={childLink.url}
>
{getNavTitle(childLink.id) ?? childLink.text}
{childLink.text}
</NavBarMenuItem>
)
);

View File

@ -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({
<FeatureHighlightWrapper>
<NavBarItemIcon link={link} />
</FeatureHighlightWrapper>
{getNavTitle(link.id) ?? link.text}
{link.text}
</div>
</NavBarMenuItem>
{children && (

View File

@ -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 (
<li className={cx(styles.container, { [styles.containerHover]: section.id === menuIdOpen }, className)}>
<NavBarItemMenuTrigger
item={section}
isActive={isActive}
label={linkText}
label={link.text}
reverseMenuDirection={reverseMenuDirection}
>
<NavBarItemMenu
@ -72,7 +69,6 @@ const NavBarItem = ({ isActive = false, className, reverseMenuDirection = false,
onNavigate={onNavigate}
>
{(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 })}

View File

@ -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<NavModelItem> {
@ -58,11 +57,10 @@ export function NavBarItemMenu(props: NavBarItemMenuProps): ReactElement | null
<NavBarItemMenuItem key={getNavModelItemKey(item.value)} item={item} state={state} onNavigate={onNavigate} />
));
if (itemComponents.length === 0 && section.value.emptyMessageId) {
const emptyMessageTranslated = getNavTitle(section.value.emptyMessageId);
if (itemComponents.length === 0 && section.value.emptyMessage) {
itemComponents.push(
<div key="empty-message" className={styles.emptyMessage}>
{emptyMessageTranslated}
{section.value.emptyMessage}
</div>
);
}

View File

@ -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({
</ul>
</CollapsibleNavItem>
);
} else if (link.emptyMessageId) {
const emptyMessageTranslated = getNavTitle(link.emptyMessageId);
} else if (link.emptyMessage) {
return (
<CollapsibleNavItem onClose={onClose} link={link} isActive={isMatchOrChildMatch(link, activeItem)}>
<ul className={styles.children}>
<div className={styles.emptyMessage}>{emptyMessageTranslated}</div>
<div className={styles.emptyMessage}>{link.emptyMessage}</div>
</ul>
</CollapsibleNavItem>
);
@ -297,7 +295,7 @@ export function NavItem({
<NavBarItemIcon link={link} />
</FeatureHighlightWrapper>
</div>
<span className={styles.linkText}>{getNavTitle(link.id) ?? link.text}</span>
<span className={styles.linkText}>{link.text}</span>
</div>
</NavBarItemWithoutMenu>
</li>
@ -398,7 +396,7 @@ function CollapsibleNavItem({
contentClassName={styles.collapseContent}
label={
<div className={cx(styles.labelWrapper, { [styles.primary]: isActive })}>
<span className={styles.linkText}>{getNavTitle(link.id) ?? link.text}</span>
<span className={styles.linkText}>{link.text}</span>
</div>
}
>

View File

@ -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 <Trans />
// 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':

View File

@ -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 && (
<Tab
label={getNavTitle(child.id) ?? child.text}
label={child.text}
active={child.active}
key={`${child.url}-${index}`}
icon={child.icon}
@ -97,18 +96,19 @@ export const PageHeader: FC<Props> = ({ 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 (
<div className="page-header__inner">
<span className="page-header__logo">
{icon && <Icon name={icon} size="xxxl" style={{ marginTop }} />}
{main.img && <img className="page-header__img" src={main.img} alt={`logo of ${text}`} />}
{main.img && <img className="page-header__img" src={main.img} alt="" />}
</span>
<div className={cx('page-header__info-block', styles.headerText)}>
{renderTitle ? renderTitle(text) : renderHeaderTitle(text, main.breadcrumbs ?? [], main.highlightText)}
{renderTitle
? renderTitle(main.text)
: renderHeaderTitle(main.text, main.breadcrumbs ?? [], main.highlightText)}
{info && <PageInfo info={info} />}
{sub && <div className="page-header__sub-title">{sub}</div>}
{actions && <div className={styles.actions}>{actions}</div>}

View File

@ -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) : <h1 className={styles.pageTitle}>{title}</h1>;
const titleElement = renderTitle ? renderTitle(navItem.text) : <h1 className={styles.pageTitle}>{navItem.text}</h1>;
return (
<div className={styles.pageHeader}>

View File

@ -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 && <item.tabSuffix className={styles.suffix} />}
</a>
{children?.map((child, index) => (

View File

@ -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) {

View File

@ -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');

View File

@ -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 };

View File

@ -287,6 +287,9 @@
"title": "Einstellungen"
},
"profile/switch-org": "Organisation wechseln",
"public": {
"title": ""
},
"scenes": {
"title": "Szenen"
},

View File

@ -287,6 +287,9 @@
"title": "Profile"
},
"profile/switch-org": "Switch organization",
"public": {
"title": "Public dashboards"
},
"scenes": {
"title": "Scenes"
},

View File

@ -287,6 +287,9 @@
"title": "Preferencias"
},
"profile/switch-org": "Cambiar de organización",
"public": {
"title": ""
},
"scenes": {
"title": "Escenas"
},

View File

@ -287,6 +287,9 @@
"title": "Préférences"
},
"profile/switch-org": "Passer à une autre organisation",
"public": {
"title": ""
},
"scenes": {
"title": "Scènes"
},

View File

@ -287,6 +287,9 @@
"title": "Přőƒįľę"
},
"profile/switch-org": "Ŝŵįŧčĥ őřģäʼnįžäŧįőʼn",
"public": {
"title": "Pūþľįč đäşĥþőäřđş"
},
"scenes": {
"title": "Ŝčęʼnęş"
},

View File

@ -287,6 +287,9 @@
"title": "首选项"
},
"profile/switch-org": "切换组织",
"public": {
"title": ""
},
"scenes": {
"title": "场景"
},