@redocly/theme 0.11.5 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/lib/I18n/LanguagePicker.d.ts +4 -0
  2. package/lib/I18n/LanguagePicker.js +111 -0
  3. package/lib/I18n/index.d.ts +1 -0
  4. package/lib/I18n/index.js +6 -0
  5. package/lib/components/Cards/Card.js +3 -1
  6. package/lib/components/Catalog/Catalog.js +14 -9
  7. package/lib/components/Catalog/CatalogCard.js +6 -1
  8. package/lib/components/Catalog/useCatalog.js +4 -1
  9. package/lib/components/CodeSample/CodeSample.js +11 -3
  10. package/lib/components/Feedback/Comment.js +12 -4
  11. package/lib/components/Feedback/Rating.js +8 -2
  12. package/lib/components/Feedback/Reasons.js +11 -4
  13. package/lib/components/Feedback/Sentiment.js +12 -4
  14. package/lib/components/Feedback/Thumbs.js +31 -18
  15. package/lib/components/Feedback/useReportDialog.js +8 -2
  16. package/lib/components/Filter/Filter.js +15 -8
  17. package/lib/components/Footer/FooterColumn.js +4 -2
  18. package/lib/components/Footer/FooterCopyright.d.ts +1 -2
  19. package/lib/components/Footer/FooterCopyright.js +6 -1
  20. package/lib/components/LastUpdated/LastUpdated.js +8 -4
  21. package/lib/components/Markdown/MarkdownLayout.js +6 -1
  22. package/lib/components/Menu/MenuGroup.js +3 -1
  23. package/lib/components/Menu/MenuItem.js +3 -1
  24. package/lib/components/Navbar/MobileNavbarItem.js +6 -2
  25. package/lib/components/Navbar/Navbar.d.ts +1 -0
  26. package/lib/components/Navbar/Navbar.js +6 -1
  27. package/lib/components/Navbar/NavbarDropdown.js +3 -1
  28. package/lib/components/Navbar/NavbarItem.js +9 -4
  29. package/lib/components/PageNavigation/NextButton.js +7 -2
  30. package/lib/components/PageNavigation/PreviousButton.js +10 -2
  31. package/lib/components/Profile/LoginLink.js +6 -1
  32. package/lib/components/Profile/UserProfileMenu.js +10 -4
  33. package/lib/components/Search/Autocomplete.d.ts +3 -3
  34. package/lib/components/Search/Autocomplete.js +21 -11
  35. package/lib/components/Search/ClearIcon.js +1 -1
  36. package/lib/components/Search/Search.js +6 -4
  37. package/lib/components/Separator/Separator.js +3 -1
  38. package/lib/components/Sidebar/DrilldownMenu.js +6 -1
  39. package/lib/components/Sidebar/DrilldownMenuItem.js +4 -2
  40. package/lib/components/Sidebar/types.d.ts +2 -0
  41. package/lib/components/TableOfContent/TableOfContent.js +6 -1
  42. package/lib/icons/SpinnerIcon/SpinnerIcon.d.ts +8 -0
  43. package/lib/icons/SpinnerIcon/SpinnerIcon.js +32 -0
  44. package/lib/icons/SpinnerIcon/index.d.ts +1 -0
  45. package/lib/icons/SpinnerIcon/index.js +6 -0
  46. package/lib/icons/index.d.ts +1 -0
  47. package/lib/icons/index.js +1 -0
  48. package/lib/index.d.ts +1 -0
  49. package/lib/index.js +1 -0
  50. package/lib/layouts/Forbidden.js +8 -2
  51. package/lib/layouts/NotFound.js +8 -2
  52. package/lib/mocks/hooks/index.d.ts +15 -1
  53. package/lib/mocks/hooks/index.js +19 -1
  54. package/lib/mocks/search.d.ts +1 -0
  55. package/lib/mocks/search.js +1 -0
  56. package/lib/mocks/utils.d.ts +5 -0
  57. package/lib/mocks/utils.js +9 -1
  58. package/lib/types/portal/index.d.ts +1 -0
  59. package/lib/types/portal/index.js +1 -0
  60. package/lib/types/portal/src/shared/types/catalog.d.ts +4 -0
  61. package/lib/types/portal/src/shared/types/nav.d.ts +7 -0
  62. package/package.json +1 -1
  63. package/src/I18n/LanguagePicker.tsx +113 -0
  64. package/src/I18n/index.ts +1 -0
  65. package/src/components/Cards/Card.tsx +5 -1
  66. package/src/components/Catalog/Catalog.tsx +23 -6
  67. package/src/components/Catalog/CatalogCard.tsx +8 -1
  68. package/src/components/Catalog/useCatalog.ts +4 -2
  69. package/src/components/CodeSample/CodeSample.tsx +22 -4
  70. package/src/components/Feedback/Comment.tsx +25 -4
  71. package/src/components/Feedback/Rating.tsx +15 -2
  72. package/src/components/Feedback/Reasons.tsx +23 -5
  73. package/src/components/Feedback/Sentiment.tsx +25 -4
  74. package/src/components/Feedback/Thumbs.tsx +61 -46
  75. package/src/components/Feedback/useReportDialog.ts +11 -2
  76. package/src/components/Filter/Filter.tsx +17 -9
  77. package/src/components/Footer/CustomFooter.tsx +1 -1
  78. package/src/components/Footer/FooterColumn.tsx +5 -3
  79. package/src/components/Footer/FooterCopyright.tsx +12 -3
  80. package/src/components/LastUpdated/LastUpdated.tsx +10 -2
  81. package/src/components/Markdown/MarkdownLayout.tsx +11 -1
  82. package/src/components/Menu/MenuGroup.tsx +4 -1
  83. package/src/components/Menu/MenuItem.tsx +3 -1
  84. package/src/components/Navbar/MobileNavbarItem.tsx +7 -1
  85. package/src/components/Navbar/Navbar.tsx +8 -0
  86. package/src/components/Navbar/NavbarDropdown.tsx +3 -1
  87. package/src/components/Navbar/NavbarItem.tsx +9 -3
  88. package/src/components/PageNavigation/NextButton.tsx +8 -2
  89. package/src/components/PageNavigation/PreviousButton.tsx +11 -2
  90. package/src/components/Profile/LoginLink.tsx +11 -1
  91. package/src/components/Profile/UserProfileMenu.tsx +13 -3
  92. package/src/components/Search/Autocomplete.tsx +31 -17
  93. package/src/components/Search/ClearIcon.tsx +1 -1
  94. package/src/components/Search/Search.tsx +8 -7
  95. package/src/components/Separator/Separator.tsx +4 -1
  96. package/src/components/Sidebar/DrilldownMenu.tsx +8 -1
  97. package/src/components/Sidebar/DrilldownMenuItem.tsx +7 -2
  98. package/src/components/Sidebar/types.ts +2 -0
  99. package/src/components/TableOfContent/TableOfContent.tsx +11 -1
  100. package/src/icons/SpinnerIcon/SpinnerIcon.tsx +42 -0
  101. package/src/icons/SpinnerIcon/index.ts +1 -0
  102. package/src/icons/index.ts +1 -0
  103. package/src/index.ts +1 -0
  104. package/src/layouts/Forbidden.tsx +18 -3
  105. package/src/layouts/NotFound.tsx +17 -3
  106. package/src/mocks/hooks/index.ts +20 -1
  107. package/src/mocks/search.ts +2 -0
  108. package/src/mocks/utils.ts +13 -0
  109. package/src/types/portal/index.ts +1 -0
  110. package/src/types/portal/src/shared/types/catalog.ts +4 -0
  111. package/src/types/portal/src/shared/types/i18n.d.ts +3 -0
  112. package/src/types/portal/src/shared/types/nav.ts +7 -0
@@ -3,12 +3,14 @@ import styled from 'styled-components';
3
3
  import { useLocation } from 'react-router-dom';
4
4
 
5
5
  import { Link } from '@portal/Link';
6
+ import { getPathnameForLocale } from '@portal/utils';
6
7
  import { NavbarDropdown, DropdownWrapper } from '@theme/components/Navbar/NavbarDropdown';
7
8
  import type {
8
9
  ResolvedNavItem,
9
10
  ResolvedNavLinkItem,
10
11
  ResolvedNavGroupItem,
11
12
  } from '@theme/types/portal';
13
+ import { useI18nConfig, useTranslate } from '@portal/hooks';
12
14
  import { withPathPrefix } from '@theme/utils';
13
15
 
14
16
  export interface NavbarItemProps {
@@ -18,11 +20,15 @@ export interface NavbarItemProps {
18
20
 
19
21
  export function NavbarItem({ navItem, className }: NavbarItemProps): JSX.Element | null {
20
22
  const { pathname } = useLocation();
23
+ const { translate } = useTranslate();
24
+ const { defaultLocale, currentLocale, locales } = useI18nConfig();
21
25
 
22
26
  if ((navItem as ResolvedNavLinkItem).link) {
23
27
  const item = navItem as ResolvedNavLinkItem;
24
28
 
25
- const isActive = pathname === withPathPrefix(item.link);
29
+ const isActive =
30
+ pathname ===
31
+ withPathPrefix(getPathnameForLocale(item.link, defaultLocale, currentLocale, locales));
26
32
  return (
27
33
  <NavbarMenuItem
28
34
  active={isActive}
@@ -30,7 +36,7 @@ export function NavbarItem({ navItem, className }: NavbarItemProps): JSX.Element
30
36
  className={className}
31
37
  >
32
38
  <NavbarLink to={item.link} external={item.external} target={item.target} active={isActive}>
33
- <NavbarLabel>{item.label}</NavbarLabel>
39
+ <NavbarLabel>{translate(item.labelTranslationKey, item.label)}</NavbarLabel>
34
40
  </NavbarLink>
35
41
  </NavbarMenuItem>
36
42
  );
@@ -45,7 +51,7 @@ export function NavbarItem({ navItem, className }: NavbarItemProps): JSX.Element
45
51
  data-component-name="Navbar/NavbarItem"
46
52
  className={className}
47
53
  >
48
- <NavbarLabel>{item.label}</NavbarLabel>
54
+ <NavbarLabel>{translate(item.labelTranslationKey, item.label)}</NavbarLabel>
49
55
  </NavbarMenuItem>
50
56
  <NavbarDropdown items={item.items as ResolvedNavLinkItem[]} />
51
57
  </NavbarMenuItemWithDropdownWrapper>
@@ -4,6 +4,7 @@ import styled from 'styled-components';
4
4
  import { Button } from '@theme/components/Button';
5
5
  import { useThemeConfig } from '@theme/hooks/useThemeConfig';
6
6
  import type { ResolvedNavItemWithLink } from '@theme/types/portal';
7
+ import { useTranslate } from '@portal/hooks';
7
8
 
8
9
  interface NextPageType {
9
10
  nextPage?: ResolvedNavItemWithLink | null;
@@ -11,15 +12,19 @@ interface NextPageType {
11
12
 
12
13
  export function NextButton({ nextPage }: NextPageType): JSX.Element {
13
14
  const { navigation } = useThemeConfig();
15
+ const { translate } = useTranslate();
14
16
 
15
17
  if (!nextPage || navigation?.nextButton?.hide) {
16
18
  return <div>&nbsp;</div>;
17
19
  }
18
20
 
19
- const text = (navigation?.nextButton?.text || 'Next to {label}').replace(
21
+ const nextPageText = nextPage.label || nextPage.routeSlug || '';
22
+ const defaultText = (navigation?.nextButton?.text || 'Next to {label}').replace(
20
23
  '{label}',
21
- nextPage.label || nextPage.routeSlug || '',
24
+ nextPageText,
22
25
  );
26
+ const translationKey = 'theme.page.nextButton';
27
+ const text = translate(translationKey, { label: nextPageText, defaultValue: defaultText });
23
28
 
24
29
  return (
25
30
  <StyledButton
@@ -27,6 +32,7 @@ export function NextButton({ nextPage }: NextPageType): JSX.Element {
27
32
  size="large"
28
33
  to={nextPage.link}
29
34
  data-component-name="PageNavigation/NextPageLink"
35
+ data-translation-key={translationKey}
30
36
  >
31
37
  {text}
32
38
  </StyledButton>
@@ -4,6 +4,7 @@ import styled from 'styled-components';
4
4
  import { Button } from '@theme/components/Button';
5
5
  import { useThemeConfig } from '@theme/hooks/useThemeConfig';
6
6
  import type { ResolvedNavItemWithLink } from '@theme/types/portal';
7
+ import { useTranslate } from '@portal/hooks';
7
8
 
8
9
  interface PreviousPageType {
9
10
  prevPage?: ResolvedNavItemWithLink | null;
@@ -11,15 +12,22 @@ interface PreviousPageType {
11
12
 
12
13
  export function PreviousButton({ prevPage }: PreviousPageType): JSX.Element {
13
14
  const { navigation } = useThemeConfig();
15
+ const { translate } = useTranslate();
14
16
 
15
17
  if (!prevPage || navigation?.previousButton?.hide) {
16
18
  return <div>&nbsp;</div>;
17
19
  }
18
20
 
19
- const text = (navigation?.previousButton?.text || 'Back to {label}').replace(
21
+ const prevPageText = prevPage.label || prevPage.routeSlug || '';
22
+ const defaultText = (navigation?.previousButton?.text || 'Back to {label}').replace(
20
23
  '{label}',
21
- prevPage.label || prevPage.routeSlug || '',
24
+ prevPageText,
22
25
  );
26
+ const translationKey = 'theme.page.previousButton';
27
+ const text = translate(translationKey, {
28
+ label: prevPageText,
29
+ defaultValue: defaultText,
30
+ });
23
31
 
24
32
  return (
25
33
  <StyledButton
@@ -27,6 +35,7 @@ export function PreviousButton({ prevPage }: PreviousPageType): JSX.Element {
27
35
  size="large"
28
36
  to={prevPage.link}
29
37
  data-component-name="PageNavigation/PreviousPageLink"
38
+ data-translation-key={translationKey}
30
39
  >
31
40
  {text}
32
41
  </StyledButton>
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import { useThemeConfig } from '@theme/hooks/useThemeConfig';
5
+ import { useTranslate } from '@portal/hooks';
5
6
 
6
7
  export interface LoginLinkProps {
7
8
  href: string;
@@ -9,7 +10,16 @@ export interface LoginLinkProps {
9
10
 
10
11
  export function LoginLink({ href }: LoginLinkProps): JSX.Element {
11
12
  const { userProfile } = useThemeConfig();
12
- return <StyledLink href={href}>{userProfile?.loginLabel || 'Login'}</StyledLink>;
13
+ const { translate } = useTranslate();
14
+ const translationKeys = {
15
+ login: 'theme.profile.login',
16
+ };
17
+
18
+ return (
19
+ <StyledLink href={href} data-translation-key={translationKeys.login}>
20
+ {translate(translationKeys.login, userProfile?.loginLabel || 'Login')}
21
+ </StyledLink>
22
+ );
13
23
  }
14
24
 
15
25
  const StyledLink = styled.a.attrs(() => ({
@@ -2,7 +2,8 @@ import React from 'react';
2
2
  import styled, { css } from 'styled-components';
3
3
 
4
4
  import { Link } from '@portal/Link';
5
- import { useThemeConfig } from '@theme/hooks';
5
+ import { useThemeConfig } from '@theme/hooks/useThemeConfig';
6
+ import { useTranslate } from '@portal/hooks';
6
7
 
7
8
  interface UserProfileMenuProps {
8
9
  setIsOpened: (isOpen: boolean) => void;
@@ -16,13 +17,20 @@ export function UserProfileMenu({
16
17
  handleLogout,
17
18
  }: UserProfileMenuProps): JSX.Element {
18
19
  const { userProfile } = useThemeConfig();
20
+ const { translate } = useTranslate();
21
+ const translationKeys = {
22
+ myApps: 'theme.profile.myApps',
23
+ logout: 'theme.profile.logout',
24
+ };
19
25
 
20
26
  return (
21
27
  <>
22
28
  <StyledUl onClick={() => setIsOpened(false)}>
23
29
  {hasDeveloperOnboarding ? (
24
30
  <Link to="/apps">
25
- <StyledLi>My Apps</StyledLi>
31
+ <StyledLi data-translation-key={translationKeys.myApps}>
32
+ {translate(translationKeys.myApps, 'My Apps')}
33
+ </StyledLi>
26
34
  </Link>
27
35
  ) : null}
28
36
 
@@ -32,7 +40,9 @@ export function UserProfileMenu({
32
40
  </Link>
33
41
  ))}
34
42
 
35
- <StyledLi onClick={() => handleLogout()}>{userProfile?.logoutLabel || 'Log out'}</StyledLi>
43
+ <StyledLi onClick={() => handleLogout()} data-translation-key={translationKeys.logout}>
44
+ {translate(translationKeys.logout, userProfile?.logoutLabel || 'Log out')}
45
+ </StyledLi>
36
46
  </StyledUl>
37
47
  </>
38
48
  );
@@ -9,17 +9,21 @@ import { FormInput } from '@theme/components/Search/Input';
9
9
  import { Popover } from '@theme/components/Search/Popover';
10
10
  import { ShortcutKey } from '@theme/components/Search/ShortcutKey';
11
11
  import type { ActiveItem } from '@theme/types/portal/src/shared/types/activeItem';
12
+ import { useTranslate } from '@portal/hooks';
13
+ import { SearchIcon } from '@theme/components/Search/SearchIcon';
14
+ import { SpinnerIcon } from '@theme/icons/SpinnerIcon';
15
+ import { ClearIcon } from '@theme/components/Search/ClearIcon';
12
16
 
13
17
  interface AutocompleteProps<T> {
14
18
  placeholder?: string;
15
19
  value: string;
16
- items: T[];
20
+ items: T[] | null;
17
21
  renderItem(item: ActiveItem<T>): ReactNode;
18
22
  change(value: string): void;
19
23
  select(item: T): void;
20
- children?(isOpen: boolean, close: () => void): ReactNode;
21
24
  inputRef?: React.RefObject<HTMLInputElement>;
22
25
  keyShortcuts?: string[];
26
+ isLoading: boolean;
23
27
  }
24
28
 
25
29
  export function Autocomplete<T>({
@@ -29,13 +33,17 @@ export function Autocomplete<T>({
29
33
  change,
30
34
  select,
31
35
  renderItem,
32
- children,
33
36
  keyShortcuts,
37
+ isLoading,
34
38
  }: AutocompleteProps<T>): JSX.Element {
35
39
  const location = useLocation();
36
- const [isOpen, setIsOpen] = useState(false);
40
+ const [isFocused, setIsFocused] = useState(false);
37
41
  const [activeIdx, setActiveIdx] = useState(-1);
38
42
  const refInput = useRef<HTMLInputElement>(null);
43
+ const { translate } = useTranslate();
44
+ const translationKeys = {
45
+ noResults: 'theme.search.noResults',
46
+ };
39
47
 
40
48
  const hotkeysKeys = keyShortcuts?.join(',');
41
49
 
@@ -50,10 +58,10 @@ export function Autocomplete<T>({
50
58
  }
51
59
  }, [hotkeysKeys]);
52
60
 
53
- const open = () => setIsOpen(true);
61
+ const onFocus = () => setIsFocused(true);
54
62
 
55
63
  const close = () => {
56
- setIsOpen(false);
64
+ setIsFocused(false);
57
65
  setActiveIdx(-1);
58
66
  };
59
67
 
@@ -66,11 +74,12 @@ export function Autocomplete<T>({
66
74
 
67
75
  const onChange = (event: ChangeEvent<HTMLInputElement>) => {
68
76
  setActiveIdx(-1);
69
- setIsOpen(true);
77
+ setIsFocused(true);
70
78
  change(event.target.value);
71
79
  };
72
80
 
73
81
  const onKeydown = (event: KeyboardEvent<HTMLInputElement>) => {
82
+ if (items === null) return;
74
83
  switch (event.code) {
75
84
  case 'Escape':
76
85
  close();
@@ -101,24 +110,29 @@ export function Autocomplete<T>({
101
110
 
102
111
  return (
103
112
  <Wrapper data-component-name="Search/Autocomplete">
104
- {isOpen ? <Overlay onClick={close} /> : null}
113
+ {isFocused ? <Overlay onClick={close} /> : null}
105
114
  <AutocompleteBox onKeyDown={onKeydown}>
106
- {children?.(isOpen, reset)}
107
-
115
+ {isFocused && isLoading ? <SpinnerIcon /> : <SearchIcon />}
108
116
  <FormInput
109
117
  value={value}
110
118
  placeholder={placeholder}
111
119
  onChange={onChange}
112
- onFocus={open}
120
+ onFocus={onFocus}
113
121
  onClick={stopPropagation}
114
122
  ref={refInput}
115
123
  />
116
-
117
- {!isOpen && <ShortcutKey keyShortcuts={keyShortcuts} />}
118
-
119
- {isOpen && value ? (
120
- <Popover>{items.length ? items.map(mapItem) : <Message>No results</Message>}</Popover>
121
- ) : null}
124
+ {isFocused ? <ClearIcon onClick={reset} /> : <ShortcutKey keyShortcuts={keyShortcuts} />}
125
+ {isFocused && items !== null && value && (
126
+ <Popover>
127
+ {items.length ? (
128
+ items.map(mapItem)
129
+ ) : (
130
+ <Message data-translation-key={translationKeys.noResults}>
131
+ {translate(translationKeys.noResults, 'No results')}
132
+ </Message>
133
+ )}
134
+ </Popover>
135
+ )}
122
136
  </AutocompleteBox>
123
137
  </Wrapper>
124
138
  );
@@ -24,7 +24,7 @@ export const ClearIcon = styled(Icon).attrs(() => ({
24
24
  cursor: pointer;
25
25
  width: 0.5em;
26
26
  height: 0.5em;
27
- left: 1.2em;
27
+ right: 1em;
28
28
  fill: var(--search-input-text-color);
29
29
 
30
30
  ${({ theme }) => theme.mediaQueries.medium} {
@@ -3,17 +3,17 @@ import styled from 'styled-components';
3
3
 
4
4
  import { useFuseSearch } from '@portal/search';
5
5
  import { Autocomplete } from '@theme/components/Search/Autocomplete';
6
- import { ClearIcon } from '@theme/components/Search/ClearIcon';
7
- import { SearchIcon } from '@theme/components/Search/SearchIcon';
8
6
  import { SearchItem } from '@theme/components/Search/SearchItem';
9
7
  import { useThemeConfig } from '@theme/hooks';
10
8
  import type { SearchDocument } from '@theme/types/portal/src/shared/types/searchDocument';
11
9
  import { usePreloadHistory } from '@portal/usePreloadHistory';
10
+ import { useTranslate } from '@portal/hooks';
12
11
 
13
12
  export function Search(): JSX.Element {
14
13
  const history = usePreloadHistory();
15
- const { query, setQuery, items } = useFuseSearch();
14
+ const { query, setQuery, items, isLoading } = useFuseSearch();
16
15
  const themeSettings = useThemeConfig();
16
+ const { translate } = useTranslate();
17
17
 
18
18
  // TODO: ask somebody about typings
19
19
  const navigate = (item: SearchDocument) => history.push(item.url);
@@ -24,12 +24,11 @@ export function Search(): JSX.Element {
24
24
  value={query}
25
25
  change={setQuery}
26
26
  select={navigate}
27
- placeholder="Search the docs"
27
+ placeholder={translate('theme.search.label', 'Search the docs')}
28
28
  keyShortcuts={themeSettings?.search?.shortcuts ?? ['/']}
29
29
  renderItem={(item) => <SearchItem key={item.id} item={item} />}
30
- >
31
- {(isOpen, reset) => (isOpen ? <ClearIcon onClick={reset} /> : <SearchIcon />)}
32
- </Autocomplete>
30
+ isLoading={isLoading}
31
+ />
33
32
  );
34
33
 
35
34
  return <Wrapper data-component-name="Search/Search">{renderAutocomplete()}</Wrapper>;
@@ -38,6 +37,8 @@ export function Search(): JSX.Element {
38
37
  const Wrapper = styled.div`
39
38
  margin-left: auto;
40
39
 
40
+ padding: 0 var(--navbar-item-padding-horizontal);
41
+
41
42
  display: none;
42
43
 
43
44
  ${({ theme }) => theme.mediaQueries?.small} {
@@ -4,11 +4,14 @@ import styled from 'styled-components';
4
4
  import { SeparatorLine } from '@theme/components/Separator/SeparatorLine';
5
5
  import { SeparatorItem } from '@theme/components/Separator/SeparatorItem';
6
6
  import type { MenuItemProps } from '@theme/components/Sidebar/types';
7
+ import { useTranslate } from '@portal/hooks';
7
8
 
8
9
  export function Separator({ item }: MenuItemProps): JSX.Element {
10
+ const { translate } = useTranslate();
11
+
9
12
  return (
10
13
  <Wrapper data-component-name="Sidebar/Separator">
11
- <SeparatorItem>{item.label}</SeparatorItem>
14
+ <SeparatorItem>{translate(item.labelTranslationKey, item.label)}</SeparatorItem>
12
15
  {item.separatorLine ? <SeparatorLine /> : null}
13
16
  </Wrapper>
14
17
  );
@@ -4,6 +4,7 @@ import styled, { keyframes } from 'styled-components';
4
4
  import { BackButton } from '@theme/components/Sidebar/BackButton';
5
5
  import { DrilldownMenuItem } from '@theme/components/Sidebar/DrilldownMenuItem';
6
6
  import type { DrilldownMenuProps } from '@theme/components/Sidebar/types';
7
+ import { useTranslate } from '@portal/hooks';
7
8
 
8
9
  export function DrilldownMenu({
9
10
  item,
@@ -11,11 +12,17 @@ export function DrilldownMenu({
11
12
  prevActiveItem,
12
13
  children,
13
14
  }: React.PropsWithChildren<DrilldownMenuProps>): JSX.Element {
15
+ const { translate } = useTranslate();
16
+ const translationKeys = {
17
+ backLabel: 'theme.sidebar.menu.backLabel',
18
+ };
14
19
  return (
15
20
  <MenuContainer data-component-name="Sidebar/DrilldownMenu">
16
21
  <MenuContent>
17
22
  <MenuWrapper>
18
- <BackButton back={back}>{prevActiveItem?.label || 'Back'}</BackButton>
23
+ <BackButton back={back} data-translation-key={translationKeys.backLabel}>
24
+ {prevActiveItem?.label || translate(translationKeys.backLabel, 'Back')}
25
+ </BackButton>
19
26
  <DrilldownMenuItem item={item} />
20
27
  </MenuWrapper>
21
28
 
@@ -2,14 +2,19 @@ import React from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { MenuItemProps } from '@theme/components/Sidebar/types';
5
+ import { useTranslate } from '@portal/hooks';
5
6
 
6
7
  export function DrilldownMenuItem({ item }: MenuItemProps): JSX.Element {
8
+ const { translate } = useTranslate();
9
+
7
10
  return (
8
11
  <Container data-component-name="Sidebar/DrilldownMenuItem">
9
12
  {item.icon ? <Icon src={item.icon} /> : null}
10
13
  <div>
11
- {item.label}
12
- {item.sublabel ? <SubLabel>{item.sublabel}</SubLabel> : null}
14
+ {translate(item.labelTranslationKey, item.label)}
15
+ {item.sublabel ? (
16
+ <SubLabel>{translate(item.subLabelTranslationKey, item.sublabel)}</SubLabel>
17
+ ) : null}
13
18
  </div>
14
19
  </Container>
15
20
  );
@@ -22,7 +22,9 @@ export interface MenuItemProps {
22
22
 
23
23
  export interface NavItem {
24
24
  label?: string;
25
+ labelTranslationKey?: string;
25
26
  sublabel?: string;
27
+ subLabelTranslationKey?: string;
26
28
  icon?: string;
27
29
  link?: string;
28
30
  type?: string;
@@ -4,6 +4,7 @@ import styled from 'styled-components';
4
4
  import { useFullHeight } from '@theme/hooks/useFullHeight';
5
5
  import { useActiveHeading } from '@theme/hooks/useActiveHeading';
6
6
  import { useThemeConfig } from '@theme/hooks/useThemeConfig';
7
+ import { useTranslate } from '@portal/hooks';
7
8
  import type { MdHeading } from '@theme/types/portal';
8
9
  import {
9
10
  getDisplayedHeadingsIds,
@@ -31,6 +32,11 @@ export function TableOfContent(props: TableOfContentProps): JSX.Element | null {
31
32
  getDisplayedHeadingsIds(displayedHeadings),
32
33
  );
33
34
 
35
+ const { translate } = useTranslate();
36
+ const translationKeys = {
37
+ header: 'theme.toc.header',
38
+ };
39
+
34
40
  if (toc?.hide) {
35
41
  return null;
36
42
  }
@@ -39,7 +45,11 @@ export function TableOfContent(props: TableOfContentProps): JSX.Element | null {
39
45
  <>
40
46
  <TableOfContentMenu data-component-name="TableOfContent/TableOfContent">
41
47
  <TableOfContentItems ref={sidebar}>
42
- {displayedHeadings.length ? <TocHeader>{toc.header || 'On this page'}</TocHeader> : null}
48
+ {displayedHeadings.length ? (
49
+ <TocHeader data-translation-key={translationKeys.header}>
50
+ {translate(translationKeys.header, toc.header || 'On this page')}
51
+ </TocHeader>
52
+ ) : null}
43
53
  {displayedHeadings.map((heading: MdHeading | null, idx: number) => {
44
54
  if (!heading) {
45
55
  return null;
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ export interface SpinnerIconProps {
5
+ className?: string;
6
+ }
7
+ export const Icon = ({ className }: SpinnerIconProps) => (
8
+ <svg className={className} viewBox="0 -1 38 40" strokeOpacity=".5">
9
+ <g fill="none" fillRule="evenodd">
10
+ <g transform="translate(1 1)" strokeWidth="4">
11
+ <circle strokeOpacity=".3" cx="18" cy="18" r="18" />
12
+ <path d="M36 18c0-9.94-8.06-18-18-18">
13
+ <animateTransform
14
+ attributeName="transform"
15
+ type="rotate"
16
+ from="0 18 18"
17
+ to="360 18 18"
18
+ dur="1s"
19
+ repeatCount="indefinite"
20
+ />
21
+ </path>
22
+ </g>
23
+ </g>
24
+ </svg>
25
+ );
26
+
27
+ export const SpinnerIcon = styled(Icon).attrs(() => ({
28
+ 'data-component-name': 'icons/AnchorIcon/AnchorIcon',
29
+ }))`
30
+ position: absolute;
31
+ cursor: pointer;
32
+ width: 1em;
33
+ height: 1em;
34
+ left: 0.8em;
35
+ stroke: var(--search-input-text-color);
36
+ z-index: -1;
37
+
38
+ ${({ theme }) => theme.mediaQueries.medium} {
39
+ width: 1.2em;
40
+ height: 1.2em;
41
+ }
42
+ `;
@@ -0,0 +1 @@
1
+ export { SpinnerIcon } from '@theme/icons/SpinnerIcon/SpinnerIcon';
@@ -4,3 +4,4 @@ export * from '@theme/icons/ArrowIcon';
4
4
  export * from '@theme/icons/ColorModeIcon';
5
5
  export * from '@theme/icons/AnchorIcon';
6
6
  export * from '@theme/icons/ExternalIcon';
7
+ export * from '@theme/icons/SpinnerIcon';
package/src/index.ts CHANGED
@@ -7,3 +7,4 @@ export * from './globalStyle';
7
7
  export * from './types/config';
8
8
  export * from './config';
9
9
  export * from './ui';
10
+ export * from './I18n';
@@ -2,13 +2,28 @@ import React from 'react';
2
2
  import styled from 'styled-components';
3
3
  import { Button } from '@theme';
4
4
 
5
+ import { useTranslate } from '@portal/hooks';
6
+
5
7
  export function Forbidden(): JSX.Element {
8
+ const { translate } = useTranslate();
9
+ const translationKeys = {
10
+ title: 'theme.page.forbidden.title',
11
+ homeButton: 'theme.page.homeButton',
12
+ };
13
+
6
14
  return (
7
15
  <Wrapper data-component-name="Pages/Forbidden">
8
16
  <Header>403</Header>
9
- <Description>Access forbidden</Description>
10
- <HomeButton color="primary" size="large" to="/">
11
- Open Homepage
17
+ <Description data-translation-key={translationKeys.title}>
18
+ {translate(translationKeys.title, 'Access forbidden')}
19
+ </Description>
20
+ <HomeButton
21
+ color="primary"
22
+ size="large"
23
+ to="/"
24
+ data-translation-key={translationKeys.homeButton}
25
+ >
26
+ {translate(translationKeys.homeButton, 'Open Homepage')}
12
27
  </HomeButton>
13
28
  </Wrapper>
14
29
  );
@@ -2,14 +2,28 @@ import React from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import { Button } from '@theme/components/Button';
5
+ import { useTranslate } from '@portal/hooks';
5
6
 
6
7
  export function NotFound(): JSX.Element {
8
+ const { translate } = useTranslate();
9
+ const translationKeys = {
10
+ title: 'theme.page.notFound.title',
11
+ homeButton: 'theme.page.homeButton',
12
+ };
13
+
7
14
  return (
8
15
  <Wrapper data-component-name="Pages/NotFound">
9
16
  <Header>404</Header>
10
- <Description>It looks like you&apos;re lost</Description>
11
- <HomeButton color="primary" size="large" to="/">
12
- Open Homepage
17
+ <Description data-translation-key={translationKeys.title}>
18
+ {translate(translationKeys.title, `It looks like you're lost`)}
19
+ </Description>
20
+ <HomeButton
21
+ color="primary"
22
+ size="large"
23
+ to="/"
24
+ data-translation-key={translationKeys.homeButton}
25
+ >
26
+ {translate(translationKeys.homeButton, 'Open Homepage')}
13
27
  </HomeButton>
14
28
  </Wrapper>
15
29
  );
@@ -1,5 +1,5 @@
1
1
  import type { ThemeUIConfig } from '@theme/config';
2
- import type { ResolvedNavItem } from '@theme/types/portal';
2
+ import type { ResolvedNavItem, TFunction } from '@theme/types/portal';
3
3
  import type { CatalogConfig, FilteredCatalog } from '@theme/types/portal/src/shared/types/catalog';
4
4
  interface PageLink {
5
5
  label: string;
@@ -75,4 +75,23 @@ export function useCatalog(_items: ResolvedNavItem[], _config: CatalogConfig): F
75
75
  throw new Error('Mock not implemented yet.');
76
76
  }
77
77
 
78
+ export function useTranslate() {
79
+ const translate: TFunction = (value?: string, options?: { defaultValue: string } | string) =>
80
+ (typeof options === 'string' ? options : options?.defaultValue) || value || '';
81
+ return { translate };
82
+ }
83
+
84
+ export function useI18n() {
85
+ const changeLanguage = (...args: any) => args.value as string;
86
+ return { changeLanguage };
87
+ }
88
+
78
89
  export { useGlobalData } from '../useGlobalData';
90
+
91
+ export function useI18nConfig() {
92
+ return {
93
+ currentLocale: 'en',
94
+ defaultLocale: 'en',
95
+ locales: [{ code: 'en', name: 'en' }],
96
+ };
97
+ }
@@ -22,12 +22,14 @@ export function useFuseSearch(): {
22
22
  query: string;
23
23
  setQuery: (val: string) => void;
24
24
  items: SearchDocument[];
25
+ isLoading: boolean;
25
26
  } {
26
27
  const [query, setQuery] = useState('');
27
28
 
28
29
  return {
29
30
  query,
30
31
  setQuery,
32
+ isLoading: false,
31
33
  items: [
32
34
  {
33
35
  id: '1',