uniweb 0.2.12 → 0.2.14
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/package.json +3 -2
- package/src/commands/build.js +135 -0
- package/src/commands/docs.js +143 -0
- package/src/commands/i18n.js +325 -0
- package/src/index.js +29 -4
- package/src/versions.js +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,8 +32,9 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"handlebars": "^4.7.8",
|
|
35
|
+
"js-yaml": "^4.1.0",
|
|
35
36
|
"prompts": "^2.4.2",
|
|
36
37
|
"tar": "^7.0.0",
|
|
37
|
-
"@uniweb/build": "0.1.
|
|
38
|
+
"@uniweb/build": "0.1.6"
|
|
38
39
|
}
|
|
39
40
|
}
|
package/src/commands/build.js
CHANGED
|
@@ -178,6 +178,112 @@ async function buildFoundation(projectDir, options = {}) {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Load site i18n configuration
|
|
183
|
+
*
|
|
184
|
+
* Resolves locales from config:
|
|
185
|
+
* - undefined → all available locales (from locales/*.json)
|
|
186
|
+
* - '*' → explicitly all available locales
|
|
187
|
+
* - ['es', 'fr'] → only those specific locales
|
|
188
|
+
*/
|
|
189
|
+
async function loadI18nConfig(projectDir) {
|
|
190
|
+
const siteYmlPath = join(projectDir, 'site.yml')
|
|
191
|
+
if (!existsSync(siteYmlPath)) return null
|
|
192
|
+
|
|
193
|
+
const { readFile } = await import('node:fs/promises')
|
|
194
|
+
const yaml = await import('js-yaml')
|
|
195
|
+
const content = await readFile(siteYmlPath, 'utf-8')
|
|
196
|
+
const config = yaml.load(content) || {}
|
|
197
|
+
|
|
198
|
+
const localesDir = config.i18n?.localesDir || 'locales'
|
|
199
|
+
const localesPath = join(projectDir, localesDir)
|
|
200
|
+
|
|
201
|
+
// Resolve locales (undefined/'*' → all available, array → specific)
|
|
202
|
+
const { resolveLocales } = await import('@uniweb/build/i18n')
|
|
203
|
+
const locales = await resolveLocales(config.i18n?.locales, localesPath)
|
|
204
|
+
|
|
205
|
+
if (locales.length === 0) return null
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
defaultLocale: config.defaultLanguage || 'en',
|
|
209
|
+
locales,
|
|
210
|
+
localesDir,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Build localized content for all configured locales
|
|
216
|
+
*/
|
|
217
|
+
async function buildLocalizedContent(projectDir, i18nConfig) {
|
|
218
|
+
const { buildLocalizedContent } = await import('@uniweb/build/i18n')
|
|
219
|
+
|
|
220
|
+
const outputs = await buildLocalizedContent(projectDir, {
|
|
221
|
+
localesDir: i18nConfig.localesDir,
|
|
222
|
+
locales: i18nConfig.locales,
|
|
223
|
+
outputDir: join(projectDir, 'dist'),
|
|
224
|
+
fallbackToSource: true,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
return outputs
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Generate index.html for each locale with hreflang tags
|
|
232
|
+
*/
|
|
233
|
+
async function generateLocalizedHtml(projectDir, i18nConfig) {
|
|
234
|
+
const { readFile, writeFile, mkdir, copyFile } = await import('node:fs/promises')
|
|
235
|
+
const distDir = join(projectDir, 'dist')
|
|
236
|
+
const baseHtmlPath = join(distDir, 'index.html')
|
|
237
|
+
|
|
238
|
+
if (!existsSync(baseHtmlPath)) {
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let baseHtml = await readFile(baseHtmlPath, 'utf-8')
|
|
243
|
+
|
|
244
|
+
// Build hreflang tags
|
|
245
|
+
const hreflangTags = i18nConfig.locales.map(locale =>
|
|
246
|
+
`<link rel="alternate" hreflang="${locale}" href="/${locale}/" />`
|
|
247
|
+
).join('\n ')
|
|
248
|
+
|
|
249
|
+
const defaultHreflang = `<link rel="alternate" hreflang="x-default" href="/" />`
|
|
250
|
+
const allHreflangTags = `${hreflangTags}\n ${defaultHreflang}`
|
|
251
|
+
|
|
252
|
+
// Add hreflang to base HTML (for default locale)
|
|
253
|
+
if (!baseHtml.includes('hreflang')) {
|
|
254
|
+
baseHtml = baseHtml.replace('</head>', ` ${allHreflangTags}\n </head>`)
|
|
255
|
+
await writeFile(baseHtmlPath, baseHtml)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Generate index.html for each locale
|
|
259
|
+
for (const locale of i18nConfig.locales) {
|
|
260
|
+
const localeDir = join(distDir, locale)
|
|
261
|
+
await mkdir(localeDir, { recursive: true })
|
|
262
|
+
|
|
263
|
+
// Read locale-specific site-content.json
|
|
264
|
+
const localeContentPath = join(localeDir, 'site-content.json')
|
|
265
|
+
let localeHtml = baseHtml
|
|
266
|
+
|
|
267
|
+
// Update html lang attribute
|
|
268
|
+
localeHtml = localeHtml.replace(/<html[^>]*lang="[^"]*"/, `<html lang="${locale}"`)
|
|
269
|
+
if (!localeHtml.includes('lang=')) {
|
|
270
|
+
localeHtml = localeHtml.replace('<html', `<html lang="${locale}"`)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Update script src for locale-specific content if inlined
|
|
274
|
+
if (existsSync(localeContentPath)) {
|
|
275
|
+
const localeContent = await readFile(localeContentPath, 'utf-8')
|
|
276
|
+
// Replace the inlined content if present
|
|
277
|
+
localeHtml = localeHtml.replace(
|
|
278
|
+
/<script id="__SITE_CONTENT__"[^>]*>[\s\S]*?<\/script>/,
|
|
279
|
+
`<script id="__SITE_CONTENT__" type="application/json">${localeContent}</script>`
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
await writeFile(join(localeDir, 'index.html'), localeHtml)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
181
287
|
/**
|
|
182
288
|
* Build a site
|
|
183
289
|
*/
|
|
@@ -191,6 +297,35 @@ async function buildSite(projectDir, options = {}) {
|
|
|
191
297
|
|
|
192
298
|
success('Site build complete')
|
|
193
299
|
|
|
300
|
+
// Check for i18n configuration
|
|
301
|
+
const i18nConfig = await loadI18nConfig(projectDir)
|
|
302
|
+
|
|
303
|
+
if (i18nConfig && i18nConfig.locales.length > 0) {
|
|
304
|
+
log('')
|
|
305
|
+
info(`Building localized content for: ${i18nConfig.locales.join(', ')}`)
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
// Generate locale-specific site-content.json
|
|
309
|
+
const outputs = await buildLocalizedContent(projectDir, i18nConfig)
|
|
310
|
+
|
|
311
|
+
// Generate locale-specific index.html files
|
|
312
|
+
await generateLocalizedHtml(projectDir, i18nConfig)
|
|
313
|
+
|
|
314
|
+
success(`Generated ${Object.keys(outputs).length} locale(s)`)
|
|
315
|
+
|
|
316
|
+
for (const [locale, path] of Object.entries(outputs)) {
|
|
317
|
+
log(` ${colors.dim}dist/${locale}/site-content.json${colors.reset}`)
|
|
318
|
+
}
|
|
319
|
+
} catch (err) {
|
|
320
|
+
error(`i18n build failed: ${err.message}`)
|
|
321
|
+
if (process.env.DEBUG) {
|
|
322
|
+
console.error(err.stack)
|
|
323
|
+
}
|
|
324
|
+
// Don't fail the build, just warn
|
|
325
|
+
log(`${colors.yellow}Continuing without localized content${colors.reset}`)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
194
329
|
// Pre-render if requested
|
|
195
330
|
if (prerender) {
|
|
196
331
|
log('')
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docs Command
|
|
3
|
+
*
|
|
4
|
+
* Generates markdown documentation from foundation schema.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* uniweb docs # Generate docs for current directory
|
|
8
|
+
* uniweb docs --output README.md # Custom output filename
|
|
9
|
+
* uniweb docs --from-source # Build schema from source (no build required)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync } from 'node:fs'
|
|
13
|
+
import { resolve, join } from 'node:path'
|
|
14
|
+
import { generateDocs } from '@uniweb/build'
|
|
15
|
+
|
|
16
|
+
// Colors for terminal output
|
|
17
|
+
const colors = {
|
|
18
|
+
reset: '\x1b[0m',
|
|
19
|
+
bright: '\x1b[1m',
|
|
20
|
+
dim: '\x1b[2m',
|
|
21
|
+
cyan: '\x1b[36m',
|
|
22
|
+
green: '\x1b[32m',
|
|
23
|
+
yellow: '\x1b[33m',
|
|
24
|
+
red: '\x1b[31m',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function log(message) {
|
|
28
|
+
console.log(message)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function success(message) {
|
|
32
|
+
console.log(`${colors.green}✓${colors.reset} ${message}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function error(message) {
|
|
36
|
+
console.error(`${colors.red}✗${colors.reset} ${message}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function info(message) {
|
|
40
|
+
console.log(`${colors.cyan}→${colors.reset} ${message}`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse command line arguments
|
|
45
|
+
*/
|
|
46
|
+
function parseArgs(args) {
|
|
47
|
+
const options = {
|
|
48
|
+
output: 'COMPONENTS.md',
|
|
49
|
+
fromSource: false,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < args.length; i++) {
|
|
53
|
+
const arg = args[i]
|
|
54
|
+
|
|
55
|
+
if (arg === '--output' || arg === '-o') {
|
|
56
|
+
options.output = args[++i]
|
|
57
|
+
} else if (arg === '--from-source' || arg === '-s') {
|
|
58
|
+
options.fromSource = true
|
|
59
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
60
|
+
options.help = true
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return options
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Show help message
|
|
69
|
+
*/
|
|
70
|
+
function showHelp() {
|
|
71
|
+
log(`
|
|
72
|
+
${colors.bright}uniweb docs${colors.reset} - Generate component documentation
|
|
73
|
+
|
|
74
|
+
${colors.dim}Usage:${colors.reset}
|
|
75
|
+
uniweb docs Generate COMPONENTS.md from schema.json
|
|
76
|
+
uniweb docs --output DOCS.md Custom output filename
|
|
77
|
+
uniweb docs --from-source Build schema from source (no build required)
|
|
78
|
+
|
|
79
|
+
${colors.dim}Options:${colors.reset}
|
|
80
|
+
-o, --output <file> Output filename (default: COMPONENTS.md)
|
|
81
|
+
-s, --from-source Read meta.js files directly instead of schema.json
|
|
82
|
+
-h, --help Show this help message
|
|
83
|
+
|
|
84
|
+
${colors.dim}Notes:${colors.reset}
|
|
85
|
+
By default, docs are generated from dist/schema.json (requires build).
|
|
86
|
+
Use --from-source to generate without building first.
|
|
87
|
+
`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Detect if current directory is a foundation
|
|
92
|
+
*/
|
|
93
|
+
function isFoundation(dir) {
|
|
94
|
+
const srcDir = join(dir, 'src')
|
|
95
|
+
const componentsDir = join(srcDir, 'components')
|
|
96
|
+
return existsSync(componentsDir)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Main docs command
|
|
101
|
+
*/
|
|
102
|
+
export async function docs(args) {
|
|
103
|
+
const options = parseArgs(args)
|
|
104
|
+
|
|
105
|
+
if (options.help) {
|
|
106
|
+
showHelp()
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const projectDir = resolve(process.cwd())
|
|
111
|
+
|
|
112
|
+
// Verify this is a foundation
|
|
113
|
+
if (!isFoundation(projectDir)) {
|
|
114
|
+
error('This directory does not appear to be a foundation.')
|
|
115
|
+
log(`${colors.dim}Foundations have a src/components/ directory with component folders.${colors.reset}`)
|
|
116
|
+
process.exit(1)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check if schema.json exists (if not using --from-source)
|
|
120
|
+
const schemaPath = join(projectDir, 'dist', 'schema.json')
|
|
121
|
+
if (!options.fromSource && !existsSync(schemaPath)) {
|
|
122
|
+
log(`${colors.yellow}⚠${colors.reset} No dist/schema.json found.`)
|
|
123
|
+
log(`${colors.dim}Run 'uniweb build' first, or use '--from-source' to read meta.js files directly.${colors.reset}`)
|
|
124
|
+
log('')
|
|
125
|
+
info('Falling back to --from-source mode')
|
|
126
|
+
options.fromSource = true
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
info('Generating documentation...')
|
|
131
|
+
|
|
132
|
+
const result = await generateDocs(projectDir, {
|
|
133
|
+
output: options.output,
|
|
134
|
+
fromSource: options.fromSource,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
success(`Generated ${result.outputPath}`)
|
|
138
|
+
log(`${colors.dim}Documented ${result.componentCount} components${colors.reset}`)
|
|
139
|
+
} catch (err) {
|
|
140
|
+
error(`Failed to generate docs: ${err.message}`)
|
|
141
|
+
process.exit(1)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n CLI Commands
|
|
3
|
+
*
|
|
4
|
+
* Commands for managing site content internationalization.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* uniweb i18n extract Extract translatable strings to manifest
|
|
8
|
+
* uniweb i18n sync Sync manifest with content changes
|
|
9
|
+
* uniweb i18n status Show translation coverage per locale
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { resolve, join } from 'path'
|
|
13
|
+
import { existsSync } from 'fs'
|
|
14
|
+
import { readFile } from 'fs/promises'
|
|
15
|
+
import yaml from 'js-yaml'
|
|
16
|
+
|
|
17
|
+
// Colors for terminal output
|
|
18
|
+
const colors = {
|
|
19
|
+
reset: '\x1b[0m',
|
|
20
|
+
bright: '\x1b[1m',
|
|
21
|
+
dim: '\x1b[2m',
|
|
22
|
+
cyan: '\x1b[36m',
|
|
23
|
+
green: '\x1b[32m',
|
|
24
|
+
yellow: '\x1b[33m',
|
|
25
|
+
red: '\x1b[31m',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function log(message) {
|
|
29
|
+
console.log(message)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function success(message) {
|
|
33
|
+
console.log(`${colors.green}✓${colors.reset} ${message}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function warn(message) {
|
|
37
|
+
console.log(`${colors.yellow}⚠${colors.reset} ${message}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function error(message) {
|
|
41
|
+
console.error(`${colors.red}✗${colors.reset} ${message}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Main i18n command handler
|
|
46
|
+
* @param {string[]} args - Command arguments
|
|
47
|
+
*/
|
|
48
|
+
export async function i18n(args) {
|
|
49
|
+
const subcommand = args[0]
|
|
50
|
+
|
|
51
|
+
if (!subcommand || subcommand === '--help' || subcommand === '-h') {
|
|
52
|
+
showHelp()
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Find site root
|
|
57
|
+
const siteRoot = await findSiteRoot()
|
|
58
|
+
if (!siteRoot) {
|
|
59
|
+
error('Could not find site root. Make sure you are in a Uniweb site directory.')
|
|
60
|
+
process.exit(1)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Load site config for locale settings
|
|
64
|
+
const config = await loadSiteConfig(siteRoot)
|
|
65
|
+
|
|
66
|
+
switch (subcommand) {
|
|
67
|
+
case 'extract':
|
|
68
|
+
await runExtract(siteRoot, config, args.slice(1))
|
|
69
|
+
break
|
|
70
|
+
case 'sync':
|
|
71
|
+
await runSync(siteRoot, config, args.slice(1))
|
|
72
|
+
break
|
|
73
|
+
case 'status':
|
|
74
|
+
await runStatus(siteRoot, config, args.slice(1))
|
|
75
|
+
break
|
|
76
|
+
default:
|
|
77
|
+
error(`Unknown subcommand: ${subcommand}`)
|
|
78
|
+
showHelp()
|
|
79
|
+
process.exit(1)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Find site root by looking for site.yml
|
|
85
|
+
*/
|
|
86
|
+
async function findSiteRoot() {
|
|
87
|
+
let dir = process.cwd()
|
|
88
|
+
|
|
89
|
+
// Check current directory and parents
|
|
90
|
+
for (let i = 0; i < 5; i++) {
|
|
91
|
+
if (existsSync(join(dir, 'site.yml'))) {
|
|
92
|
+
return dir
|
|
93
|
+
}
|
|
94
|
+
const parent = resolve(dir, '..')
|
|
95
|
+
if (parent === dir) break
|
|
96
|
+
dir = parent
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Load site configuration
|
|
104
|
+
*
|
|
105
|
+
* Resolves locales from config:
|
|
106
|
+
* - undefined → all available locales (from locales/*.json)
|
|
107
|
+
* - '*' → explicitly all available locales
|
|
108
|
+
* - ['es', 'fr'] → only those specific locales
|
|
109
|
+
*/
|
|
110
|
+
async function loadSiteConfig(siteRoot) {
|
|
111
|
+
const configPath = join(siteRoot, 'site.yml')
|
|
112
|
+
const content = await readFile(configPath, 'utf-8')
|
|
113
|
+
const config = yaml.load(content) || {}
|
|
114
|
+
|
|
115
|
+
const localesDir = config.i18n?.localesDir || 'locales'
|
|
116
|
+
const localesPath = join(siteRoot, localesDir)
|
|
117
|
+
|
|
118
|
+
// Resolve locales (undefined/'*' → all available, array → specific)
|
|
119
|
+
const { resolveLocales } = await import('@uniweb/build/i18n')
|
|
120
|
+
const locales = await resolveLocales(config.i18n?.locales, localesPath)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
defaultLocale: config.defaultLanguage || 'en',
|
|
124
|
+
locales,
|
|
125
|
+
localesDir,
|
|
126
|
+
...config.i18n,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Extract command - extract translatable strings from site content
|
|
132
|
+
*/
|
|
133
|
+
async function runExtract(siteRoot, config, args) {
|
|
134
|
+
const verbose = args.includes('--verbose') || args.includes('-v')
|
|
135
|
+
|
|
136
|
+
log(`\n${colors.cyan}Extracting translatable content...${colors.reset}\n`)
|
|
137
|
+
|
|
138
|
+
// Check if site has been built
|
|
139
|
+
const siteContentPath = join(siteRoot, 'dist', 'site-content.json')
|
|
140
|
+
if (!existsSync(siteContentPath)) {
|
|
141
|
+
error('Site content not found. Run "uniweb build" first.')
|
|
142
|
+
process.exit(1)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
// Dynamic import to avoid loading at CLI startup
|
|
147
|
+
const { extractManifest, formatSyncReport } = await import('@uniweb/build/i18n')
|
|
148
|
+
|
|
149
|
+
const { manifest, report } = await extractManifest(siteRoot, {
|
|
150
|
+
localesDir: config.localesDir,
|
|
151
|
+
siteContentPath,
|
|
152
|
+
verbose,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// Show results
|
|
156
|
+
const unitCount = Object.keys(manifest.units).length
|
|
157
|
+
success(`Extracted ${unitCount} translatable strings`)
|
|
158
|
+
|
|
159
|
+
if (report) {
|
|
160
|
+
log('')
|
|
161
|
+
log(formatSyncReport(report))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
log(`\nManifest written to: ${colors.dim}${config.localesDir}/manifest.json${colors.reset}`)
|
|
165
|
+
|
|
166
|
+
if (config.locales.length === 0) {
|
|
167
|
+
log(`\n${colors.dim}No translation files found in ${config.localesDir}/.`)
|
|
168
|
+
log(`After translating, create locale files like ${config.localesDir}/es.json${colors.reset}`)
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
error(`Extraction failed: ${err.message}`)
|
|
172
|
+
if (verbose) console.error(err)
|
|
173
|
+
process.exit(1)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Sync command - detect changes and update manifest
|
|
179
|
+
*/
|
|
180
|
+
async function runSync(siteRoot, config, args) {
|
|
181
|
+
const verbose = args.includes('--verbose') || args.includes('-v')
|
|
182
|
+
const dryRun = args.includes('--dry-run')
|
|
183
|
+
|
|
184
|
+
log(`\n${colors.cyan}Syncing i18n manifest...${colors.reset}\n`)
|
|
185
|
+
|
|
186
|
+
// Check if site has been built
|
|
187
|
+
const siteContentPath = join(siteRoot, 'dist', 'site-content.json')
|
|
188
|
+
if (!existsSync(siteContentPath)) {
|
|
189
|
+
error('Site content not found. Run "uniweb build" first.')
|
|
190
|
+
process.exit(1)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check if manifest exists
|
|
194
|
+
const manifestPath = join(siteRoot, config.localesDir, 'manifest.json')
|
|
195
|
+
if (!existsSync(manifestPath)) {
|
|
196
|
+
warn('No existing manifest found. Running extract instead.')
|
|
197
|
+
return runExtract(siteRoot, config, args)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const { extractManifest, formatSyncReport } = await import('@uniweb/build/i18n')
|
|
202
|
+
|
|
203
|
+
if (dryRun) {
|
|
204
|
+
log(`${colors.dim}(dry run - no files will be modified)${colors.reset}\n`)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const { manifest, report } = await extractManifest(siteRoot, {
|
|
208
|
+
localesDir: config.localesDir,
|
|
209
|
+
siteContentPath,
|
|
210
|
+
verbose,
|
|
211
|
+
dryRun,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
log(formatSyncReport(report))
|
|
215
|
+
|
|
216
|
+
if (!dryRun) {
|
|
217
|
+
success('\nManifest updated')
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
error(`Sync failed: ${err.message}`)
|
|
221
|
+
if (verbose) console.error(err)
|
|
222
|
+
process.exit(1)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Status command - show translation coverage
|
|
228
|
+
*/
|
|
229
|
+
async function runStatus(siteRoot, config, args) {
|
|
230
|
+
const locale = args.find(a => !a.startsWith('-'))
|
|
231
|
+
|
|
232
|
+
log(`\n${colors.cyan}Translation Status${colors.reset}\n`)
|
|
233
|
+
|
|
234
|
+
// Check if manifest exists
|
|
235
|
+
const manifestPath = join(siteRoot, config.localesDir, 'manifest.json')
|
|
236
|
+
if (!existsSync(manifestPath)) {
|
|
237
|
+
error('No manifest found. Run "uniweb i18n extract" first.')
|
|
238
|
+
process.exit(1)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (config.locales.length === 0) {
|
|
242
|
+
log(`${colors.dim}No translation files found in ${config.localesDir}/.`)
|
|
243
|
+
log(`Create locale files like ${config.localesDir}/es.json to add translations.${colors.reset}`)
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const { getTranslationStatus, formatTranslationStatus } = await import('@uniweb/build/i18n')
|
|
249
|
+
|
|
250
|
+
const localesToCheck = locale ? [locale] : config.locales
|
|
251
|
+
|
|
252
|
+
const status = await getTranslationStatus(siteRoot, {
|
|
253
|
+
localesDir: config.localesDir,
|
|
254
|
+
locales: localesToCheck,
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
log(formatTranslationStatus(status))
|
|
258
|
+
|
|
259
|
+
// Show next steps if there are missing translations
|
|
260
|
+
const hasMissing = Object.values(status.locales).some(l => l.missing > 0)
|
|
261
|
+
if (hasMissing) {
|
|
262
|
+
log(`\n${colors.dim}To translate missing strings, edit the locale files in ${config.localesDir}/`)
|
|
263
|
+
log(`Or use AI tools with the manifest.json as reference.${colors.reset}`)
|
|
264
|
+
}
|
|
265
|
+
} catch (err) {
|
|
266
|
+
error(`Status check failed: ${err.message}`)
|
|
267
|
+
process.exit(1)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Show help for i18n commands
|
|
273
|
+
*/
|
|
274
|
+
function showHelp() {
|
|
275
|
+
log(`
|
|
276
|
+
${colors.cyan}${colors.bright}Uniweb i18n${colors.reset}
|
|
277
|
+
|
|
278
|
+
Site content internationalization commands.
|
|
279
|
+
|
|
280
|
+
${colors.bright}Usage:${colors.reset}
|
|
281
|
+
uniweb i18n <command> [options]
|
|
282
|
+
|
|
283
|
+
${colors.bright}Commands:${colors.reset}
|
|
284
|
+
extract Extract translatable strings to locales/manifest.json
|
|
285
|
+
sync Update manifest with content changes (detects moved/changed content)
|
|
286
|
+
status Show translation coverage per locale
|
|
287
|
+
|
|
288
|
+
${colors.bright}Options:${colors.reset}
|
|
289
|
+
--verbose Show detailed output
|
|
290
|
+
--dry-run (sync) Show changes without writing files
|
|
291
|
+
|
|
292
|
+
${colors.bright}Configuration:${colors.reset}
|
|
293
|
+
Optional site.yml settings:
|
|
294
|
+
|
|
295
|
+
i18n:
|
|
296
|
+
locales: [es, fr] # Specific locales only (default: all available)
|
|
297
|
+
locales: '*' # Explicitly all available locales
|
|
298
|
+
localesDir: locales # Directory for translation files (default: locales)
|
|
299
|
+
|
|
300
|
+
By default, all *.json files in locales/ are treated as translation targets.
|
|
301
|
+
|
|
302
|
+
${colors.bright}Workflow:${colors.reset}
|
|
303
|
+
1. Build your site: uniweb build
|
|
304
|
+
2. Extract strings: uniweb i18n extract
|
|
305
|
+
3. Translate locale files: Edit locales/es.json, locales/fr.json, etc.
|
|
306
|
+
4. Build with translations: uniweb build (generates locale-specific output)
|
|
307
|
+
|
|
308
|
+
${colors.bright}File Structure:${colors.reset}
|
|
309
|
+
locales/
|
|
310
|
+
manifest.json Auto-generated: source strings + hashes + contexts
|
|
311
|
+
es.json Translations for Spanish
|
|
312
|
+
fr.json Translations for French
|
|
313
|
+
_memory.json Optional: translation memory for reuse
|
|
314
|
+
|
|
315
|
+
${colors.bright}Examples:${colors.reset}
|
|
316
|
+
uniweb i18n extract # Extract all translatable strings
|
|
317
|
+
uniweb i18n extract --verbose # Show extracted strings
|
|
318
|
+
uniweb i18n sync # Update manifest after content changes
|
|
319
|
+
uniweb i18n sync --dry-run # Preview changes without writing
|
|
320
|
+
uniweb i18n status # Show coverage for all locales
|
|
321
|
+
uniweb i18n status es # Show coverage for Spanish only
|
|
322
|
+
`)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export default i18n
|
package/src/index.js
CHANGED
|
@@ -3,14 +3,13 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Uniweb CLI
|
|
5
5
|
*
|
|
6
|
-
* Scaffolds new Uniweb sites and foundations, and
|
|
6
|
+
* Scaffolds new Uniweb sites and foundations, builds projects, and generates docs.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
9
|
* npx uniweb create [project-name]
|
|
10
|
-
* npx uniweb create --template
|
|
11
|
-
* npx uniweb create --template multi # sites/* + foundations/*
|
|
10
|
+
* npx uniweb create --template marketing
|
|
12
11
|
* npx uniweb build
|
|
13
|
-
* npx uniweb
|
|
12
|
+
* npx uniweb docs # Generate COMPONENTS.md from schema
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
15
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs'
|
|
@@ -18,6 +17,8 @@ import { resolve, join, dirname } from 'node:path'
|
|
|
18
17
|
import { fileURLToPath } from 'node:url'
|
|
19
18
|
import prompts from 'prompts'
|
|
20
19
|
import { build } from './commands/build.js'
|
|
20
|
+
import { docs } from './commands/docs.js'
|
|
21
|
+
import { i18n } from './commands/i18n.js'
|
|
21
22
|
import { getVersionsForTemplates, getVersion } from './versions.js'
|
|
22
23
|
import {
|
|
23
24
|
resolveTemplate,
|
|
@@ -84,6 +85,18 @@ async function main() {
|
|
|
84
85
|
return
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
// Handle docs command
|
|
89
|
+
if (command === 'docs') {
|
|
90
|
+
await docs(args.slice(1))
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Handle i18n command
|
|
95
|
+
if (command === 'i18n') {
|
|
96
|
+
await i18n(args.slice(1))
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
87
100
|
// Handle create command
|
|
88
101
|
if (command !== 'create') {
|
|
89
102
|
error(`Unknown command: ${command}`)
|
|
@@ -239,6 +252,8 @@ ${colors.bright}Usage:${colors.reset}
|
|
|
239
252
|
${colors.bright}Commands:${colors.reset}
|
|
240
253
|
create [name] Create a new project
|
|
241
254
|
build Build the current project
|
|
255
|
+
docs Generate component documentation
|
|
256
|
+
i18n <cmd> Internationalization (extract, sync, status)
|
|
242
257
|
|
|
243
258
|
${colors.bright}Create Options:${colors.reset}
|
|
244
259
|
--template <type> Project template
|
|
@@ -251,6 +266,15 @@ ${colors.bright}Build Options:${colors.reset}
|
|
|
251
266
|
--foundation-dir Path to foundation directory (for prerendering)
|
|
252
267
|
--platform <name> Deployment platform (e.g., vercel) for platform-specific output
|
|
253
268
|
|
|
269
|
+
${colors.bright}Docs Options:${colors.reset}
|
|
270
|
+
--output <file> Output filename (default: COMPONENTS.md)
|
|
271
|
+
--from-source Read meta.js files directly instead of schema.json
|
|
272
|
+
|
|
273
|
+
${colors.bright}i18n Commands:${colors.reset}
|
|
274
|
+
extract Extract translatable strings to manifest
|
|
275
|
+
sync Update manifest with content changes
|
|
276
|
+
status Show translation coverage per locale
|
|
277
|
+
|
|
254
278
|
${colors.bright}Template Types:${colors.reset}
|
|
255
279
|
single One site + one foundation (default)
|
|
256
280
|
multi Multiple sites and foundations
|
|
@@ -268,6 +292,7 @@ ${colors.bright}Examples:${colors.reset}
|
|
|
268
292
|
npx uniweb build
|
|
269
293
|
npx uniweb build --target foundation
|
|
270
294
|
npx uniweb build --prerender # Build site + pre-render to static HTML
|
|
295
|
+
cd foundation && npx uniweb docs # Generate COMPONENTS.md
|
|
271
296
|
`)
|
|
272
297
|
}
|
|
273
298
|
|
package/src/versions.js
CHANGED
|
@@ -72,6 +72,9 @@ export function getResolvedVersions() {
|
|
|
72
72
|
'@uniweb/runtime': '^0.1.0',
|
|
73
73
|
'@uniweb/core': '^0.1.0',
|
|
74
74
|
|
|
75
|
+
// Foundation utility library (used by official templates)
|
|
76
|
+
'@uniweb/kit': '^0.1.0',
|
|
77
|
+
|
|
75
78
|
// CLI itself (use current version)
|
|
76
79
|
'uniweb': `^${pkg.version}`,
|
|
77
80
|
}
|
|
@@ -106,6 +109,7 @@ export function getVersionsForTemplates() {
|
|
|
106
109
|
build: versions['@uniweb/build'],
|
|
107
110
|
runtime: versions['@uniweb/runtime'],
|
|
108
111
|
core: versions['@uniweb/core'],
|
|
112
|
+
kit: versions['@uniweb/kit'],
|
|
109
113
|
templates: versions['@uniweb/templates'],
|
|
110
114
|
cli: versions['uniweb'],
|
|
111
115
|
}
|