diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
index 4ea2d626c..e9f36ab5e 100644
--- a/src/components/Footer/Footer.tsx
+++ b/src/components/Footer/Footer.tsx
@@ -5,7 +5,7 @@ import React from 'react';
import { CONTAINER_MAX_WIDTH_NORMAL, CONTAINER_PADDING } from '../layout';
export function Footer(props: { space: Space; asFullWidth: boolean }) {
- const { asFullWidth } = props;
+ const { space, asFullWidth } = props;
return (
-
+
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
index 49ed5d58c..c2f06e366 100644
--- a/src/components/Header/Header.tsx
+++ b/src/components/Header/Header.tsx
@@ -6,6 +6,7 @@ import Link from 'next/link';
import { Suspense } from 'react';
import { SearchButton } from '../Search';
import { CONTAINER_MAX_WIDTH_NORMAL, CONTAINER_PADDING } from '@/components/layout';
+import { t } from '@/lib/intl';
/**
* Render the header for the space.
@@ -50,7 +51,7 @@ export function Header(props: { space: Space; asFullWidth: boolean }) {
-
+ {t({ space }, 'search')}
diff --git a/src/components/PageAside/PageAside.tsx b/src/components/PageAside/PageAside.tsx
index 3677dfb1a..e327c43f5 100644
--- a/src/components/PageAside/PageAside.tsx
+++ b/src/components/PageAside/PageAside.tsx
@@ -1,14 +1,15 @@
import { getDocumentSections } from '@/lib/document';
import { tcls } from '@/lib/tailwind';
-import { RevisionPageDocument } from '@gitbook/api';
+import { RevisionPageDocument, Space } from '@gitbook/api';
import React from 'react';
import { ScrollSectionsList } from './ScrollSectionsList';
+import { t } from '@/lib/intl';
/**
* Aside listing the headings in the document.
*/
-export function PageAside(props: { page: RevisionPageDocument; document: any }) {
- const { document } = props;
+export function PageAside(props: { space: Space; page: RevisionPageDocument; document: any }) {
+ const { space, document } = props;
const sections = getDocumentSections(document);
return (
@@ -29,7 +30,7 @@ export function PageAside(props: { page: RevisionPageDocument; document: any })
{sections.length > 0 ? (
<>
- On this page
+ {t({ space }, 'on_this_page')}
diff --git a/src/components/Search/SearchButton.tsx b/src/components/Search/SearchButton.tsx
index a4f57d796..1beeaa2f7 100644
--- a/src/components/Search/SearchButton.tsx
+++ b/src/components/Search/SearchButton.tsx
@@ -7,8 +7,8 @@ import { IconSearch } from '@/components/icons/IconSearch';
/**
* Button to open the search modal.
*/
-export function SearchButton(props: { style?: ClassValue }) {
- const { style } = props;
+export function SearchButton(props: { children: React.ReactNode; style?: ClassValue }) {
+ const { style, children } = props;
const router = useRouter();
const pathname = usePathname();
@@ -41,7 +41,7 @@ export function SearchButton(props: { style?: ClassValue }) {
)}
>
- Search
+ {children}
);
}
diff --git a/src/components/SpaceContent/SpaceContent.tsx b/src/components/SpaceContent/SpaceContent.tsx
index 16f02d3b9..1051f65df 100644
--- a/src/components/SpaceContent/SpaceContent.tsx
+++ b/src/components/SpaceContent/SpaceContent.tsx
@@ -35,9 +35,14 @@ export function SpaceContent(props: {
asFullWidth ? null : [CONTAINER_MAX_WIDTH_NORMAL, 'mx-auto'],
)}
>
-
+
-
+
diff --git a/src/components/TableOfContents/TableOfContents.tsx b/src/components/TableOfContents/TableOfContents.tsx
index 831fb78e0..4b18696d4 100644
--- a/src/components/TableOfContents/TableOfContents.tsx
+++ b/src/components/TableOfContents/TableOfContents.tsx
@@ -2,13 +2,16 @@ import { Revision, RevisionPageDocument, RevisionPageGroup } from '@gitbook/api'
import { tcls } from '@/lib/tailwind';
import { PagesList } from './PagesList';
import { Trademark } from './Trademark';
+import { IntlContext } from '@/lib/intl';
-export function TableOfContents(props: {
- revision: Revision;
- activePage: RevisionPageDocument;
- ancestors: Array;
-}) {
- const { revision, activePage, ancestors } = props;
+export function TableOfContents(
+ props: IntlContext & {
+ revision: Revision;
+ activePage: RevisionPageDocument;
+ ancestors: Array;
+ },
+) {
+ const { space, revision, activePage, ancestors } = props;
return (
);
}
diff --git a/src/components/TableOfContents/Trademark.tsx b/src/components/TableOfContents/Trademark.tsx
index 55f496c5b..15edecbb1 100644
--- a/src/components/TableOfContents/Trademark.tsx
+++ b/src/components/TableOfContents/Trademark.tsx
@@ -1,9 +1,10 @@
+import { IntlContext, t } from '@/lib/intl';
import { tcls } from '@/lib/tailwind';
/**
* Link to the GitBook platform.
*/
-export function Trademark() {
+export function Trademark(props: IntlContext) {
return (
);
diff --git a/src/components/ThemeToggler/ThemeToggler.tsx b/src/components/ThemeToggler/ThemeToggler.tsx
index d8b1e0d2a..44aa0c8b3 100644
--- a/src/components/ThemeToggler/ThemeToggler.tsx
+++ b/src/components/ThemeToggler/ThemeToggler.tsx
@@ -5,6 +5,7 @@ import React from 'react';
import Moon from '@geist-ui/icons/moon';
import Sun from '@geist-ui/icons/sun';
import Monitor from '@geist-ui/icons/monitor';
+import { IntlContext, tString } from '@/lib/intl';
type ThemeMode = 'light' | 'system' | 'dark';
@@ -13,7 +14,7 @@ const LOCALSTORAGE_KEY = 'color-theme';
/**
* Buttons to toggle between light/system/dark modes.
*/
-export function ThemeToggler(props: {}) {
+export function ThemeToggler(props: IntlContext) {
const [mode, setMode] = React.useState('system');
const onSwitchMode = (to: ThemeMode) => {
@@ -59,19 +60,19 @@ export function ThemeToggler(props: {}) {
active={mode === 'light'}
icon={Sun}
onClick={() => onSwitchMode('light')}
- title="Switch to light theme"
+ title={tString(props, 'switch_to_light_theme')}
/>
onSwitchMode('system')}
- title="Switch to system theme"
+ title={tString(props, 'switch_to_system_theme')}
/>
onSwitchMode('dark')}
- title="Switch to dark theme"
+ title={tString(props, 'switch_to_dark_theme')}
/>
);
diff --git a/src/lib/intl.tsx b/src/lib/intl.tsx
new file mode 100644
index 000000000..f41b9b4c2
--- /dev/null
+++ b/src/lib/intl.tsx
@@ -0,0 +1,81 @@
+import * as translations from '@/translations';
+import { Space } from '@gitbook/api';
+import React from 'react';
+
+export interface IntlContext {
+ space: Space;
+}
+
+/**
+ * Translate a string.
+ */
+export function t(
+ context: IntlContext,
+ id: keyof typeof translations.en,
+ ...args: React.ReactNode[]
+): React.ReactNode {
+ const locale = 'en'; // TODO
+
+ // fallback to english if no translation
+ const string = translations[locale]?.[id] || translations.en[id];
+
+ // Now we are going to replace the arguments
+ // but we want to return a string as long as it's possible
+ // (eg. if there isn't any argument that is a ReactNode)
+ const parts: React.ReactNode[] = [];
+ let currentStringToReplace: string = string;
+
+ args.forEach((arg, i) => {
+ if (typeof arg === 'string') {
+ currentStringToReplace = currentStringToReplace.replace(`\${${i + 1}}`, arg);
+ } else {
+ const [partToPush, partToReplace] = currentStringToReplace.split(`\${${i + 1}}`);
+ parts.push({partToPush});
+ parts.push({arg});
+ currentStringToReplace = partToReplace;
+ }
+ });
+
+ if (!parts.length) {
+ return currentStringToReplace;
+ }
+
+ return (
+ <>
+ {parts}
+ {currentStringToReplace}
+ >
+ );
+}
+
+/**
+ * Version of `t` that returns a string.
+ */
+export function tString(
+ context: IntlContext,
+ id: keyof typeof translations.en,
+ ...args: React.ReactNode[]
+): string {
+ const result = t(context, id, ...args);
+ return reactToString(result);
+}
+
+function reactToString(el: React.ReactNode): string {
+ if (typeof el === 'string' || typeof el === 'number' || typeof el === 'boolean') {
+ return `${el}`;
+ }
+
+ if (el === null || el === undefined) {
+ return '';
+ }
+
+ if (Array.isArray(el)) {
+ return el.map(reactToString).join('');
+ }
+
+ if ('props' in el) {
+ return el.props.children.map(reactToString).join('');
+ }
+
+ throw new Error(`Unsupported type ${typeof el}`);
+}
diff --git a/src/translations/en.json b/src/translations/en.json
new file mode 100644
index 000000000..4bdbb8b8a
--- /dev/null
+++ b/src/translations/en.json
@@ -0,0 +1,8 @@
+{
+ "powered_by_gitbook": "Powered by GitBook",
+ "switch_to_dark_theme": "Switch to dark theme",
+ "switch_to_light_theme": "Switch to light theme",
+ "switch_to_system_theme": "Switch to system theme",
+ "search": "Search",
+ "on_this_page": "On this page"
+}
diff --git a/src/translations/index.ts b/src/translations/index.ts
new file mode 100644
index 000000000..8ae0b0f9b
--- /dev/null
+++ b/src/translations/index.ts
@@ -0,0 +1,3 @@
+import en from './en.json';
+
+export { en };