From f325d96c52be82f1b0f38359dc01e0f83f714818 Mon Sep 17 00:00:00 2001 From: codingwithmanny Date: Sun, 11 Feb 2024 21:36:24 +0300 Subject: [PATCH 1/6] feat: WIP i18n Added support and examples for supporting multiple languages BREAKING CHANGE: N --- playgrounds/i18n/docs/pages/index.mdx | 13 + .../i18n/docs/pages/translated/index.mdx | 3 + playgrounds/i18n/docs/pages/zh/index.mdx | 13 + .../i18n/docs/pages/zh/translated/index.mdx | 3 + playgrounds/i18n/docs/styles.css | 3 + playgrounds/i18n/package.json | 20 + playgrounds/i18n/vocs.config.ts | 69 ++++ src/app/components/DesktopTopNav.tsx | 228 ++++++++--- src/app/components/Sidebar.tsx | 200 +++++---- src/app/components/icons/Language.tsx | 9 + src/app/hooks/useLocale.ts | 29 ++ src/app/hooks/useSidebar.ts | 42 +- src/config.ts | 387 ++++++++++-------- 13 files changed, 687 insertions(+), 332 deletions(-) create mode 100644 playgrounds/i18n/docs/pages/index.mdx create mode 100644 playgrounds/i18n/docs/pages/translated/index.mdx create mode 100644 playgrounds/i18n/docs/pages/zh/index.mdx create mode 100644 playgrounds/i18n/docs/pages/zh/translated/index.mdx create mode 100644 playgrounds/i18n/docs/styles.css create mode 100644 playgrounds/i18n/package.json create mode 100644 playgrounds/i18n/vocs.config.ts create mode 100644 src/app/components/icons/Language.tsx create mode 100644 src/app/hooks/useLocale.ts diff --git a/playgrounds/i18n/docs/pages/index.mdx b/playgrounds/i18n/docs/pages/index.mdx new file mode 100644 index 00000000..41637db6 --- /dev/null +++ b/playgrounds/i18n/docs/pages/index.mdx @@ -0,0 +1,13 @@ +# Overview + +**i18n** is an example implementation of a multi-lingual supported site that allows for more than one language to be supported. + +In the top right, look for this symbol with additional options as a dropdown with different languages offered. + + + Language + + + + + diff --git a/playgrounds/i18n/docs/pages/translated/index.mdx b/playgrounds/i18n/docs/pages/translated/index.mdx new file mode 100644 index 00000000..7b304740 --- /dev/null +++ b/playgrounds/i18n/docs/pages/translated/index.mdx @@ -0,0 +1,3 @@ +# Translated + +An example of a page that is translated. \ No newline at end of file diff --git a/playgrounds/i18n/docs/pages/zh/index.mdx b/playgrounds/i18n/docs/pages/zh/index.mdx new file mode 100644 index 00000000..a7f1a457 --- /dev/null +++ b/playgrounds/i18n/docs/pages/zh/index.mdx @@ -0,0 +1,13 @@ +# 概述 + +**i18n** 是多语言支持站点的示例实现,它允许支持多种语言。 + +在右上角,通过提供不同语言的下拉菜单查找此符号以及其他选项。 + + + Language + + + + + diff --git a/playgrounds/i18n/docs/pages/zh/translated/index.mdx b/playgrounds/i18n/docs/pages/zh/translated/index.mdx new file mode 100644 index 00000000..9616eb1c --- /dev/null +++ b/playgrounds/i18n/docs/pages/zh/translated/index.mdx @@ -0,0 +1,3 @@ +# 已翻译 + +已翻译页面的示例。 \ No newline at end of file diff --git a/playgrounds/i18n/docs/styles.css b/playgrounds/i18n/docs/styles.css new file mode 100644 index 00000000..c9ba9c23 --- /dev/null +++ b/playgrounds/i18n/docs/styles.css @@ -0,0 +1,3 @@ +.vocs_NavLogo_logoImage { + height: 50% !important; +} diff --git a/playgrounds/i18n/package.json b/playgrounds/i18n/package.json new file mode 100644 index 00000000..57570a5b --- /dev/null +++ b/playgrounds/i18n/package.json @@ -0,0 +1,20 @@ +{ + "name": "playgrounds-i18n", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "node --import tsx/esm ../../src/cli/index.ts dev", + "build": "NODE_ENV=production node --import tsx/esm ../../src/cli/index.ts build", + "preview": "node --import tsx/esm ../../src/cli/index.ts preview", + "dist:dev": "vocs dev", + "dist:build": "NODE_ENV=production vocs build", + "dist:preview": "vocs preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.17.0", + "vocs": "workspace:*" + } +} diff --git a/playgrounds/i18n/vocs.config.ts b/playgrounds/i18n/vocs.config.ts new file mode 100644 index 00000000..9b56a8e5 --- /dev/null +++ b/playgrounds/i18n/vocs.config.ts @@ -0,0 +1,69 @@ +import { defineConfig } from "../../src/index.js"; + +export default defineConfig({ + sidebar: { + // NOTE: the order is important + "/": [ + { + text: "Overview", + link: "/", + }, + { + text: "Example", + items: [ + { + text: "Translated", + link: "/translated", + }, + ], + }, + ], + "/zh": [ + { + text: "概述", + link: "/zh", + }, + { + text: "例子", + items: [ + { + text: "已翻译", + link: "/zh/translated", + }, + ], + }, + ], + }, + defaultLocale: { + label: "English", + lang: "en", + }, + locales: { + something: { + label: "简体中文", + lang: "zh", + }, + }, + title: "i18n", + // topNav: { + // '/': [ + // { + // tsext: "Company", + // items: [ + // { + // text: "About", + // link: "/about", + // }, + // { + // text: "Blog", + // link: "/about", + // }, + // { + // text: "Careers", + // link: "/about", + // }, + // ], + // }, + // ] + // } +}); diff --git a/src/app/components/DesktopTopNav.tsx b/src/app/components/DesktopTopNav.tsx index 1fea974a..3037fa63 100644 --- a/src/app/components/DesktopTopNav.tsx +++ b/src/app/components/DesktopTopNav.tsx @@ -1,34 +1,38 @@ -import clsx from 'clsx' -import { type ComponentType } from 'react' -import { useLocation } from 'react-router-dom' - -import type { ParsedSocialItem, ParsedTopNavItem } from '../../config.js' -import { useActiveNavIds } from '../hooks/useActiveNavIds.js' -import { useConfig } from '../hooks/useConfig.js' -import { useLayout } from '../hooks/useLayout.js' -import { useTheme } from '../hooks/useTheme.js' -import { visibleDark, visibleLight } from '../styles/utils.css.js' -import { DesktopSearch } from './DesktopSearch.js' -import * as styles from './DesktopTopNav.css.js' -import { Icon } from './Icon.js' -import { NavLogo } from './NavLogo.js' -import * as NavigationMenu from './NavigationMenu.js' -import { RouterLink } from './RouterLink.js' -import { Discord } from './icons/Discord.js' -import { GitHub } from './icons/GitHub.js' -import { Moon } from './icons/Moon.js' -import { Sun } from './icons/Sun.js' -import { Telegram } from './icons/Telegram.js' -import { X } from './icons/X.js' - -DesktopTopNav.Curtain = Curtain +import clsx from "clsx"; +import { type ComponentType } from "react"; +import { useLocation } from "react-router-dom"; + +import type { ParsedSocialItem, ParsedTopNavItem } from "../../config.js"; +import { useActiveNavIds } from "../hooks/useActiveNavIds.js"; +import { useConfig } from "../hooks/useConfig.js"; +import { useLayout } from "../hooks/useLayout.js"; +import { useTheme } from "../hooks/useTheme.js"; +import { visibleDark, visibleLight } from "../styles/utils.css.js"; +import { DesktopSearch } from "./DesktopSearch.js"; +import * as styles from "./DesktopTopNav.css.js"; +import { Icon } from "./Icon.js"; +import { NavLogo } from "./NavLogo.js"; +import * as NavigationMenu from "./NavigationMenu.js"; +import { RouterLink } from "./RouterLink.js"; +import { Discord } from "./icons/Discord.js"; +import { GitHub } from "./icons/GitHub.js"; +import { Moon } from "./icons/Moon.js"; +import { Sun } from "./icons/Sun.js"; +import { Telegram } from "./icons/Telegram.js"; +import { X } from "./icons/X.js"; +import { Language } from "./icons/Language.js"; +import { useLocale } from "../hooks/useLocale.js"; + +DesktopTopNav.Curtain = Curtain; export function DesktopTopNav() { - const config = useConfig() - const { showLogo, showSidebar } = useLayout() + const config = useConfig(); + const { showLogo, showSidebar } = useLayout(); return ( -
+
{showLogo && ( @@ -36,7 +40,12 @@ export function DesktopTopNav() {
@@ -56,11 +65,24 @@ export function DesktopTopNav() { )} + {config.defaultLocale && + config.defaultLocale.label && + config.defaultLocale.lang && + config.locales && + Object.keys(config.locales).length > 0 && ( + <> +
+ +
+
+ + )} + {config.socials && config.socials?.length > 0 && ( <>
{config.socials.map((social, i) => (
@@ -77,7 +99,7 @@ export function DesktopTopNav() { {!config.theme?.colorScheme && (
@@ -86,19 +108,20 @@ export function DesktopTopNav() { )}
- ) + ); } export function Curtain() { - return
+ return
; } function Navigation() { - const { topNav } = useConfig() - if (!topNav) return null + const { topNav } = useConfig(); + if (!topNav) return null; - const { pathname } = useLocation() - const activeIds = useActiveNavIds({ pathname, items: topNav }) + const { pathname } = useLocation(); + const { locale } = useLocale(); + const activeIds = useActiveNavIds({ pathname, items: topNav }); return ( @@ -109,7 +132,7 @@ function Navigation() { key={i} active={activeIds.includes(item.id)} className={styles.item} - href={item.link!} + href={`${locale ? `/${locale}` : ""}${item.link!}`} > {item.text} @@ -122,41 +145,131 @@ function Navigation() { - ) : null, + ) : null )} - ) + ); } function NavigationMenuContent({ items }: { items: ParsedTopNavItem[] }) { - const { pathname } = useLocation() - const activeIds = useActiveNavIds({ pathname, items }) + const { pathname } = useLocation(); + const activeIds = useActiveNavIds({ pathname, items }); return (
    {items?.map((item, i) => ( - + {item.text} ))}
+ ); +} + +// START +function NavigationLocale() { + const config = useConfig(); + const { pathname } = useLocation(); + /** + * + * @param item + * @param lang + * @returns + */ + const removeLocalePrefix = (item: ParsedTopNavItem, lang: string) => { + // Get all language prefixes + const prefixLocales = [ + config?.defaultLocale?.lang, + ...Object.keys(config?.locales ?? {}).map((i) => + config?.locales ? config.locales[i]?.lang : null + ), + ]; + // Regex for removal + const regexString = `^\/(${prefixLocales.join("|")})`; + const regex = new RegExp(regexString); + return { + ...item, + link: `${lang ? `/${lang}` : ""}${item.link?.replace(regex, "") ?? ""}`, + }; + }; + + if ( + !( + config.locales || + (config.locales && Object.keys(config.locales).length === 0) + ) ) + return null; + return ( + + + + + } + /> + + + { + return removeLocalePrefix( + { + id: key + (config?.defaultLocale?.label ? 1 : 0), + text: `${config.locales?.[locale].label}`, + link: `${pathname}`, + }, + `${config.locales?.[locale].lang}` + ); + }), + ]} + /> + + + + + ); } +// END function ThemeToggleButton() { - const { toggle } = useTheme() + const { toggle } = useTheme(); return ( - ) + ); } const iconsForIcon = { @@ -164,24 +277,29 @@ const iconsForIcon = { github: GitHub, telegram: Telegram, x: X, -} satisfies Record +} satisfies Record; const sizesForType = { - discord: '23px', - github: '20px', - telegram: '21px', - x: '18px', -} satisfies Record + discord: "23px", + github: "20px", + telegram: "21px", + x: "18px", +} satisfies Record; function SocialButton({ icon, label, link }: ParsedSocialItem) { return ( - + - ) + ); } diff --git a/src/app/components/Sidebar.tsx b/src/app/components/Sidebar.tsx index 0b67a1b2..abd8514a 100644 --- a/src/app/components/Sidebar.tsx +++ b/src/app/components/Sidebar.tsx @@ -1,4 +1,4 @@ -import clsx from 'clsx' +import clsx from "clsx"; import { type KeyboardEvent, type MouseEvent, @@ -9,45 +9,54 @@ import { useMemo, useRef, useState, -} from 'react' -import { matchPath, useLocation, useMatch } from 'react-router-dom' +} from "react"; +import { matchPath, useLocation, useMatch } from "react-router-dom"; -import { type SidebarItem as SidebarItemType } from '../../config.js' -import { usePageData } from '../hooks/usePageData.js' -import { useSidebar } from '../hooks/useSidebar.js' -import { Icon } from './Icon.js' -import { NavLogo } from './NavLogo.js' -import { RouterLink } from './RouterLink.js' -import * as styles from './Sidebar.css.js' -import { ChevronRight } from './icons/ChevronRight.js' +import { type SidebarItem as SidebarItemType } from "../../config.js"; +import { usePageData } from "../hooks/usePageData.js"; +import { useSidebar } from "../hooks/useSidebar.js"; +import { Icon } from "./Icon.js"; +import { NavLogo } from "./NavLogo.js"; +import { RouterLink } from "./RouterLink.js"; +import * as styles from "./Sidebar.css.js"; +import { ChevronRight } from "./icons/ChevronRight.js"; +import { useLocale } from "../hooks/useLocale.js"; export function Sidebar(props: { - className?: string - onClickItem?: MouseEventHandler + className?: string; + onClickItem?: MouseEventHandler; }) { - const { className, onClickItem } = props + const { className, onClickItem } = props; - const { previousPath } = usePageData() - const sidebarRef = useRef(null) - const sidebar = useSidebar() - const [backPath, setBackPath] = useState('/') + const { previousPath } = usePageData(); + const sidebarRef = useRef(null); + const sidebar = useSidebar(); + const [backPath, setBackPath] = useState("/"); + const { locale } = useLocale(); // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { - if (typeof window === 'undefined') return - if (!previousPath) return - setBackPath(previousPath) - }, [sidebar.key, sidebar.backLink]) + if (typeof window === "undefined") return; + if (!previousPath) return; + setBackPath(previousPath); + }, [sidebar.key, sidebar.backLink]); - if (!sidebar) return null + if (!sidebar) return null; - const groups = getSidebarGroups(sidebar.items) + const groups = getSidebarGroups(sidebar.items); return ( -