@lexibase/rdx-ui 1.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/THIRD_PARTY_NOTICES.md +34 -0
- package/package.json +10 -6
- package/src/components/button.tsx +22 -8
- package/src/components/code-block.tsx +274 -43
- package/src/components/copy-code-button.tsx +1 -76
- package/src/components/dialog.tsx +124 -34
- package/src/components/docs-search-dialog.tsx +29 -14
- package/src/components/docs-search.tsx +9 -5
- package/src/components/docs-sidebar.tsx +9 -7
- package/src/components/docs-toc.tsx +6 -3
- package/src/components/footer-credit.tsx +8 -4
- package/src/components/home.tsx +3 -3
- package/src/components/kbd.tsx +29 -0
- package/src/components/loading-button.tsx +5 -1
- package/src/components/mdx-code-block-view.tsx +77 -0
- package/src/components/mdx-code-block.tsx +108 -0
- package/src/components/mdx-components.tsx +105 -47
- package/src/components/mdx-image.tsx +73 -0
- package/src/components/mode-toggle.tsx +5 -2
- package/src/components/navbar.tsx +13 -9
- package/src/components/not-found.tsx +36 -0
- package/src/components/table.tsx +122 -0
- package/src/components/tabs.tsx +17 -9
- package/src/components/version-dropdown.tsx +9 -2
- package/src/components/version-notice.tsx +7 -5
- package/src/hooks/use-highlighted-code.ts +75 -0
- package/src/lib/highlighter.tsx +124 -0
- package/src/lib/i18n.ts +156 -0
- package/src/providers/locale-provider.tsx +29 -0
- package/src/styles/globals.css +1 -0
- package/src/styles/shadcn.css +650 -0
- package/src/components/button-link.tsx +0 -27
package/src/components/tabs.tsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { useMessages } from '../providers/locale-provider'
|
|
4
|
+
|
|
3
5
|
import { Tabs as TabsPrimitive } from '@base-ui/react/tabs'
|
|
4
6
|
import { Check, Copy } from 'lucide-react'
|
|
5
7
|
import { useEffect, useRef, useState } from 'react'
|
|
@@ -23,8 +25,13 @@ function panelText(node: Node): string {
|
|
|
23
25
|
)
|
|
24
26
|
)
|
|
25
27
|
return ''
|
|
26
|
-
if (node.matches('[data-rdx-code-block]')) {
|
|
27
|
-
|
|
28
|
+
if (node.matches('[data-rdx-code-block], [data-slot="code-block"]')) {
|
|
29
|
+
const pre = node.querySelector('pre')
|
|
30
|
+
const lines = pre?.querySelectorAll('[data-slot="code-block-line-content"]')
|
|
31
|
+
const text = lines?.length
|
|
32
|
+
? Array.from(lines, (line) => line.textContent ?? '').join('\n')
|
|
33
|
+
: pre?.textContent
|
|
34
|
+
return `${node.dataset.rdxCodeValue ?? text ?? ''}\n`
|
|
28
35
|
}
|
|
29
36
|
if (node.tagName === 'PRE') return `${node.textContent ?? ''}\n`
|
|
30
37
|
if (node.tagName === 'BR') return '\n'
|
|
@@ -43,6 +50,7 @@ function panelText(node: Node): string {
|
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
function CopyTabButton() {
|
|
53
|
+
const messages = useMessages()
|
|
46
54
|
const [status, setStatus] = useState<'idle' | 'copied' | 'error'>('idle')
|
|
47
55
|
const pending = useRef(false)
|
|
48
56
|
const mounted = useRef(false)
|
|
@@ -92,13 +100,13 @@ function CopyTabButton() {
|
|
|
92
100
|
type="button"
|
|
93
101
|
variant="ghost"
|
|
94
102
|
size="icon"
|
|
95
|
-
aria-label=
|
|
103
|
+
aria-label={messages.copyTab}
|
|
96
104
|
title={
|
|
97
105
|
status === 'error'
|
|
98
|
-
?
|
|
106
|
+
? messages.copyTabError
|
|
99
107
|
: status === 'copied'
|
|
100
|
-
?
|
|
101
|
-
:
|
|
108
|
+
? messages.copied
|
|
109
|
+
: messages.copyTab
|
|
102
110
|
}
|
|
103
111
|
data-status={status}
|
|
104
112
|
className="mr-1.5 size-7 shrink-0 text-muted-foreground hover:text-foreground data-[status=copied]:text-emerald-700 dark:data-[status=copied]:text-emerald-400 data-[status=error]:text-amber-600 [&>svg]:size-3.5"
|
|
@@ -113,9 +121,9 @@ function CopyTabButton() {
|
|
|
113
121
|
className="sr-only"
|
|
114
122
|
>
|
|
115
123
|
{status === 'copied'
|
|
116
|
-
?
|
|
124
|
+
? messages.tabCopied
|
|
117
125
|
: status === 'error'
|
|
118
|
-
?
|
|
126
|
+
? messages.copyTabError
|
|
119
127
|
: ''}
|
|
120
128
|
</span>
|
|
121
129
|
</>
|
|
@@ -172,7 +180,7 @@ function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
|
|
|
172
180
|
<TabsPrimitive.Panel
|
|
173
181
|
data-slot="tabs-content"
|
|
174
182
|
className={mergeStateClassName(
|
|
175
|
-
'min-w-0 rounded-b-lg p-3 outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring [&>:first-child]:mt-0 [&>:last-child]:mb-0 [&>[data-rdx-code-block]]:my-3 [&_[data-rdx-code-block]>pre]:p-3',
|
|
183
|
+
'min-w-0 rounded-b-lg p-3 outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring [&>:first-child]:mt-0 [&>:last-child]:mb-0 [&>[data-rdx-code-block]]:my-3 [&_[data-rdx-code-block]>pre]:p-3 font-mono',
|
|
176
184
|
className
|
|
177
185
|
)}
|
|
178
186
|
{...props}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { useMessages } from '../providers/locale-provider'
|
|
4
|
+
|
|
3
5
|
import { Archive, Bird, PackageOpen } from 'lucide-react'
|
|
4
6
|
import Link from 'next/link'
|
|
5
7
|
import type { ReactNode } from 'react'
|
|
6
8
|
|
|
9
|
+
import { formatMessage } from '../lib/i18n'
|
|
10
|
+
|
|
7
11
|
import { Button } from './button'
|
|
8
12
|
import {
|
|
9
13
|
DropdownMenu,
|
|
@@ -42,6 +46,7 @@ export function VersionDropdown({
|
|
|
42
46
|
onSelect?: (id: string) => void
|
|
43
47
|
footer?: ReactNode
|
|
44
48
|
}) {
|
|
49
|
+
const messages = useMessages()
|
|
45
50
|
const current =
|
|
46
51
|
options.find((option) => option.id === currentId) ?? options[0]
|
|
47
52
|
if (!current) return null
|
|
@@ -87,7 +92,9 @@ export function VersionDropdown({
|
|
|
87
92
|
variant="outline"
|
|
88
93
|
size="sm"
|
|
89
94
|
className="min-w-0 capitalize sm:min-w-[134px]"
|
|
90
|
-
aria-label={
|
|
95
|
+
aria-label={formatMessage(messages.documentationVersion, {
|
|
96
|
+
version: current.label,
|
|
97
|
+
})}
|
|
91
98
|
/>
|
|
92
99
|
}
|
|
93
100
|
>
|
|
@@ -119,7 +126,7 @@ export function VersionDropdown({
|
|
|
119
126
|
{active.length > 0 && <DropdownMenuSeparator />}
|
|
120
127
|
<DropdownMenuGroup>
|
|
121
128
|
<DropdownMenuLabel className="text-center py-px cursor-default">
|
|
122
|
-
|
|
129
|
+
{messages.archived}
|
|
123
130
|
</DropdownMenuLabel>
|
|
124
131
|
<DropdownMenuSeparator />
|
|
125
132
|
{archived.map(item)}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getMessages } from '../lib/i18n'
|
|
1
2
|
import Link from 'next/link'
|
|
2
3
|
import { ArrowRight } from 'lucide-react'
|
|
3
4
|
|
|
@@ -5,29 +6,30 @@ import { Callout } from './callout'
|
|
|
5
6
|
|
|
6
7
|
/** Presentation only: version selection remains the responsibility of the caller. */
|
|
7
8
|
export function VersionNotice({
|
|
9
|
+
lang,
|
|
8
10
|
kind,
|
|
9
11
|
currentHref,
|
|
10
12
|
}: {
|
|
13
|
+
lang?: string
|
|
11
14
|
kind: 'canary' | 'outdated'
|
|
12
15
|
currentHref: string
|
|
13
16
|
}) {
|
|
17
|
+
const messages = getMessages(lang)
|
|
14
18
|
const isCanary = kind === 'canary'
|
|
15
19
|
return (
|
|
16
20
|
<Callout
|
|
17
21
|
data-rdx-version-notice={kind}
|
|
18
22
|
variant={isCanary ? 'tip' : 'warning'}
|
|
19
|
-
title={isCanary ?
|
|
23
|
+
title={isCanary ? messages.preReleaseTitle : messages.outdatedTitle}
|
|
20
24
|
className="mt-0 mb-5"
|
|
21
25
|
>
|
|
22
26
|
<p>
|
|
23
|
-
{isCanary
|
|
24
|
-
? 'This is a pre-release version of the documentation.'
|
|
25
|
-
: 'This is an outdated version of the documentation.'}{' '}
|
|
27
|
+
{isCanary ? messages.preReleaseMessage : messages.outdatedMessage}{' '}
|
|
26
28
|
<Link
|
|
27
29
|
href={currentHref}
|
|
28
30
|
className="font-medium text-foreground underline decoration-current/40 underline-offset-4 transition-colors hover:decoration-current focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
29
31
|
>
|
|
30
|
-
|
|
32
|
+
{messages.backToCurrent}
|
|
31
33
|
<ArrowRight
|
|
32
34
|
aria-hidden="true"
|
|
33
35
|
className="ml-1 inline size-3.5 align-text-bottom"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
import { loadHighlighter } from '../lib/highlighter'
|
|
4
|
+
|
|
5
|
+
export function useHighlightedCode(value: string, language: string) {
|
|
6
|
+
// A trailing newline would render as an extra empty line.
|
|
7
|
+
const code = value.endsWith('\n') ? value.slice(0, -1) : value
|
|
8
|
+
const [loaded, setLoaded] = React.useState<{
|
|
9
|
+
language: string
|
|
10
|
+
highlighter: Awaited<ReturnType<typeof loadHighlighter>>
|
|
11
|
+
} | null>(null)
|
|
12
|
+
const highlighter = loaded?.language === language ? loaded.highlighter : null
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
let cancelled = false
|
|
16
|
+
|
|
17
|
+
loadHighlighter(language)
|
|
18
|
+
.then((next) => {
|
|
19
|
+
if (!cancelled) {
|
|
20
|
+
setLoaded({ language, highlighter: next })
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
.catch(() => {})
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
cancelled = true
|
|
27
|
+
}
|
|
28
|
+
}, [language])
|
|
29
|
+
|
|
30
|
+
// The complete lines, highlighted. While the code streams, only the new
|
|
31
|
+
// lines are tokenized, starting from the grammar state of the last one.
|
|
32
|
+
const [done, setDone] = React.useState<{
|
|
33
|
+
highlighter: typeof highlighter
|
|
34
|
+
text: string
|
|
35
|
+
lines: React.ReactNode[]
|
|
36
|
+
state?: Parameters<NonNullable<typeof highlighter>['highlight']>[1]
|
|
37
|
+
}>({ highlighter: null, text: '', lines: [] })
|
|
38
|
+
|
|
39
|
+
let next = done
|
|
40
|
+
|
|
41
|
+
if (next.highlighter !== highlighter || !code.startsWith(next.text)) {
|
|
42
|
+
next = { highlighter, text: '', lines: [] }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const end = code.lastIndexOf('\n') + 1
|
|
46
|
+
|
|
47
|
+
if (highlighter && end > next.text.length) {
|
|
48
|
+
const result = highlighter.highlight(
|
|
49
|
+
code.slice(next.text.length, end - 1),
|
|
50
|
+
next.state
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
next = {
|
|
54
|
+
highlighter,
|
|
55
|
+
text: code.slice(0, end),
|
|
56
|
+
lines: [...next.lines, ...result.lines],
|
|
57
|
+
state: result.state,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (next !== done) {
|
|
62
|
+
setDone(next)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The last line is still being written, so it is tokenized on every render.
|
|
66
|
+
const tail = code.slice(end)
|
|
67
|
+
|
|
68
|
+
return React.useMemo(() => {
|
|
69
|
+
if (!highlighter) {
|
|
70
|
+
return code.split('\n')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return [...next.lines, highlighter.highlight(tail, next.state).lines[0]]
|
|
74
|
+
}, [code, highlighter, next, tail])
|
|
75
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
createHighlighterCore,
|
|
4
|
+
type GrammarState,
|
|
5
|
+
type HighlighterCore,
|
|
6
|
+
} from 'shiki/core'
|
|
7
|
+
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
|
|
8
|
+
|
|
9
|
+
// Add the languages you need. Each one loads the first time it is used.
|
|
10
|
+
const languages = {
|
|
11
|
+
bash: () => import('shiki/langs/shellscript.mjs'),
|
|
12
|
+
css: () => import('shiki/langs/css.mjs'),
|
|
13
|
+
diff: () => import('shiki/langs/diff.mjs'),
|
|
14
|
+
html: () => import('shiki/langs/html.mjs'),
|
|
15
|
+
javascript: () => import('shiki/langs/javascript.mjs'),
|
|
16
|
+
json: () => import('shiki/langs/json.mjs'),
|
|
17
|
+
jsx: () => import('shiki/langs/jsx.mjs'),
|
|
18
|
+
markdown: () => import('shiki/langs/markdown.mjs'),
|
|
19
|
+
python: () => import('shiki/langs/python.mjs'),
|
|
20
|
+
sql: () => import('shiki/langs/sql.mjs'),
|
|
21
|
+
tsx: () => import('shiki/langs/tsx.mjs'),
|
|
22
|
+
typescript: () => import('shiki/langs/typescript.mjs'),
|
|
23
|
+
yaml: () => import('shiki/langs/yaml.mjs'),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const aliases: Record<string, keyof typeof languages> = {
|
|
27
|
+
js: 'javascript',
|
|
28
|
+
md: 'markdown',
|
|
29
|
+
py: 'python',
|
|
30
|
+
sh: 'bash',
|
|
31
|
+
shell: 'bash',
|
|
32
|
+
shellscript: 'bash',
|
|
33
|
+
ts: 'typescript',
|
|
34
|
+
yml: 'yaml',
|
|
35
|
+
zsh: 'bash',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let highlighter: Promise<HighlighterCore> | undefined
|
|
39
|
+
|
|
40
|
+
function getHighlighter() {
|
|
41
|
+
highlighter ??= createHighlighterCore({
|
|
42
|
+
themes: [
|
|
43
|
+
import('shiki/themes/github-light.mjs'),
|
|
44
|
+
import('shiki/themes/github-dark.mjs'),
|
|
45
|
+
],
|
|
46
|
+
langs: [],
|
|
47
|
+
engine: createJavaScriptRegexEngine({ forgiving: true }),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return highlighter
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getLanguage(language: string) {
|
|
54
|
+
const name = language.toLowerCase()
|
|
55
|
+
|
|
56
|
+
if (name === 'ansi') {
|
|
57
|
+
return name
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (name in languages) {
|
|
61
|
+
return name as keyof typeof languages
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return aliases[name] ?? null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Loads a language and returns a highlighter for it, or null for a language
|
|
68
|
+
// that is not listed.
|
|
69
|
+
export async function loadHighlighter(language: string) {
|
|
70
|
+
const lang = getLanguage(language)
|
|
71
|
+
|
|
72
|
+
if (!lang) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const shiki = await getHighlighter()
|
|
77
|
+
|
|
78
|
+
if (lang !== 'ansi' && !shiki.getLoadedLanguages().includes(lang)) {
|
|
79
|
+
await shiki.loadLanguage(languages[lang])
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
// Returns one React node per line, and the grammar state after the last
|
|
84
|
+
// line. Pass that state back to continue where the code left off.
|
|
85
|
+
highlight(code: string, state?: GrammarState) {
|
|
86
|
+
const tokenize = (text: string) =>
|
|
87
|
+
shiki.codeToTokens(text, {
|
|
88
|
+
lang,
|
|
89
|
+
themes: { light: 'github-light', dark: 'github-dark' },
|
|
90
|
+
defaultColor: false,
|
|
91
|
+
grammarState: state,
|
|
92
|
+
}).tokens
|
|
93
|
+
|
|
94
|
+
// ANSI has no grammar state, so each line is tokenized on its own and
|
|
95
|
+
// gets the same colors whether it arrives in one piece or streams.
|
|
96
|
+
const tokens =
|
|
97
|
+
lang === 'ansi'
|
|
98
|
+
? code.split('\n').flatMap((line) => tokenize(line))
|
|
99
|
+
: tokenize(code)
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
lines: tokens.map((line) =>
|
|
103
|
+
line.map((token, index) => (
|
|
104
|
+
<span
|
|
105
|
+
key={index}
|
|
106
|
+
style={token.htmlStyle as React.CSSProperties}
|
|
107
|
+
className="text-(--shiki-light) dark:text-(--shiki-dark)"
|
|
108
|
+
>
|
|
109
|
+
{token.content}
|
|
110
|
+
</span>
|
|
111
|
+
))
|
|
112
|
+
),
|
|
113
|
+
state: shiki.getLastGrammarState(tokens),
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Returns one React node per line, or null for a language that is not listed.
|
|
120
|
+
export async function highlightCode(code: string, language: string) {
|
|
121
|
+
const highlighter = await loadHighlighter(language)
|
|
122
|
+
|
|
123
|
+
return highlighter ? highlighter.highlight(code).lines : null
|
|
124
|
+
}
|
package/src/lib/i18n.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/** Shared labels only: document content and author-provided names stay untouched. */
|
|
2
|
+
const en = {
|
|
3
|
+
skipContent: 'Skip to content',
|
|
4
|
+
mainNavigation: 'Main navigation',
|
|
5
|
+
docs: 'Docs',
|
|
6
|
+
documentationPages: 'Documentation pages',
|
|
7
|
+
documentationNavigation: 'Documentation navigation',
|
|
8
|
+
openSidebar: 'Open sidebar',
|
|
9
|
+
closeSidebar: 'Close sidebar',
|
|
10
|
+
previous: 'Previous',
|
|
11
|
+
next: 'Next',
|
|
12
|
+
previousPage: 'Previous page',
|
|
13
|
+
nextPage: 'Next page',
|
|
14
|
+
onThisPage: 'On this page',
|
|
15
|
+
search: 'Search…',
|
|
16
|
+
searchDocumentation: 'Search documentation',
|
|
17
|
+
searchPlaceholder: 'Search documentation…',
|
|
18
|
+
searchDescription:
|
|
19
|
+
'Search pages and sections in {version}. Use arrow keys to choose a result and Enter to open it.',
|
|
20
|
+
loadingSearch: 'Loading search…',
|
|
21
|
+
searchUnavailable: 'Search is unavailable.',
|
|
22
|
+
tryAgain: 'Try again',
|
|
23
|
+
typeToSearch: 'Type to search this documentation.',
|
|
24
|
+
noResults: 'No results found.',
|
|
25
|
+
navigate: 'Navigate',
|
|
26
|
+
open: 'Open',
|
|
27
|
+
close: 'Close',
|
|
28
|
+
closeSearch: 'Close search',
|
|
29
|
+
enlargeImage: 'Enlarge image: {alt}',
|
|
30
|
+
closeImage: 'Close image',
|
|
31
|
+
imagePreviewDescription:
|
|
32
|
+
'Press Escape or use the close button to return to the document.',
|
|
33
|
+
copyCode: 'Copy code',
|
|
34
|
+
wrapCode: 'Wrap lines',
|
|
35
|
+
codeCopied: 'Code copied to clipboard.',
|
|
36
|
+
copyCodeError: 'Could not copy. Select the code and copy it manually.',
|
|
37
|
+
copyRetry: 'Could not copy. Try again or copy manually.',
|
|
38
|
+
copied: 'Copied!',
|
|
39
|
+
copyTab: 'Copy active tab',
|
|
40
|
+
tabCopied: 'Tab content copied to clipboard.',
|
|
41
|
+
copyTabError: 'Could not copy. Select the tab content and copy it manually.',
|
|
42
|
+
code: 'Code',
|
|
43
|
+
folderTree: 'Folder',
|
|
44
|
+
table: 'Table',
|
|
45
|
+
current: 'Current',
|
|
46
|
+
version: 'Version {version}',
|
|
47
|
+
documentationVersion: 'Documentation version: {version}',
|
|
48
|
+
archived: 'Archived',
|
|
49
|
+
archivedVersion: 'Archived',
|
|
50
|
+
loadingVersions: 'Loading versions',
|
|
51
|
+
preReleaseTitle: 'Pre-release documentation',
|
|
52
|
+
outdatedTitle: 'Outdated documentation',
|
|
53
|
+
preReleaseMessage: 'This is a pre-release version of the documentation.',
|
|
54
|
+
outdatedMessage: 'This is an outdated version of the documentation.',
|
|
55
|
+
backToCurrent: 'Go back to the current version',
|
|
56
|
+
poweredBy: 'Powered by',
|
|
57
|
+
developedBy: 'Developed by',
|
|
58
|
+
modeToggle: 'Mode Toggle',
|
|
59
|
+
documentationReady: 'Your documentation is ready.',
|
|
60
|
+
addDocument: 'Add an .mdx file inside docs/.',
|
|
61
|
+
notFound: 'Page not found',
|
|
62
|
+
notFoundDescription: "We couldn't find what you were looking for.",
|
|
63
|
+
brokenLink:
|
|
64
|
+
'Please contact the owner of the site that led you to this link and let them know the link is broken.',
|
|
65
|
+
backToDocs: 'Go back to documentation',
|
|
66
|
+
} satisfies Record<string, string>
|
|
67
|
+
|
|
68
|
+
export type InterfaceMessages = { [Key in keyof typeof en]: string }
|
|
69
|
+
export type InterfaceLocale = 'en' | 'pt-BR'
|
|
70
|
+
|
|
71
|
+
const ptBR = {
|
|
72
|
+
skipContent: 'Ir para o conteúdo',
|
|
73
|
+
mainNavigation: 'Navegação principal',
|
|
74
|
+
docs: 'Docs',
|
|
75
|
+
documentationPages: 'Páginas da documentação',
|
|
76
|
+
documentationNavigation: 'Navegação da documentação',
|
|
77
|
+
openSidebar: 'Abrir menu lateral',
|
|
78
|
+
closeSidebar: 'Fechar menu lateral',
|
|
79
|
+
previous: 'Anterior',
|
|
80
|
+
next: 'Próxima',
|
|
81
|
+
previousPage: 'Página anterior',
|
|
82
|
+
nextPage: 'Próxima página',
|
|
83
|
+
onThisPage: 'Nesta página',
|
|
84
|
+
search: 'Buscar…',
|
|
85
|
+
searchDocumentation: 'Buscar na documentação',
|
|
86
|
+
searchPlaceholder: 'Buscar na documentação…',
|
|
87
|
+
searchDescription:
|
|
88
|
+
'Busque páginas e seções em {version}. Use as setas para escolher um resultado e Enter para abri-lo.',
|
|
89
|
+
loadingSearch: 'Carregando busca…',
|
|
90
|
+
searchUnavailable: 'A busca está indisponível.',
|
|
91
|
+
tryAgain: 'Tentar novamente',
|
|
92
|
+
typeToSearch: 'Digite para buscar nesta documentação.',
|
|
93
|
+
noResults: 'Nenhum resultado encontrado.',
|
|
94
|
+
navigate: 'Navegar',
|
|
95
|
+
open: 'Abrir',
|
|
96
|
+
close: 'Fechar',
|
|
97
|
+
closeSearch: 'Fechar busca',
|
|
98
|
+
enlargeImage: 'Ampliar imagem: {alt}',
|
|
99
|
+
closeImage: 'Fechar imagem',
|
|
100
|
+
imagePreviewDescription:
|
|
101
|
+
'Pressione Escape ou use o botão de fechar para voltar ao documento.',
|
|
102
|
+
copyCode: 'Copiar código',
|
|
103
|
+
wrapCode: 'Quebrar linhas',
|
|
104
|
+
codeCopied: 'Código copiado para a área de transferência.',
|
|
105
|
+
copyCodeError:
|
|
106
|
+
'Não foi possível copiar. Selecione o código e copie manualmente.',
|
|
107
|
+
copyRetry: 'Não foi possível copiar. Tente novamente ou copie manualmente.',
|
|
108
|
+
copied: 'Copiado!',
|
|
109
|
+
copyTab: 'Copiar aba ativa',
|
|
110
|
+
tabCopied: 'Conteúdo da aba copiado para a área de transferência.',
|
|
111
|
+
copyTabError:
|
|
112
|
+
'Não foi possível copiar. Selecione o conteúdo da aba e copie manualmente.',
|
|
113
|
+
code: 'Código',
|
|
114
|
+
folderTree: 'Pastas',
|
|
115
|
+
table: 'Tabela',
|
|
116
|
+
current: 'Atual',
|
|
117
|
+
version: 'Versão {version}',
|
|
118
|
+
documentationVersion: 'Versão da documentação: {version}',
|
|
119
|
+
archived: 'Arquivadas',
|
|
120
|
+
archivedVersion: 'Arquivada',
|
|
121
|
+
loadingVersions: 'Carregando versões',
|
|
122
|
+
preReleaseTitle: 'Documentação de pré-lançamento',
|
|
123
|
+
outdatedTitle: 'Documentação desatualizada',
|
|
124
|
+
preReleaseMessage: 'Esta é uma versão de pré-lançamento da documentação.',
|
|
125
|
+
outdatedMessage: 'Esta é uma versão desatualizada da documentação.',
|
|
126
|
+
backToCurrent: 'Voltar para a versão atual',
|
|
127
|
+
poweredBy: 'Feito com',
|
|
128
|
+
developedBy: 'Desenvolvido por',
|
|
129
|
+
modeToggle: 'Alternar tema',
|
|
130
|
+
documentationReady: 'Sua documentação está pronta.',
|
|
131
|
+
addDocument: 'Adicione um arquivo .mdx dentro de docs/.',
|
|
132
|
+
notFound: 'Página não encontrada',
|
|
133
|
+
notFoundDescription: 'Não encontramos o que você estava procurando.',
|
|
134
|
+
brokenLink:
|
|
135
|
+
'Entre em contato com o responsável pelo site que indicou este link e informe que ele não está funcionando.',
|
|
136
|
+
backToDocs: 'Voltar para a documentação',
|
|
137
|
+
} satisfies InterfaceMessages
|
|
138
|
+
|
|
139
|
+
/** Match the locale, then its base language; unsupported languages use English. */
|
|
140
|
+
export function resolveInterfaceLocale(lang = 'en'): InterfaceLocale {
|
|
141
|
+
const normalized = lang.trim().replaceAll('_', '-').toLowerCase()
|
|
142
|
+
if (normalized === 'pt-br' || normalized.split('-')[0] === 'pt')
|
|
143
|
+
return 'pt-BR'
|
|
144
|
+
return 'en'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function getMessages(lang?: string): InterfaceMessages {
|
|
148
|
+
return resolveInterfaceLocale(lang) === 'pt-BR' ? ptBR : en
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function formatMessage(message: string, values: Record<string, string>) {
|
|
152
|
+
return message.replace(
|
|
153
|
+
/\{(\w+)\}/g,
|
|
154
|
+
(placeholder, key: string) => values[key] ?? placeholder
|
|
155
|
+
)
|
|
156
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, type ReactNode } from 'react'
|
|
4
|
+
import { getMessages, resolveInterfaceLocale } from '../lib/i18n'
|
|
5
|
+
|
|
6
|
+
const LocaleContext = createContext('en')
|
|
7
|
+
|
|
8
|
+
/** Use the configured locale on both server rendering and hydration. */
|
|
9
|
+
export function LocaleProvider({
|
|
10
|
+
lang,
|
|
11
|
+
children,
|
|
12
|
+
}: {
|
|
13
|
+
lang?: string
|
|
14
|
+
children: ReactNode
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<LocaleContext.Provider value={resolveInterfaceLocale(lang)}>
|
|
18
|
+
{children}
|
|
19
|
+
</LocaleContext.Provider>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useInterfaceLocale() {
|
|
24
|
+
return useContext(LocaleContext)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useMessages() {
|
|
28
|
+
return getMessages(useInterfaceLocale())
|
|
29
|
+
}
|