uniweb 0.2.13 → 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/i18n.js +325 -0
- package/src/index.js +13 -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,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
|
@@ -18,6 +18,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
18
18
|
import prompts from 'prompts'
|
|
19
19
|
import { build } from './commands/build.js'
|
|
20
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,
|
|
@@ -90,6 +91,12 @@ async function main() {
|
|
|
90
91
|
return
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
// Handle i18n command
|
|
95
|
+
if (command === 'i18n') {
|
|
96
|
+
await i18n(args.slice(1))
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
93
100
|
// Handle create command
|
|
94
101
|
if (command !== 'create') {
|
|
95
102
|
error(`Unknown command: ${command}`)
|
|
@@ -246,6 +253,7 @@ ${colors.bright}Commands:${colors.reset}
|
|
|
246
253
|
create [name] Create a new project
|
|
247
254
|
build Build the current project
|
|
248
255
|
docs Generate component documentation
|
|
256
|
+
i18n <cmd> Internationalization (extract, sync, status)
|
|
249
257
|
|
|
250
258
|
${colors.bright}Create Options:${colors.reset}
|
|
251
259
|
--template <type> Project template
|
|
@@ -262,6 +270,11 @@ ${colors.bright}Docs Options:${colors.reset}
|
|
|
262
270
|
--output <file> Output filename (default: COMPONENTS.md)
|
|
263
271
|
--from-source Read meta.js files directly instead of schema.json
|
|
264
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
|
+
|
|
265
278
|
${colors.bright}Template Types:${colors.reset}
|
|
266
279
|
single One site + one foundation (default)
|
|
267
280
|
multi Multiple sites and foundations
|