diff --git a/jsconfig.json b/jsconfig.json
index e401488e3..1a068ad8b 100644
--- a/jsconfig.json
+++ b/jsconfig.json
@@ -9,5 +9,6 @@
"noEmit": true,
"allowJs": true,
"checkJs": true
- }
+ },
+ "exclude": ["node_modules", "build"]
}
diff --git a/package.json b/package.json
index 7db6ef166..b05073ba1 100644
--- a/package.json
+++ b/package.json
@@ -7,8 +7,8 @@
"node": ">=20"
},
"scripts": {
- "dev": "vite --port 8080",
- "server": "vite preview --port 8080",
+ "serve:dev": "vite --port 3000",
+ "serve:prod": "vite preview --port 3000",
"build": "vite build",
"lint": "eslint src test",
"format": "prettier --write \"{src,test}/**/*.{css,js,json}\"",
diff --git a/src/components/blog-overview/index.jsx b/src/components/blog-overview/index.jsx
index 88d94a578..4905138b2 100644
--- a/src/components/blog-overview/index.jsx
+++ b/src/components/blog-overview/index.jsx
@@ -1,42 +1,55 @@
-import config from '../../config.json';
-import { useLanguage, useTranslation, getRouteName } from '../../lib/i18n';
+import { blogPosts } from '../../route-config.js';
+import { useTranslate, useBlogTranslate } from '../../lib/i18n';
import { Time } from '../time';
import { prefetchContent } from '../../lib/use-content';
import { BlogPage } from '../routes.jsx';
import s from './style.module.css';
export default function BlogOverview() {
- const [lang] = useLanguage();
- const continueReading = useTranslation('continueReading');
+ const translate = useTranslate();
+ const translateBlog = useBlogTranslate();
- return (
-
-
- {config.blog.map(post => {
- const name = getRouteName(post, lang);
- const excerpt = post.excerpt[lang] || post.excerpt.en;
+ const posts = [];
+ for (const post in blogPosts) {
+ const translatedBlog = translateBlog(
+ /** @type {keyof typeof blogPosts} */ (post)
+ );
+
+ const prefetchAndPreload = () => {
+ BlogPage.preload();
+ prefetchContent(post);
+ };
- const prefetchAndPreload = () => {
- BlogPage.preload();
- prefetchContent(post.path);
- };
+ posts.push(
+
+
+
+
+
+ {translatedBlog.excerpt}
+
+ {translate('i18n', 'continueReading')} →
+
+
+ );
+ }
- return (
-
-
-
-
-
- {excerpt}
-
- {continueReading} →
-
-
- );
- })}
-
+ return (
+
);
}
diff --git a/src/components/content-region/index.jsx b/src/components/content-region/index.jsx
index 24a98020c..a1300146e 100644
--- a/src/components/content-region/index.jsx
+++ b/src/components/content-region/index.jsx
@@ -2,7 +2,7 @@ import { useEffect } from 'preact/hooks';
import Markup from 'preact-markup';
import widgets from '../widgets';
import style from './style.module.css';
-import { useTranslation } from '../../lib/i18n';
+import { useTranslate } from '../../lib/i18n';
import { TocContext } from '../table-of-contents';
import { prefetchContent } from '../../lib/use-content';
import { ReplPage, TutorialPage, CodeEditor } from '../routes';
@@ -43,7 +43,7 @@ function SiblingNav({ route, lang, start }) {
? route.name[lang || 'en']
: route.name || route.title;
}
- const label = useTranslation(start ? 'previous' : 'next');
+ const translate = useTranslate();
return (
@@ -53,7 +53,9 @@ function SiblingNav({ route, lang, start }) {
{title}
- {label}
+
+ {translate('i18n', start ? 'previousPage' : 'nextPage')}
+
);
@@ -73,7 +75,11 @@ export default function ContentRegion({ content, components, ...props }) {
}, [props.current]);
return (
-
+
{content && (
diff --git a/src/components/controllers/guide-page.jsx b/src/components/controllers/guide-page.jsx
index 9bdf3c97a..b248b80c7 100644
--- a/src/components/controllers/guide-page.jsx
+++ b/src/components/controllers/guide-page.jsx
@@ -1,20 +1,19 @@
import { useState, useEffect } from 'preact/hooks';
import { useRoute, ErrorBoundary } from 'preact-iso';
import { useContent } from '../../lib/use-content';
-import { useLanguage } from '../../lib/i18n.jsx';
-import config from '../../config.json';
+import { useLanguageContext } from '../../lib/i18n.jsx';
import { NotFound } from './not-found';
import cx from '../../lib/cx';
import { MarkdownRegion } from './markdown-region';
import Sidebar from '../sidebar';
import Footer from '../footer/index';
-import { docRoutes } from '../../lib/route-utils';
+import { flatDocPages } from '../../route-config.js';
import { LATEST_MAJOR, PREVIEW_MAJOR } from '../doc-version';
import style from './style.module.css';
export function GuidePage() {
const { version, name } = useRoute().params;
- const isValidRoute = docRoutes[version]['/' + name];
+ const isValidRoute = flatDocPages[version]['/' + name];
return (
@@ -57,9 +56,7 @@ function OldDocsWarning() {
}
const outdatedVersion = version !== PREVIEW_MAJOR;
- const latestExists = config.docs[LATEST_MAJOR].some(section =>
- section.routes.some(route => route.path === '/' + name)
- );
+ const latestExists = flatDocPages[LATEST_MAJOR]['/' + name];
return (
@@ -93,7 +90,7 @@ const MAINTAINED_LANGUAGES = ['en', 'ru', 'zh'];
function UnmaintainedTranslationWarning({ meta }) {
const { path, params } = useRoute();
const { name, version } = params;
- const [lang, setLang] = useLanguage();
+ const { lang, setLang } = useLanguageContext();
if (
version !== LATEST_MAJOR ||
diff --git a/src/components/controllers/page.jsx b/src/components/controllers/page.jsx
index 7c7e58b60..ed8b7d070 100644
--- a/src/components/controllers/page.jsx
+++ b/src/components/controllers/page.jsx
@@ -1,15 +1,15 @@
import { useRoute, ErrorBoundary } from 'preact-iso';
-import { navRoutes } from '../../lib/route-utils';
import { useContent } from '../../lib/use-content';
import { NotFound } from './not-found';
import { MarkdownRegion } from './markdown-region';
import Footer from '../footer/index';
import style from './style.module.css';
+import { headerNav } from '../../route-config.js';
// Supports generic pages like `/`, `/about/*`, `/blog`, etc.
export function Page() {
const { path } = useRoute();
- const isValidRoute = navRoutes[path];
+ const isValidRoute = headerNav[path];
return (
diff --git a/src/components/controllers/tutorial-page.jsx b/src/components/controllers/tutorial-page.jsx
index 53e681e98..f147f548a 100644
--- a/src/components/controllers/tutorial-page.jsx
+++ b/src/components/controllers/tutorial-page.jsx
@@ -4,13 +4,13 @@ import { Tutorial } from './tutorial';
import { SolutionProvider } from './tutorial/contexts';
import { NotFound } from './not-found';
import { useContent, prefetchContent } from '../../lib/use-content';
-import { tutorialRoutes } from '../../lib/route-utils';
+import { tutorialPages } from '../../route-config.js';
import style from './tutorial/style.module.css';
export default function TutorialPage() {
const { step } = useRoute().params;
- const isValidRoute = tutorialRoutes[`/tutorial${step ? `/${step}` : ''}`];
+ const isValidRoute = tutorialPages[`/tutorial${step ? `/${step}` : ''}`];
return (
diff --git a/src/components/controllers/tutorial/index.jsx b/src/components/controllers/tutorial/index.jsx
index 1fb99a08a..caa940347 100644
--- a/src/components/controllers/tutorial/index.jsx
+++ b/src/components/controllers/tutorial/index.jsx
@@ -13,8 +13,7 @@ import { TutorialContext, SolutionContext } from './contexts';
import { parseStackTrace } from '../repl/errors';
import cx from '../../../lib/cx';
import { CodeEditor, Runner, ErrorOverlay, Splitter } from '../../routes';
-import { useLanguage } from '../../../lib/i18n';
-import config from '../../../config.json';
+import { useTranslate } from '../../../lib/i18n.jsx';
import { MarkdownRegion } from '../markdown-region';
import style from './style.module.css';
@@ -75,7 +74,6 @@ export function Tutorial({ html, meta }) {
return () => clearTimeout(delay);
}, [editorCode]);
-
const useResult = fn => {
useEffect(() => {
resultHandlers.add(fn);
@@ -197,7 +195,7 @@ export function Tutorial({ html, meta }) {
components={TUTORIAL_COMPONENTS}
/>
- {meta.tutorial?.setup &&
+ {meta.tutorial?.setup && (
- }
+ )}
@@ -217,13 +215,13 @@ export function Tutorial({ html, meta }) {
}
function ButtonContainer({ meta, showCode, help }) {
- const [lang] = useLanguage();
+ const translate = useTranslate();
return (
diff --git a/src/components/doc-version/index.jsx b/src/components/doc-version/index.jsx
index c42cd20c7..9ec53f02d 100644
--- a/src/components/doc-version/index.jsx
+++ b/src/components/doc-version/index.jsx
@@ -1,6 +1,6 @@
import { useCallback } from 'preact/hooks';
import { useLocation, useRoute } from 'preact-iso';
-import { docRoutes } from '../../lib/route-utils.js';
+import { flatDocPages } from '../../route-config.js';
import style from './style.module.css';
export const LATEST_MAJOR = 'v10';
@@ -18,7 +18,7 @@ export default function DocVersion() {
const onChange = useCallback(
e => {
const version = e.currentTarget.value;
- const url = docRoutes[version]?.[`/${name}`]
+ const url = flatDocPages[version]?.[`/${name}`]
? path.replace(/(v\d{1,2})/, version)
: `/guide/${version}/getting-started`;
route(url);
diff --git a/src/components/edit-button/index.jsx b/src/components/edit-button/index.jsx
index 81e80412f..62961d0a3 100644
--- a/src/components/edit-button/index.jsx
+++ b/src/components/edit-button/index.jsx
@@ -1,10 +1,10 @@
import { useRoute } from 'preact-iso';
-import { useLanguage } from '../../lib/i18n';
+import { useLanguageContext } from '../../lib/i18n';
import style from './style.module.css';
export default function EditThisPage({ isFallback }) {
let { path } = useRoute();
- const [lang] = useLanguage();
+ const { lang } = useLanguageContext();
path = !isFallback ? path + '.md' : '';
const editUrl = `https://github.com/preactjs/preact-www/tree/master/content/${lang}${path}`;
diff --git a/src/components/footer/index.jsx b/src/components/footer/index.jsx
index b0fc0429e..b1cf4c5f0 100644
--- a/src/components/footer/index.jsx
+++ b/src/components/footer/index.jsx
@@ -1,6 +1,6 @@
import { useCallback } from 'preact/hooks';
import config from '../../config.json';
-import { useLanguage } from '../../lib/i18n';
+import { useLanguageContext } from '../../lib/i18n';
import { useResource } from '../../lib/use-resource';
import style from './style.module.css';
@@ -39,7 +39,7 @@ function useContributors() {
export default function Footer() {
const contrib = useContributors();
- const [lang, setLang] = useLanguage();
+ const { lang, setLang } = useLanguageContext();
const onSelect = useCallback(e => setLang(e.target.value), [setLang]);
diff --git a/src/components/header/index.jsx b/src/components/header/index.jsx
index ad049d5aa..098e3f9d6 100644
--- a/src/components/header/index.jsx
+++ b/src/components/header/index.jsx
@@ -8,7 +8,11 @@ import ReleaseLink from './gh-version';
import Corner from './corner';
import { useOverlayToggle } from '../../lib/toggle-overlay';
import { useLocation } from 'preact-iso';
-import { useLanguage, useTranslation, useNavTranslation } from '../../lib/i18n';
+import {
+ useLanguageContext,
+ useTranslate,
+ usePathTranslate
+} from '../../lib/i18n';
import { prefetchContent } from '../../lib/use-content';
import { ReplPage, TutorialPage, CodeEditor } from '../routes';
@@ -44,7 +48,7 @@ export default function Header() {
function MainNav() {
const { path, route } = useLocation();
- const about = useNavTranslation('/about');
+ const translatePath = usePathTranslate();
const brandingRedirect = e => {
e.preventDefault();
@@ -66,7 +70,7 @@ function MainNav() {
{isOpen => (
<>
@@ -118,8 +122,23 @@ function SocialLinks() {
}
function LanguagePicker() {
- const [lang, setLang] = useLanguage();
- const selectYourLanguage = useTranslation('selectYourLanguage');
+ const { lang, setLang } = useLanguageContext();
+ const translate = useTranslate();
+
+ const languages = [];
+ if (typeof window !== 'undefined') {
+ for (const language in config.languages) {
+ languages.push(
+
+ );
+ }
+ }
return (
@@ -132,18 +151,9 @@ function LanguagePicker() {
}
- aria-label={selectYourLanguage}
+ aria-label={translate('i18n', 'selectYourLanguage')}
>
- {typeof window !== 'undefined' &&
- Object.keys(config.languages).map(id => (
-
- ))}
+ {languages}
)}
@@ -225,7 +235,7 @@ function NavMenu(props) {
/**
* Button that expands into a menu when clicked. Pass in label & menu items as children.
*
- * @param {ExpandableNavLinkProps & import('preact').JSX.ButtonHTMLAttributes} props
+ * @param {ExpandableNavLinkProps & import('preact').ButtonHTMLAttributes} props
*/
function ExpandableNavLink({ isOpen, label, children, ...rest }) {
return (
@@ -255,20 +265,24 @@ const prefetchAndPreload = href => {
prefetchContent(href);
};
+/**
+ * @typedef {import('../../locales/en.json')} Translations
+ */
+
/**
* @typedef {Object} NavLinkProps
- * @property {string} props.href
+ * @property {keyof typeof import('../../route-config.js').headerNav} props.href
* @property {string} [props.clsx]
* @property {import('preact').ComponentChildren} [props.flair]
* @property {boolean} [props.isOpen]
*/
/**
- * @param {NavLinkProps & import('preact').AnchorHTMLAttributes} props
+ * @param {NavLinkProps & Omit
} props
*/
function NavLink({ href, flair, clsx, isOpen, ...rest }) {
const { path } = useLocation();
- const label = useNavTranslation(href);
+ const translatePath = usePathTranslate();
return (
{flair}
- {label}
+ {translatePath('headerNav', href)}
);
}
diff --git a/src/components/header/search.jsx b/src/components/header/search.jsx
index 113a54c39..034aa824b 100644
--- a/src/components/header/search.jsx
+++ b/src/components/header/search.jsx
@@ -65,14 +65,13 @@ const transformItems = items =>
});
});
+const getLoadingBar = () =>
+ typeof window !== 'undefined' ? document.querySelector('loading-bar') : null;
+
export default function Search() {
const root = useRef(null);
const rendered = useRef(false);
const interactedWith = useRef(false);
- const loadingBar =
- typeof window !== 'undefined'
- ? document.querySelector('loading-bar')
- : null;
const loadDocSearch = () => {
if (!rendered.current) {
@@ -91,7 +90,8 @@ export default function Search() {
waitForDocsearch(root.current).then(docsearchButton => {
rendered.current = true;
- loadingBar.removeAttribute('showing');
+ const loadingBar = getLoadingBar();
+ if (loadingBar) loadingBar.removeAttribute('showing');
if (interactedWith.current) {
docsearchButton.click();
}
@@ -107,6 +107,7 @@ export default function Search() {
// The is sat alongside the router & has it's state controlled by it,
// so while we could create a new context to be able to set it here, direct DOM
// manipulation is a heck of a lot simpler.
+ const loadingBar = getLoadingBar();
loadingBar.setAttribute('showing', 'true');
}
};
diff --git a/src/components/routes.jsx b/src/components/routes.jsx
index dd7eb68c7..218db6b0e 100644
--- a/src/components/routes.jsx
+++ b/src/components/routes.jsx
@@ -3,32 +3,37 @@ import { Router, Route, lazy } from 'preact-iso';
import { Page } from './controllers/page';
import { GuidePage } from './controllers/guide-page';
import { NotFound } from './controllers/not-found';
-import { navRoutes } from '../lib/route-utils';
+import { headerNav } from '../route-config.js';
export const ReplPage = lazy(() => import('./controllers/repl-page'));
export const BlogPage = lazy(() => import('./controllers/blog-page'));
export const TutorialPage = lazy(() => import('./controllers/tutorial-page'));
// Combined 'REPL' components, re-evaluate if any are used outside of the REPL in the future
-export const CodeEditor = lazy(() => import('../lib/repl').then(m => m.CodeEditor));
+export const CodeEditor = lazy(() =>
+ import('../lib/repl').then(m => m.CodeEditor)
+);
export const Runner = lazy(() => import('../lib/repl').then(m => m.Runner));
-export const ErrorOverlay = lazy(() => import('../lib/repl').then(m => m.ErrorOverlay));
+export const ErrorOverlay = lazy(() =>
+ import('../lib/repl').then(m => m.ErrorOverlay)
+);
export const Splitter = lazy(() => import('../lib/repl').then(m => m.Splitter));
const routeChange = url =>
// @ts-ignore
typeof ga === 'function' && ga('send', 'pageview', url);
-const genericRoutes = Object.keys(navRoutes)
- .filter(
- route =>
- !route.startsWith('/guide') &&
- !route.startsWith('/tutorial') &&
- !route.startsWith('/repl')
+const genericRoutes = [];
+for (const route in headerNav) {
+ if (
+ route.startsWith('/guide') ||
+ route.startsWith('/tutorial') ||
+ route.startsWith('/repl')
)
- .map(route => (
-
- ));
+ continue;
+
+ genericRoutes.push();
+}
export default function Routes() {
const [loading, setLoading] = useState(false);
diff --git a/src/components/sidebar/index.jsx b/src/components/sidebar/index.jsx
index 6f55217bd..915e9e917 100644
--- a/src/components/sidebar/index.jsx
+++ b/src/components/sidebar/index.jsx
@@ -1,54 +1,54 @@
import { useRoute } from 'preact-iso';
import DocVersion from '../doc-version';
import SidebarNav from './sidebar-nav';
-import config from '../../config.json';
import { useOverlayToggle } from '../../lib/toggle-overlay';
-import { useLanguage, getRouteName } from '../../lib/i18n';
+import { useTranslate, usePathTranslate } from '../../lib/i18n';
+import { docPages } from '../../route-config.js';
import style from './style.module.css';
export default function Sidebar() {
- const { version } = useRoute().params;
- const [lang] = useLanguage();
+ const {
+ version
+ } = /** @type {{ version: 'v8' | 'v10' | 'v11' }} */ (useRoute().params);
const [open, setOpen] = useOverlayToggle();
+ const translate = useTranslate();
+ const translatePath = usePathTranslate();
const navItems = [];
- const routes = config.docs[version];
- for (let i = 0; i < routes.length; i++) {
- const item = routes[i];
- if (item.routes) {
+ for (const item in docPages[version]) {
+ if (version == 'v8') {
navItems.push({
- text: getRouteName(item, lang),
+ text: translatePath(
+ 'sidebarNav',
+ /** @type {keyof typeof import('../../route-config.js').allPages} */ (item)
+ ),
level: 2,
- href: null,
- routes: item.routes.map(nested => ({
- text: getRouteName(nested, lang),
- level: 3,
- href: `/guide/${version}${nested.path}`
- }))
+ href: `/guide/${version}${item}`
});
} else {
navItems.push({
- text: getRouteName(item, lang),
+ text: translate(
+ 'sidebarSections',
+ /** @type {keyof typeof docPages['v10']} */ (item)
+ ),
level: 2,
- href: `/guide/${version}${item.path}`
+ href: null,
+ routes: Object.keys(docPages[version][item]).map(nested => ({
+ text: translatePath(
+ 'sidebarNav',
+ /** @type {keyof typeof import('../../route-config.js').allPages} */ (nested)
+ ),
+ level: 3,
+ href: `/guide/${version}${nested}`
+ }))
});
}
}
- // TODO: Need to entirely disassociate nav labels from URLs
- const guide = config.nav.find(
- item => item.path === '/guide/v10/getting-started'
- );
- const sectionName = getRouteName(guide, lang);
-
return (
-