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..59860539 --- /dev/null +++ b/playgrounds/i18n/vocs.config.ts @@ -0,0 +1,137 @@ +import { defineConfig } from '../../src/index.js' + +export default defineConfig({ + editLink: { + // NOTE: the order is important + '/': { + pattern: 'https://github.com/wagmi-dev/vocs/edit/main/site/pages/:path', + text: 'Suggest changes to this page', + // Optional + // lastUpdated: 'Last updated' + }, + '/zh': { + pattern: 'https://github.com/wagmi-dev/vocs/edit/main/site/pages/:path', + text: '建议对此页面进行更改', + lastUpdated: '最后更新时间', + }, + }, + footerNav: { + // NOTE: the order is important + '/': { + previous: 'Previous', + next: 'Next', + }, + '/zh': { + previous: '以前的', + next: '下一个', + }, + }, + search: { + // NOTE: the order is important + '/': { + placeholder: 'Search', + navigate: 'Navigate', + select: 'Select', + close: 'Close', + reset: 'Reset', + noResults: 'No results for', + labelClose: 'Close search dialog', + }, + '/zh': { + placeholder: '搜索', + navigate: '导航', + select: '选择', + close: '关闭', + reset: '重置', + noResults: '没有结果', + labelClose: '关闭搜索对话框', + }, + }, + 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: + // NOTE: the order is important + { + '/': 'i18n', + '/zh': 'i18n 中文', + }, + description: + // NOTE: the order is important and this will show up when mdx description not defined + { + '/': 'English description', + '/zh': '中文说明', + }, + topNav: { + '/': [ + { + text: 'Overview', + link: '/', + }, + { + text: 'Example', + items: [ + { + text: 'Translated', + link: '/translated', + }, + ], + }, + ], + '/zh': [ + { + text: '概述', + link: '/zh', + }, + { + text: '例子', + items: [ + { + text: '已翻译', + link: '/zh/translated', + }, + ], + }, + ], + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3c41c40..e0a3a59a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -171,6 +171,21 @@ importers: specifier: workspace:* version: link:../../src + playgrounds/i18n: + dependencies: + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + react-router-dom: + specifier: ^6.17.0 + version: 6.20.0(react-dom@18.2.0)(react@18.2.0) + vocs: + specifier: workspace:* + version: link:../../src + playgrounds/op-stack: dependencies: react: diff --git a/src/app/components/DesktopSearch.tsx b/src/app/components/DesktopSearch.tsx index 6410b0ac..378db626 100644 --- a/src/app/components/DesktopSearch.tsx +++ b/src/app/components/DesktopSearch.tsx @@ -2,6 +2,8 @@ import * as Dialog from '@radix-ui/react-dialog' import { MagnifyingGlassIcon } from '@radix-ui/react-icons' import { useEffect, useState } from 'react' +import { useLocation } from 'react-router-dom' +import { useConfig } from '../hooks/useConfig.js' import { useSearchIndex } from '../hooks/useSearchIndex.js' import * as styles from './DesktopSearch.css.js' import { SearchDialog } from './SearchDialog.js' @@ -9,6 +11,19 @@ import { SearchDialog } from './SearchDialog.js' export function DesktopSearch() { useSearchIndex() const [open, setOpen] = useState(false) + const config = useConfig() + const { pathname } = useLocation() + + let pathKey = '' + if (typeof config?.title === 'object' && Object.keys(config?.title ?? {}).length > 0) { + let keys: string[] = [] + keys = Object.keys(config?.title).filter((key) => pathname.startsWith(key)) + pathKey = keys[keys.length - 1] + } + + const configSearch = !config.search?.placeholder + ? (config?.search as any)?.[pathKey] + : config.search useEffect(() => { function keyDownHandler(event: KeyboardEvent) { @@ -37,7 +52,7 @@ export function DesktopSearch() { diff --git a/src/app/components/MobileTopNav.css.ts b/src/app/components/MobileTopNav.css.ts index 68eb19b1..f8537c78 100644 --- a/src/app/components/MobileTopNav.css.ts +++ b/src/app/components/MobileTopNav.css.ts @@ -48,11 +48,18 @@ export const button = style( 'button', ) -export const content = style( +export const contentLeft = style( { left: `calc(-1 * ${spaceVars['24']})`, }, - 'content', + 'contentLeft', +) + +export const contentRight = style( + { + right: `calc(-1 * ${spaceVars['24']})`, + }, + 'contentRight', ) export const curtain = style( diff --git a/src/app/components/MobileTopNav.tsx b/src/app/components/MobileTopNav.tsx index f14ee1d9..73c42265 100644 --- a/src/app/components/MobileTopNav.tsx +++ b/src/app/components/MobileTopNav.tsx @@ -25,6 +25,7 @@ import { ChevronRight } from './icons/ChevronRight.js' import { ChevronUp } from './icons/ChevronUp.js' import { Discord } from './icons/Discord.js' import { GitHub } from './icons/GitHub.js' +import { Language } from './icons/Language.js' import { Menu } from './icons/Menu.js' import { Telegram } from './icons/Telegram.js' import { X } from './icons/X.js' @@ -33,8 +34,17 @@ MobileTopNav.Curtain = Curtain export function MobileTopNav() { const config = useConfig() + const { pathname } = useLocation() const { showLogo } = useLayout() + let pathKey = '' + if (typeof config.topNav === 'object' && Object.keys(config.topNav ?? {}).length > 0) { + let keys: string[] = [] + keys = Object.keys(config.topNav).filter((key) => pathname.startsWith(key)) + pathKey = keys[keys.length - 1] + } + const configTopNav = Array.isArray(config.topNav) ? config.topNav : config?.topNav?.[pathKey] + return (
@@ -47,17 +57,28 @@ export function MobileTopNav() {
)} - {config.topNav && ( + {configTopNav && ( <>
- - + +
)}
+ {config.defaultLocale?.label && + config.defaultLocale.lang && + config.locales && + Object.keys(config.locales).length > 0 && ( + <> +
+ + +
+ + )}
@@ -76,6 +97,79 @@ export function MobileTopNav() { ) } +function NavigationLocale() { + const config = useConfig() + const { pathname } = useLocation() + /** + * + * @param item + * @param lang + * @returns + */ + const removeLocalePrefix = (item: Config.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}`, + ) + }), + ]} + /> + + + + + ) +} + function Navigation({ items }: { items: Config.ParsedTopNavItem[] }) { const { pathname } = useLocation() const activeIds = useActiveNavIds({ pathname, items }) @@ -92,7 +186,7 @@ function Navigation({ items }: { items: Config.ParsedTopNavItem[] }) { {item.text} - + @@ -117,6 +211,104 @@ function NavigationMenuContent({ items }: { items: Config.ParsedTopNavItem[] }) ) } +function CompactNavigationLocale() { + const config = useConfig() + const { pathname } = useLocation() + + /** + * + * @param item + * @param lang + * @returns + */ + const removeLocalePrefix = (item: Config.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, '') ?? ''}`, + } + } + + const items = [ + ...(config?.defaultLocale?.label && config?.defaultLocale?.lang + ? [ + removeLocalePrefix( + { + id: 0, + text: `${config?.defaultLocale?.label}`, + link: `${pathname}`, + }, + '', + ), + ] + : []), + ...Object.keys(config?.locales ?? {}).map((locale, key) => { + return removeLocalePrefix( + { + id: key + (config?.defaultLocale?.label ? 1 : 0), + text: `${config.locales?.[locale].label}`, + link: `${pathname}`, + }, + `${config.locales?.[locale].lang}`, + ) + }), + ] + const [showPopover, setShowPopover] = useState(false) + const activeIds = useActiveNavIds({ pathname, items }) + const activeItem = items.filter((item) => item.id === activeIds[0])[0] + + return ( +
+ {activeItem ? ( + + + } + /> + + + + + {items.map((item, i) => ( + setShowPopover(false)} + variant="styleless" + > + {item.text} + + ))} + + + + ) : items[0]?.link ? ( + + {items[0].text} + + ) : null} +
+ ) +} + function CompactNavigation({ items }: { items: Config.ParsedTopNavItem[] }) { const [showPopover, setShowPopover] = useState(false) diff --git a/src/app/components/NavLogo.tsx b/src/app/components/NavLogo.tsx index a63e0118..74acc2db 100644 --- a/src/app/components/NavLogo.tsx +++ b/src/app/components/NavLogo.tsx @@ -1,10 +1,20 @@ +import { useLocation } from 'react-router-dom' import { useConfig } from '../hooks/useConfig.js' import { Logo } from './Logo.js' import * as styles from './NavLogo.css.js' export function NavLogo() { const config = useConfig() + const { pathname } = useLocation() + + let pathKey = '' + if (typeof config?.title === 'object' && Object.keys(config?.title ?? {}).length > 0) { + let keys: string[] = [] + keys = Object.keys(config?.title).filter((key) => pathname.startsWith(key)) + pathKey = keys[keys.length - 1] + } + const configTitle = typeof config?.title === 'object' ? config?.title?.[pathKey] : config?.title if (config.logoUrl) return - return
{config.title}
+ return
{configTitle}
} diff --git a/src/app/components/SearchDialog.tsx b/src/app/components/SearchDialog.tsx index 8b6d994f..f26a9038 100644 --- a/src/app/components/SearchDialog.tsx +++ b/src/app/components/SearchDialog.tsx @@ -2,17 +2,18 @@ import * as Dialog from '@radix-ui/react-dialog' import { ArrowLeftIcon, ChevronRightIcon, + FileIcon, ListBulletIcon, MagnifyingGlassIcon, - FileIcon, } from '@radix-ui/react-icons' import * as Label from '@radix-ui/react-label' import clsx from 'clsx' import { default as Mark } from 'mark.js' import { type SearchResult } from 'minisearch' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Link, useNavigate } from 'react-router-dom' +import { Link, useLocation, useNavigate } from 'react-router-dom' +import { useConfig } from '../hooks/useConfig.js' import { useDebounce } from '../hooks/useDebounce.js' import { useLocalStorage } from '../hooks/useLocalStorage.js' import { type Result, useSearchIndex } from '../hooks/useSearchIndex.js' @@ -23,6 +24,19 @@ import * as styles from './SearchDialog.css.js' export function SearchDialog(props: { open: boolean; onClose(): void }) { const navigate = useNavigate() + const config = useConfig() + const { pathname } = useLocation() + let pathKey = '' + if (typeof config?.title === 'object' && Object.keys(config?.title ?? {}).length > 0) { + let keys: string[] = [] + keys = Object.keys(config?.title).filter((key) => pathname.startsWith(key)) + pathKey = keys[keys.length - 1] + } + + const configSearch = !config.search?.placeholder + ? (config?.search as any)?.[pathKey] + : config.search + const inputRef = useRef(null) const listRef = useRef(null) @@ -148,11 +162,13 @@ export function SearchDialog(props: { open: boolean; onClose(): void }) { className={styles.root} aria-describedby={undefined} > - Search + + {configSearch?.placeholder ?? 'Search'} +