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( + + ); + } - return ( - - ); - })} -
+ return ( +
+
{posts}
); } 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 (
{meta.prev && ( - {config.i18n.previous[lang] || config.i18n.previous.en} + {translate('i18n', 'previousPage')} )} {meta.solvable && ( @@ -233,16 +231,14 @@ function ButtonContainer({ meta, showCode, help }) { disabled={!showCode} title="Show solution to this example" > - {config.i18n.tutorial.solve[lang] || - config.i18n.tutorial.solve.en} + {translate('i18n', 'solve')} )} {meta.next && ( {meta.code == false - ? (config.i18n.tutorial.begin[lang] || config.i18n.tutorial.begin.en) - : (config.i18n.next[lang] || config.i18n.next.en) - } + ? translate('i18n', 'beginTutorial') + : translate('i18n', 'nextPage')} )}
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 (
-