docs-i18n 0.6.3 → 0.7.1
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/{src/admin/ui → admin/app}/components/JobDialog.tsx +21 -2
- package/{src/admin/ui → admin/app}/components/JobPanel.tsx +1 -1
- package/{src/admin/ui → admin/app}/components/Preview.tsx +2 -5
- package/{src/admin/ui → admin/app}/lib/api.ts +18 -39
- package/admin/app/routeTree.gen.ts +68 -0
- package/admin/app/router.tsx +23 -0
- package/admin/app/routes/__root.tsx +55 -0
- package/admin/app/routes/index.tsx +416 -0
- package/{src/admin/ui → admin/app}/styles.css +36 -3
- package/admin/package.json +26 -0
- package/admin/server/functions/jobs.ts +53 -0
- package/admin/server/functions/misc.ts +84 -0
- package/{src/admin/server/routes → admin/server/functions}/models.ts +16 -29
- package/admin/server/functions/status.ts +61 -0
- package/admin/server/index.ts +35 -0
- package/admin/server/init.ts +46 -0
- package/{src/admin → admin}/server/services/job-manager.ts +39 -10
- package/{src/admin → admin}/server/services/status.ts +6 -6
- package/admin/tsconfig.json +19 -0
- package/{src/admin → admin}/vite.config.ts +8 -2
- package/dist/{assemble-7H4QCW35.js → assemble-CP2BRYQJ.js} +6 -4
- package/dist/{chunk-A3YQNPKZ.js → chunk-CLYUAWZE.js} +1 -1
- package/dist/{chunk-YN4VJHCQ.js → chunk-JHBSHTXC.js} +1 -1
- package/dist/chunk-L64GJ4OB.js +32 -0
- package/dist/{chunk-SKKZIV3L.js → chunk-PNKVD2UK.js} +1 -29
- package/dist/{chunk-XEOYZUHS.js → chunk-QKIR7RKQ.js} +4 -31
- package/dist/chunk-TRURQFP4.js +31 -0
- package/dist/cli.js +108 -7
- package/dist/index.d.ts +41 -1
- package/dist/index.js +92 -3
- package/dist/{rescan-O5D3CYC2.js → rescan-HXMWFAOC.js} +5 -3
- package/dist/{status-F4MYIAAY.js → status-AGZDXOTZ.js} +4 -2
- package/dist/{translate-ZIVKNAC4.js → translate-A5X6MX4Y.js} +14 -7
- package/dist/upload-XL6KG6S2.js +132 -0
- package/package.json +17 -15
- package/template/app/components/BlogArticle.tsx +159 -0
- package/template/app/components/BlogList.tsx +88 -0
- package/template/app/components/Breadcrumbs.tsx +81 -0
- package/template/app/components/Card.tsx +31 -0
- package/template/app/components/Doc.tsx +191 -0
- package/template/app/components/DocBreadcrumb.tsx +60 -0
- package/template/app/components/DocContainer.tsx +13 -0
- package/template/app/components/DocTitle.tsx +11 -0
- package/template/app/components/DocsLayout.tsx +715 -0
- package/template/app/components/Dropdown.tsx +116 -0
- package/template/app/components/FallbackBanner.tsx +36 -0
- package/template/app/components/Footer.tsx +29 -0
- package/template/app/components/FrameworkSelect.tsx +150 -0
- package/template/app/components/LibraryCard.tsx +178 -0
- package/template/app/components/LocaleSwitcher.tsx +43 -0
- package/template/app/components/Navbar.tsx +430 -0
- package/template/app/components/PostNotFound.tsx +20 -0
- package/template/app/components/SearchButton.tsx +32 -0
- package/template/app/components/Select.tsx +103 -0
- package/template/app/components/Spinner.tsx +18 -0
- package/template/app/components/ThemeProvider.tsx +141 -0
- package/template/app/components/ThemeToggle.tsx +31 -0
- package/template/app/components/Toc.tsx +86 -0
- package/template/app/components/VersionSelect.tsx +118 -0
- package/template/app/components/icons/BSkyIcon.tsx +27 -0
- package/template/app/components/icons/BaseballCapIcon.tsx +25 -0
- package/template/app/components/icons/BrandXIcon.tsx +28 -0
- package/template/app/components/icons/CheckCircleIcon.tsx +28 -0
- package/template/app/components/icons/CogsIcon.tsx +25 -0
- package/template/app/components/icons/DiscordIcon.tsx +24 -0
- package/template/app/components/icons/GithubIcon.tsx +24 -0
- package/template/app/components/icons/GoogleIcon.tsx +24 -0
- package/template/app/components/icons/InstagramIcon.tsx +24 -0
- package/template/app/components/icons/NpmIcon.tsx +26 -0
- package/template/app/components/icons/YinYangIcon.tsx +26 -0
- package/template/app/components/icons/YouTubeIcon.tsx +24 -0
- package/template/app/components/markdown/CodeBlock.tsx +254 -0
- package/template/app/components/markdown/FileTabs.tsx +58 -0
- package/template/app/components/markdown/FrameworkContent.tsx +76 -0
- package/template/app/components/markdown/Markdown.tsx +216 -0
- package/template/app/components/markdown/MarkdownContent.tsx +89 -0
- package/template/app/components/markdown/MarkdownFrameworkHandler.tsx +66 -0
- package/template/app/components/markdown/MarkdownHeadingContext.tsx +35 -0
- package/template/app/components/markdown/MarkdownLink.tsx +46 -0
- package/template/app/components/markdown/MarkdownTabsHandler.tsx +109 -0
- package/template/app/components/markdown/PackageManagerTabs.tsx +95 -0
- package/template/app/components/markdown/Tabs.tsx +139 -0
- package/template/app/components/markdown/index.ts +15 -0
- package/template/app/components/ui/Button.tsx +141 -0
- package/template/app/components/ui/InlineCode.tsx +16 -0
- package/template/app/components/ui/MarkdownImg.tsx +21 -0
- package/template/app/config/frameworks.ts +93 -0
- package/template/app/contexts/SearchContext.tsx +36 -0
- package/template/app/db/index.ts +17 -0
- package/template/app/db/schema.ts +74 -0
- package/template/app/hooks/useClickOutside.ts +106 -0
- package/template/app/routeTree.gen.ts +584 -0
- package/template/app/router.tsx +29 -0
- package/template/app/routes/$lang.$project.$version.docs.$.tsx +128 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.$framework.$.tsx +106 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.$framework.index.tsx +27 -0
- package/template/app/routes/$lang.$project.$version.docs.framework.index.tsx +44 -0
- package/template/app/routes/$lang.$project.$version.docs.index.tsx +27 -0
- package/template/app/routes/$lang.$project.$version.docs.tsx +70 -0
- package/template/app/routes/$lang.$project.$version.tsx +69 -0
- package/template/app/routes/$lang.$project.docs.$.tsx +104 -0
- package/template/app/routes/$lang.$project.docs.index.tsx +20 -0
- package/template/app/routes/$lang.$project.docs.tsx +79 -0
- package/template/app/routes/$lang.$project.tsx +89 -0
- package/template/app/routes/$lang.blog.$.tsx +82 -0
- package/template/app/routes/$lang.blog.index.tsx +56 -0
- package/template/app/routes/$lang.blog.tsx +26 -0
- package/template/app/routes/$lang.docs.$.tsx +100 -0
- package/template/app/routes/$lang.docs.framework.$framework.$.tsx +104 -0
- package/template/app/routes/$lang.docs.framework.$framework.index.tsx +32 -0
- package/template/app/routes/$lang.docs.framework.index.tsx +47 -0
- package/template/app/routes/$lang.docs.index.tsx +20 -0
- package/template/app/routes/$lang.docs.tsx +90 -0
- package/template/app/routes/$lang.tsx +16 -0
- package/template/app/routes/__root.tsx +180 -0
- package/template/app/routes/index.tsx +89 -0
- package/template/app/site.config.ts +182 -0
- package/template/app/styles/app.css +1029 -0
- package/template/app/types/index.ts +77 -0
- package/template/app/utils/blog.server.ts +193 -0
- package/template/app/utils/blog.ts +42 -0
- package/template/app/utils/config.ts +120 -0
- package/template/app/utils/content-loader.ts +400 -0
- package/template/app/utils/dates.ts +29 -0
- package/template/app/utils/docs.server.ts +150 -0
- package/template/app/utils/markdown/filterFrameworkContent.ts +233 -0
- package/template/app/utils/markdown/index.ts +2 -0
- package/template/app/utils/markdown/installCommand.ts +143 -0
- package/template/app/utils/markdown/plugins/collectHeadings.ts +104 -0
- package/template/app/utils/markdown/plugins/extractCodeMeta.ts +57 -0
- package/template/app/utils/markdown/plugins/helpers.ts +33 -0
- package/template/app/utils/markdown/plugins/index.ts +8 -0
- package/template/app/utils/markdown/plugins/parseCommentComponents.ts +103 -0
- package/template/app/utils/markdown/plugins/transformCommentComponents.ts +23 -0
- package/template/app/utils/markdown/plugins/transformFrameworkComponent.ts +217 -0
- package/template/app/utils/markdown/plugins/transformTabsComponent.ts +359 -0
- package/template/app/utils/markdown/processor.ts +75 -0
- package/template/app/utils/site-config.tsx +11 -0
- package/template/app/utils/upload.ts +232 -0
- package/template/app/utils/useLocalStorage.ts +65 -0
- package/template/app/utils/utils.ts +23 -0
- package/template/package.json +53 -0
- package/template/public/favicon.svg +1 -0
- package/template/public/fonts/Inter-latin-ext.woff2 +0 -0
- package/template/public/fonts/Inter-latin.woff2 +0 -0
- package/template/public/images/frameworks/angular-logo.svg +1 -0
- package/template/public/images/frameworks/js-logo.svg +1 -0
- package/template/public/images/frameworks/lit-logo.svg +1 -0
- package/template/public/images/frameworks/preact-logo.svg +6 -0
- package/template/public/images/frameworks/qwik-logo.svg +1 -0
- package/template/public/images/frameworks/react-logo.svg +1 -0
- package/template/public/images/frameworks/solid-logo.svg +1 -0
- package/template/public/images/frameworks/svelte-logo.svg +1 -0
- package/template/public/images/frameworks/vue-logo.svg +4 -0
- package/template/tsconfig.json +24 -0
- package/template/vite.config.ts +43 -0
- package/template/wrangler.jsonc +16 -0
- package/README.md +0 -161
- package/dist/server-73AVSOL5.js +0 -598
- package/src/admin/index.html +0 -13
- package/src/admin/server/index.ts +0 -138
- package/src/admin/server/routes/jobs.ts +0 -113
- package/src/admin/server/routes/status.ts +0 -57
- package/src/admin/ui/App.tsx +0 -332
- package/src/admin/ui/main.tsx +0 -19
- /package/{src/admin/ui → admin/app}/components/FileList.tsx +0 -0
- /package/{src/admin/ui → admin/app}/components/LangGrid.tsx +0 -0
- /package/{src/admin/ui → admin/app}/components/ProgressBar.tsx +0 -0
- /package/{src/admin/ui → admin/app}/lib/flags.ts +0 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload content and translation cache from local filesystem to D1.
|
|
3
|
+
*
|
|
4
|
+
* Used by `docs-i18n site upload` CLI command to push content to production.
|
|
5
|
+
*
|
|
6
|
+
* Walks the content/ directory, reading each .md file and upserting it into
|
|
7
|
+
* the D1 `content` table. Also uploads the local SQLite translation cache
|
|
8
|
+
* into the D1 `translations` and `sources` tables.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
|
|
12
|
+
import { resolve, relative, join } from 'node:path'
|
|
13
|
+
|
|
14
|
+
interface UploadOptions {
|
|
15
|
+
/** Absolute path to the project root (where content/ lives). */
|
|
16
|
+
projectRoot: string
|
|
17
|
+
/** Wrangler D1 database name to target. */
|
|
18
|
+
databaseName: string
|
|
19
|
+
/** Optional: only upload specific project. */
|
|
20
|
+
project?: string
|
|
21
|
+
/** Optional: only upload specific version. */
|
|
22
|
+
version?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ContentRow {
|
|
26
|
+
path: string
|
|
27
|
+
body: string
|
|
28
|
+
project: string
|
|
29
|
+
version: string
|
|
30
|
+
lang: string
|
|
31
|
+
updated_at: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Walk the content directory and collect all .md files as content rows.
|
|
36
|
+
*/
|
|
37
|
+
export function collectContentFiles(projectRoot: string): ContentRow[] {
|
|
38
|
+
const contentDir = resolve(projectRoot, 'content')
|
|
39
|
+
if (!existsSync(contentDir)) {
|
|
40
|
+
throw new Error(`Content directory not found: ${contentDir}`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rows: ContentRow[] = []
|
|
44
|
+
walkDir(contentDir, contentDir, rows)
|
|
45
|
+
return rows
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function walkDir(dir: string, contentRoot: string, rows: ContentRow[]) {
|
|
49
|
+
const entries = readdirSync(dir, { withFileTypes: true })
|
|
50
|
+
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
const fullPath = join(dir, entry.name)
|
|
53
|
+
|
|
54
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
55
|
+
walkDir(fullPath, contentRoot, rows)
|
|
56
|
+
} else if (entry.isFile() && (entry.name.endsWith('.md') || entry.name.endsWith('.json'))) {
|
|
57
|
+
const relativePath = relative(contentRoot, fullPath)
|
|
58
|
+
const body = readFileSync(fullPath, 'utf-8')
|
|
59
|
+
|
|
60
|
+
// Parse path structure: content/{project}/{version}/{lang}/... or content/{lang}/...
|
|
61
|
+
const parts = relativePath.split('/')
|
|
62
|
+
const { project, version, lang } = parseContentPath(parts)
|
|
63
|
+
|
|
64
|
+
rows.push({
|
|
65
|
+
path: relativePath,
|
|
66
|
+
body,
|
|
67
|
+
project,
|
|
68
|
+
version,
|
|
69
|
+
lang,
|
|
70
|
+
updated_at: Math.floor(Date.now() / 1000),
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parse content path parts to extract project, version, and lang.
|
|
78
|
+
*
|
|
79
|
+
* Supports:
|
|
80
|
+
* - {project}/{version}/{lang}/slug.md
|
|
81
|
+
* - {project}/{lang}/slug.md (no version)
|
|
82
|
+
* - {lang}/slug.md (single project, no version)
|
|
83
|
+
*/
|
|
84
|
+
function parseContentPath(parts: string[]): {
|
|
85
|
+
project: string
|
|
86
|
+
version: string
|
|
87
|
+
lang: string
|
|
88
|
+
} {
|
|
89
|
+
// If only 2 parts: lang/file.md -> single project
|
|
90
|
+
if (parts.length === 2) {
|
|
91
|
+
return { project: 'default', version: 'latest', lang: parts[0] }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// If 3 parts: project/lang/file.md or lang/subdir/file.md
|
|
95
|
+
if (parts.length === 3) {
|
|
96
|
+
// Heuristic: if first part looks like a lang code (2-5 chars, lowercase), it's lang/subdir/file
|
|
97
|
+
if (isLangCode(parts[0])) {
|
|
98
|
+
return { project: 'default', version: 'latest', lang: parts[0] }
|
|
99
|
+
}
|
|
100
|
+
return { project: parts[0], version: 'latest', lang: parts[1] }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 4+ parts: project/version/lang/...
|
|
104
|
+
if (parts.length >= 4) {
|
|
105
|
+
return { project: parts[0], version: parts[1], lang: parts[2] }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { project: 'default', version: 'latest', lang: 'en' }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isLangCode(s: string): boolean {
|
|
112
|
+
return /^[a-z]{2}(-[a-z]{2,})?$/.test(s)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Generate SQL statements for batch insert/upsert into D1.
|
|
117
|
+
* Returns an array of SQL strings suitable for wrangler d1 execute.
|
|
118
|
+
*/
|
|
119
|
+
export function generateContentSql(rows: ContentRow[]): string[] {
|
|
120
|
+
const statements: string[] = []
|
|
121
|
+
|
|
122
|
+
// Create table if not exists
|
|
123
|
+
statements.push(`
|
|
124
|
+
CREATE TABLE IF NOT EXISTS content (
|
|
125
|
+
path TEXT PRIMARY KEY NOT NULL,
|
|
126
|
+
body TEXT NOT NULL,
|
|
127
|
+
project TEXT NOT NULL,
|
|
128
|
+
version TEXT NOT NULL,
|
|
129
|
+
lang TEXT NOT NULL,
|
|
130
|
+
updated_at INTEGER
|
|
131
|
+
);
|
|
132
|
+
`.trim())
|
|
133
|
+
|
|
134
|
+
// Batch upsert in chunks of 50 to avoid SQL size limits
|
|
135
|
+
const chunkSize = 50
|
|
136
|
+
for (let i = 0; i < rows.length; i += chunkSize) {
|
|
137
|
+
const chunk = rows.slice(i, i + chunkSize)
|
|
138
|
+
for (const row of chunk) {
|
|
139
|
+
const escapedBody = row.body.replace(/'/g, "''")
|
|
140
|
+
const escapedPath = row.path.replace(/'/g, "''")
|
|
141
|
+
statements.push(
|
|
142
|
+
`INSERT OR REPLACE INTO content (path, body, project, version, lang, updated_at) VALUES ('${escapedPath}', '${escapedBody}', '${row.project}', '${row.version}', '${row.lang}', ${row.updated_at});`,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return statements
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Collect translation cache entries from local SQLite for upload.
|
|
152
|
+
* Reads from the project's .docs-i18n/translations.db file.
|
|
153
|
+
*/
|
|
154
|
+
export function collectTranslations(projectRoot: string): {
|
|
155
|
+
sources: { key: string; text: string; type: string }[]
|
|
156
|
+
translations: { lang: string; key: string; value: string }[]
|
|
157
|
+
} {
|
|
158
|
+
const dbPath = resolve(projectRoot, '.docs-i18n', 'translations.db')
|
|
159
|
+
|
|
160
|
+
if (!existsSync(dbPath)) {
|
|
161
|
+
return { sources: [], translations: [] }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Dynamic import to avoid bundling better-sqlite3 in the template
|
|
165
|
+
// This runs in Node.js CLI context, not in the browser
|
|
166
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
167
|
+
const Database = require('better-sqlite3')
|
|
168
|
+
const db = new Database(dbPath, { readonly: true })
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const sources = db
|
|
172
|
+
.prepare('SELECT key, text, type FROM sources')
|
|
173
|
+
.all() as { key: string; text: string; type: string }[]
|
|
174
|
+
|
|
175
|
+
const translations = db
|
|
176
|
+
.prepare('SELECT lang, key, value FROM translations')
|
|
177
|
+
.all() as { lang: string; key: string; value: string }[]
|
|
178
|
+
|
|
179
|
+
return { sources, translations }
|
|
180
|
+
} finally {
|
|
181
|
+
db.close()
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Generate SQL for uploading translation cache to D1.
|
|
187
|
+
*/
|
|
188
|
+
export function generateTranslationSql(data: {
|
|
189
|
+
sources: { key: string; text: string; type: string }[]
|
|
190
|
+
translations: { lang: string; key: string; value: string }[]
|
|
191
|
+
}): string[] {
|
|
192
|
+
const statements: string[] = []
|
|
193
|
+
|
|
194
|
+
// Create tables if not exist
|
|
195
|
+
statements.push(`
|
|
196
|
+
CREATE TABLE IF NOT EXISTS sources (
|
|
197
|
+
key TEXT PRIMARY KEY NOT NULL,
|
|
198
|
+
text TEXT NOT NULL,
|
|
199
|
+
type TEXT NOT NULL DEFAULT 'paragraph'
|
|
200
|
+
);
|
|
201
|
+
`.trim())
|
|
202
|
+
|
|
203
|
+
statements.push(`
|
|
204
|
+
CREATE TABLE IF NOT EXISTS translations (
|
|
205
|
+
lang TEXT NOT NULL,
|
|
206
|
+
key TEXT NOT NULL,
|
|
207
|
+
value TEXT NOT NULL,
|
|
208
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
209
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
210
|
+
PRIMARY KEY (lang, key)
|
|
211
|
+
);
|
|
212
|
+
CREATE INDEX IF NOT EXISTS idx_translations_lang ON translations(lang);
|
|
213
|
+
`.trim())
|
|
214
|
+
|
|
215
|
+
// Upsert sources
|
|
216
|
+
for (const s of data.sources) {
|
|
217
|
+
const escapedText = s.text.replace(/'/g, "''")
|
|
218
|
+
statements.push(
|
|
219
|
+
`INSERT OR REPLACE INTO sources (key, text, type) VALUES ('${s.key}', '${escapedText}', '${s.type}');`,
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Upsert translations
|
|
224
|
+
for (const t of data.translations) {
|
|
225
|
+
const escapedValue = t.value.replace(/'/g, "''")
|
|
226
|
+
statements.push(
|
|
227
|
+
`INSERT INTO translations (lang, key, value) VALUES ('${t.lang}', '${t.key}', '${escapedValue}') ON CONFLICT(lang, key) DO UPDATE SET value = excluded.value, updated_at = unixepoch();`,
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return statements
|
|
232
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
function getWithExpiry<T>(key: string): T | undefined {
|
|
4
|
+
if (typeof window === 'undefined') {
|
|
5
|
+
return undefined
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const itemStr = localStorage.getItem(key)
|
|
9
|
+
if (!itemStr) {
|
|
10
|
+
return undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const item: { value: T; ttl: number } = JSON.parse(itemStr)
|
|
15
|
+
|
|
16
|
+
// If there is no TTL set, return the value
|
|
17
|
+
if (!item.ttl) {
|
|
18
|
+
return item.value
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// If the item is expired, delete the item from storage
|
|
22
|
+
if (new Date().getTime() > item.ttl) {
|
|
23
|
+
localStorage.removeItem(key)
|
|
24
|
+
return undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return item.value
|
|
28
|
+
} catch {
|
|
29
|
+
// If JSON parsing fails, remove the corrupted item and return undefined
|
|
30
|
+
localStorage.removeItem(key)
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* React state that persists to `localStorage` (with optional TTL).
|
|
37
|
+
*
|
|
38
|
+
* - `key`: localStorage key to read/write
|
|
39
|
+
* - `defaultValue`: initial value if no stored value
|
|
40
|
+
* - `ttl` (ms): optional time-to-live; expired values are cleared and ignored
|
|
41
|
+
*/
|
|
42
|
+
export function useLocalStorage<T>(
|
|
43
|
+
key: string,
|
|
44
|
+
defaultValue: T,
|
|
45
|
+
ttl?: number,
|
|
46
|
+
): [T, typeof setValue] {
|
|
47
|
+
const [value, setValue] = useState(defaultValue)
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const item = getWithExpiry<T>(key)
|
|
51
|
+
if (item !== undefined) setValue(item)
|
|
52
|
+
}, [key])
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
localStorage.setItem(
|
|
56
|
+
key,
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
value,
|
|
59
|
+
ttl: ttl ? new Date().getTime() + ttl : null,
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
}, [key, value, ttl])
|
|
63
|
+
|
|
64
|
+
return [value, setValue]
|
|
65
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uppercases the first character of a string and returns the result.
|
|
3
|
+
*/
|
|
4
|
+
export function capitalize(str: string) {
|
|
5
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converts a kebab-case slug (eg. "my-example") into a Title Case string.
|
|
10
|
+
*/
|
|
11
|
+
export function slugToTitle(str: string) {
|
|
12
|
+
return str
|
|
13
|
+
.split('-')
|
|
14
|
+
.map((word) => capitalize(word))
|
|
15
|
+
.join(' ')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns the last element from an array.
|
|
20
|
+
*/
|
|
21
|
+
export function last<T>(arr: T[]) {
|
|
22
|
+
return arr[arr.length - 1]
|
|
23
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@docs-i18n/template",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite dev",
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"start": "vite start"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
12
|
+
"@shikijs/transformers": "^3.0.0",
|
|
13
|
+
"@tailwindcss/typography": "^0.5.16",
|
|
14
|
+
"@tailwindcss/vite": "^4.1.7",
|
|
15
|
+
"@tanstack/react-router": "^1.120.3",
|
|
16
|
+
"@tanstack/react-query": "^5.0.0",
|
|
17
|
+
"@tanstack/react-start": "^1.120.3",
|
|
18
|
+
"drizzle-orm": "^0.38.0",
|
|
19
|
+
"gray-matter": "^4.0.3",
|
|
20
|
+
"hast-util-is-element": "^3.0.0",
|
|
21
|
+
"hast-util-to-string": "^3.0.1",
|
|
22
|
+
"html-react-parser": "^5.1.10",
|
|
23
|
+
"lru-cache": "^11.0.0",
|
|
24
|
+
"lucide-react": "^0.400.0",
|
|
25
|
+
"mermaid": "^11.13.0",
|
|
26
|
+
"react": "^19.1.0",
|
|
27
|
+
"react-dom": "^19.1.0",
|
|
28
|
+
"rehype-autolink-headings": "^7.1.0",
|
|
29
|
+
"rehype-callouts": "^2.0.0",
|
|
30
|
+
"rehype-parse": "^9.0.0",
|
|
31
|
+
"rehype-raw": "^7.0.0",
|
|
32
|
+
"rehype-slug": "^6.0.0",
|
|
33
|
+
"rehype-stringify": "^10.0.1",
|
|
34
|
+
"remark-gfm": "^4.0.1",
|
|
35
|
+
"remark-parse": "^11.0.0",
|
|
36
|
+
"remark-rehype": "^11.1.2",
|
|
37
|
+
"shiki": "^3.0.0",
|
|
38
|
+
"tailwind-merge": "^2.0.0",
|
|
39
|
+
"tailwindcss": "^4.1.7",
|
|
40
|
+
"unified": "^11.0.5",
|
|
41
|
+
"unist-util-visit": "^5.0.0",
|
|
42
|
+
"vite": "^7.0.0",
|
|
43
|
+
"vite-tsconfig-paths": "^5.0.1",
|
|
44
|
+
"zustand": "^5.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^22.15.0",
|
|
48
|
+
"@types/react": "^19.1.0",
|
|
49
|
+
"@types/react-dom": "^19.1.0",
|
|
50
|
+
"drizzle-kit": "^0.30.0",
|
|
51
|
+
"typescript": "^5.8.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#2563eb"/><text x="16" y="23" font-family="sans-serif" font-size="20" font-weight="bold" fill="white" text-anchor="middle">D</text></svg>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" viewBox="0 0 223 236" width="32" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="49.009" x2="225.829" y1="213.75" y2="129.722"><stop offset="0" stop-color="#e40035"/><stop offset=".24" stop-color="#f60a48"/><stop offset=".352" stop-color="#f20755"/><stop offset=".494" stop-color="#dc087d"/><stop offset=".745" stop-color="#9717e7"/><stop offset="1" stop-color="#6c00f5"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="41.025" x2="156.741" y1="28.344" y2="160.344"><stop offset="0" stop-color="#ff31d9"/><stop offset="1" stop-color="#ff5be1" stop-opacity="0"/></linearGradient><clipPath id="c"><path d="m0 0h223v236h-223z"/></clipPath><g clip-path="url(#c)"><path d="m222.077 39.192-8.019 125.923-76.671-165.115zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301zm-57.933-139.342 30.357 73.803h-60.715zm-103.102 102.44-7.937-125.923 84.69-39.192z" fill="url(#a)"/><path d="m222.077 39.192-8.019 125.923-76.671-165.115zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301zm-57.933-139.342 30.357 73.803h-60.715zm-103.102 102.44-7.937-125.923 84.69-39.192z" fill="url(#b)"/></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 630 630" xmlns="http://www.w3.org/2000/svg"><path d="m0 0h630v630h-630z" fill="#f7df1e"/><path d="m423.2 492.19c12.69 20.72 29.2 35.95 58.4 35.95 24.53 0 40.2-12.26 40.2-29.2 0-20.3-16.1-27.49-43.1-39.3l-14.8-6.35c-42.72-18.2-71.1-41-71.1-89.2 0-44.4 33.83-78.2 86.7-78.2 37.64 0 64.7 13.1 84.2 47.4l-46.1 29.6c-10.15-18.2-21.1-25.37-38.1-25.37-17.34 0-28.33 11-28.33 25.37 0 17.76 11 24.95 36.4 35.95l14.8 6.34c50.3 21.57 78.7 43.56 78.7 93 0 53.3-41.87 82.5-98.1 82.5-54.98 0-90.5-26.2-107.88-60.54zm-209.13 5.13c9.3 16.5 17.76 30.45 38.1 30.45 19.45 0 31.72-7.61 31.72-37.2v-201.3h59.2v202.1c0 61.3-35.94 89.2-88.4 89.2-47.4 0-74.85-24.53-88.81-54.075z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" height="200" viewBox="0 0 160 200" width="160" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="a"><path d="m0 0h160v200h-160z"/></clipPath><g clip-path="url(#a)"><path d="m40 120 20-60 90 90-30 50-40-40h-20" fill="#00e8ff"/><path d="m80 160v-80l40-40v80m-120 40 40 40 20-40-20-40h-20" fill="#283198"/><path d="m40 120v-80l40-40v80m40 120v-80l40-40v80m-160 0v-80l40 40" fill="#324fff"/><path d="m40 200v-80l40 40" fill="#0ff"/></g></svg>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg width="100%" height="100%" viewBox="-256 -256 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
|
|
2
|
+
<path d="M0,-256 221.7025033688164,-128 221.7025033688164,128 0,256 -221.7025033688164,128 -221.7025033688164,-128z" fill="#673ab8"/>
|
|
3
|
+
<ellipse cx="0" cy="0" stroke-width="16px" rx="75px" ry="196px" fill="none" stroke="white" transform="rotate(52.5)"/>
|
|
4
|
+
<ellipse cx="0" cy="0" stroke-width="16px" rx="75px" ry="196px" fill="none" stroke="white" transform="rotate(-52.5)"/>
|
|
5
|
+
<circle cx="0" cy="0" r="34" fill="white"/>
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" height="50" viewBox="0 0 47 50" width="47" xmlns="http://www.w3.org/2000/svg"><path d="m40.973 49.535-8.8869-8.8366-.1358.0194v-.0969l-18.8992-18.6614 4.6569-4.4959-2.7359-15.69651-12.98107 16.08411c-2.212022 2.2285-2.619497 5.8523-1.028395 8.5265l8.110735 13.4487c1.24183 2.0735 3.49263 3.3137 5.91813 3.2943l4.0166-.0387z" fill="#18b6f6"/><path d="m45.8232 17.5412-1.7852-3.2943-.9314-1.686-.3686-.6588-.0388.0387-4.8898-8.46836c-1.2224-2.13163-3.512-3.4493713-5.9957-3.4299928l-4.2882.1162658-12.7871.038759c-2.4254.019379-4.6374 1.298358-5.85987 3.371858l-7.78087 15.42527 13.91244-17.26622 18.2395 20.03732-3.2405 3.275 1.9404 15.6771.0194-.0387v.0387h-.0388l.0388.0388 1.5135 1.4728 7.354 7.1894c.3105.2906.815-.0582.6015-.4264l-4.5404-8.9334 7.9167-14.6308.2522-.2906c.097-.1163.194-.2326.2717-.3489 1.5523-2.1122 1.7657-4.9415.4851-7.2475z" fill="#ac7ef4"/><path d="m33.3076 21.6882-18.2977-19.9404 2.6001 15.619-4.6569 4.5152 18.9574 18.8165-1.7075-15.619z" fill="#fff"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="-11.5 -10.23 23 20.46" xmlns="http://www.w3.org/2000/svg"><circle fill="#61dafb" r="2.05"/><g fill="none" stroke="#61dafb"><ellipse rx="11" ry="4.2"/><ellipse rx="11" ry="4.2" transform="matrix(.5 .8660254 -.8660254 .5 0 0)"/><ellipse rx="11" ry="4.2" transform="matrix(-.5 .8660254 -.8660254 -.5 0 0)"/></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 75.8 70.7" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="11.67" x2="70.61" y1=".04" y2="28.68"><stop offset=".1" stop-color="#76b3e1"/><stop offset=".3" stop-color="#dcf2fd"/><stop offset="1" stop-color="#76b3e1"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="43.99" x2="33.67" y1="14.07" y2="48.44"><stop offset="0" stop-color="#76b3e1"/><stop offset=".5" stop-color="#4377bb"/><stop offset="1" stop-color="#1f3b77"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="7.34" x2="66.94" y1="29.06" y2="69.58"><stop offset="0" stop-color="#315aa9"/><stop offset=".5" stop-color="#518ac8"/><stop offset="1" stop-color="#315aa9"/></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="34.25" x2="10.2" y1="33.91" y2="122.1"><stop offset="0" stop-color="#4377bb"/><stop offset=".5" stop-color="#1a336b"/><stop offset="1" stop-color="#1a336b"/></linearGradient><path d="m75.8 15.4s-24.9-18.8-44.3-14.4l-1.7.5c-2.6.8-5 2.4-6.6 4.6-.3.4-.5.8-.7 1.2l-7.2 12.1 12.3 2.4a22 22 0 0 0 17.6 3.5l22 4.3z" fill="#76b3e1"/><path d="m75.8 15.4s-24.9-18.8-44.3-14.4l-1.7.5c-2.6.8-5 2.4-6.6 4.6-.3.4-.5.8-.7 1.2l-7.2 12.1 12.3 2.4a22 22 0 0 0 17.6 3.5l22 4.3z" fill="url(#a)" opacity=".3"/><path d="m23 15.1-1.7.5c-7.9 2.6-10.6 9.9-6.1 16.4a21.1 21.1 0 0 0 22.5 7.1l29.5-9.6c.1 0-24.8-18.8-44.2-14.4z" fill="#518ac8"/><path d="m23 15.1-1.7.5c-7.9 2.6-10.6 9.9-6.1 16.4a21.1 21.1 0 0 0 22.5 7.1l29.5-9.6c.1 0-24.8-18.8-44.2-14.4z" fill="url(#b)" opacity=".3"/><path d="m61.9 36.4c-5.4-6.8-14.3-9.6-22.6-7.1l-29.5 9.5-9.3 16.5 52.8 9 9.5-16.8c1.9-3.2 1.7-7.3-.9-11.1z" fill="url(#c)"/><path d="m52.6 52.9c-5.4-6.8-14.3-9.6-22.5-7.2l-29.6 9.6s25 18.8 44.3 14.4l1.7-.5c8-2.4 10.7-9.8 6.1-16.3z" fill="url(#d)"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 98.1 118" xmlns="http://www.w3.org/2000/svg"><path d="m91.8 15.6c-10.9-15.7-32.6-20.3-48.2-10.4l-27.5 17.6a31.25 31.25 0 0 0 -14.2 21.1c-1.3 7.3-.2 14.8 3.3 21.3-2.4 3.6-4 7.6-4.7 11.8-1.6 8.9.5 18.1 5.7 25.4 11 15.7 32.6 20.3 48.2 10.4l27.5-17.5c7.5-4.7 12.7-12.4 14.2-21.1 1.3-7.3.2-14.8-3.3-21.3 2.4-3.6 4-7.6 4.7-11.8 1.7-9-.4-18.2-5.7-25.5" fill="#ff3e00"/><path d="m40.9 103.9a21.8 21.8 0 0 1 -23.4-8.7c-3.2-4.4-4.4-9.9-3.5-15.3l.6-2.6.5-1.6 1.4 1c3.3 2.4 6.9 4.2 10.8 5.4l1 .3-.1 1c-.1 1.4.3 2.9 1.1 4.1a6.62 6.62 0 0 0 8.8 2l27.4-17.5c1.4-.9 2.3-2.2 2.6-3.8s-.1-3.3-1-4.6a6.56 6.56 0 0 0 -8.8-1.9l-10.5 6.7a18.6 18.6 0 0 1 -5.6 2.4 21.8 21.8 0 0 1 -23.4-8.7 20.2 20.2 0 0 1 -3.4-15.3c.9-5.2 4.1-9.9 8.6-12.7l27.5-17.5c1.7-1.1 3.6-1.9 5.6-2.5a21.8 21.8 0 0 1 23.4 8.7c3.2 4.4 4.4 9.9 3.5 15.3-.2.9-.4 1.7-.7 2.6l-.5 1.6-1.4-1c-3.3-2.4-6.9-4.2-10.8-5.4l-1-.3.1-1c.1-1.4-.3-2.9-1.1-4.1a6.56 6.56 0 0 0 -8.8-1.9l-27.4 17.5c-1.4.9-2.3 2.2-2.6 3.8s.1 3.3 1 4.6a6.56 6.56 0 0 0 8.8 1.9l10.5-6.7c1.7-1.1 3.6-1.9 5.6-2.5a21.8 21.8 0 0 1 23.4 8.7c3.2 4.4 4.4 9.9 3.5 15.3-.9 5.2-4.1 9.9-8.6 12.7l-27.5 17.5c-1.7 1.1-3.6 1.9-5.6 2.5" fill="#fff"/></svg>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
3
|
+
"exclude": ["node_modules"],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"strict": true,
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"module": "ESNext",
|
|
9
|
+
"moduleResolution": "Bundler",
|
|
10
|
+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"target": "ES2022",
|
|
15
|
+
"allowJs": true,
|
|
16
|
+
"forceConsistentCasingInFileNames": true,
|
|
17
|
+
"baseUrl": ".",
|
|
18
|
+
"paths": {
|
|
19
|
+
"~/*": ["./app/*"]
|
|
20
|
+
},
|
|
21
|
+
"types": ["vite/client"],
|
|
22
|
+
"noEmit": true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { defineConfig } from 'vite'
|
|
4
|
+
import tsConfigPaths from 'vite-tsconfig-paths'
|
|
5
|
+
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
|
6
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
7
|
+
|
|
8
|
+
// When launched via `docs-i18n site`, DOCS_I18N_PROJECT_ROOT points to the
|
|
9
|
+
// consumer project. If that project ships its own `site.config.ts` we alias
|
|
10
|
+
// `~/site.config` so every template import picks it up automatically.
|
|
11
|
+
const projectRoot = process.env.DOCS_I18N_PROJECT_ROOT
|
|
12
|
+
const consumerConfig = projectRoot
|
|
13
|
+
? resolve(projectRoot, 'site.config.ts')
|
|
14
|
+
: undefined
|
|
15
|
+
|
|
16
|
+
const aliasEntries: Record<string, string> = {}
|
|
17
|
+
if (consumerConfig && existsSync(consumerConfig)) {
|
|
18
|
+
aliasEntries['~/site.config'] = consumerConfig
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Deduplicate React — ensure all imports resolve to the template's copy,
|
|
22
|
+
// preventing "Cannot read properties of null (reading 'useContext')" when the
|
|
23
|
+
// consumer project has its own node_modules/react.
|
|
24
|
+
const templateRoot = resolve(import.meta.dirname ?? __dirname, '.')
|
|
25
|
+
|
|
26
|
+
export default defineConfig({
|
|
27
|
+
esbuild: {
|
|
28
|
+
jsx: 'automatic',
|
|
29
|
+
},
|
|
30
|
+
resolve: {
|
|
31
|
+
alias: aliasEntries,
|
|
32
|
+
dedupe: ['react', 'react-dom'],
|
|
33
|
+
},
|
|
34
|
+
plugins: [
|
|
35
|
+
tsConfigPaths({
|
|
36
|
+
projects: ['./tsconfig.json'],
|
|
37
|
+
}),
|
|
38
|
+
tanstackStart({
|
|
39
|
+
srcDirectory: 'app',
|
|
40
|
+
}),
|
|
41
|
+
tailwindcss(),
|
|
42
|
+
],
|
|
43
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "docs-i18n-site",
|
|
3
|
+
"compatibility_date": "2025-01-01",
|
|
4
|
+
"compatibility_flags": ["nodejs_compat_v2"],
|
|
5
|
+
"main": "dist/server/index.mjs",
|
|
6
|
+
"assets": {
|
|
7
|
+
"directory": "dist/client"
|
|
8
|
+
},
|
|
9
|
+
"d1_databases": [
|
|
10
|
+
{
|
|
11
|
+
"binding": "DB",
|
|
12
|
+
"database_name": "docs-i18n-db",
|
|
13
|
+
"database_id": "placeholder-replace-with-actual-id"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|