@lexibase/rdx-next 6.0.1 → 6.1.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/bin/cli.mjs CHANGED
@@ -67,6 +67,9 @@ export async function runRdxCli({ templateDir } = {}) {
67
67
  console.log(
68
68
  ' rdx add i18n [--locales en,es-ES] [--package-manager npm|pnpm|yarn]'
69
69
  )
70
+ console.log(
71
+ ' rdx i18n sync: add missing phrases to homepage translation catalogs'
72
+ )
70
73
  console.log(
71
74
  'Plugin commands are registered through plugins in rdx.config.ts.'
72
75
  )
@@ -86,6 +89,18 @@ export async function runRdxCli({ templateDir } = {}) {
86
89
  if (commandArgs.length) throw new Error('Usage: rdx check')
87
90
  }
88
91
 
92
+ const syncHomepage = command === 'i18n' && commandArgs[0] === 'sync'
93
+ if (
94
+ syncHomepage &&
95
+ commandArgs.length === 2 &&
96
+ ['--help', '-h'].includes(commandArgs[1])
97
+ ) {
98
+ console.log(
99
+ 'Usage: rdx i18n sync\nAdd missing homepage phrases to the configured languages’ home.json files.'
100
+ )
101
+ return
102
+ }
103
+
89
104
  if (command === 'add') {
90
105
  if (
91
106
  (commandArgs.length === 1 &&
@@ -145,6 +160,16 @@ export async function runRdxCli({ templateDir } = {}) {
145
160
  rootDir: projectRoot,
146
161
  command,
147
162
  })
163
+ if (
164
+ syncHomepage &&
165
+ !project.commands.some((entry) => entry.name === 'i18n')
166
+ ) {
167
+ if (commandArgs.length !== 1) throw new Error('Usage: rdx i18n sync')
168
+ const { syncHomepageTranslations } =
169
+ await import('./homepage-translations.mjs')
170
+ await syncHomepageTranslations(project)
171
+ return
172
+ }
148
173
  let homepageTranslations
149
174
  if (
150
175
  project.config.homepage?.enabled &&
@@ -1,4 +1,12 @@
1
- import { readFile, writeFile } from 'node:fs/promises'
1
+ import { randomUUID } from 'node:crypto'
2
+ import {
3
+ lstat,
4
+ readFile,
5
+ realpath,
6
+ rename,
7
+ rm,
8
+ writeFile,
9
+ } from 'node:fs/promises'
2
10
  import path from 'node:path'
3
11
 
4
12
  const sourceFile = 'components/home.tsx'
@@ -16,7 +24,7 @@ async function readOptional(file) {
16
24
  }
17
25
  }
18
26
 
19
- async function sourcePhrases(rootDir) {
27
+ async function sourcePhrases(rootDir, report) {
20
28
  const source = await readOptional(path.join(rootDir, sourceFile))
21
29
  if (source === undefined)
22
30
  throw new Error(`Create ${sourceFile} to use homepage.translations.`)
@@ -99,20 +107,118 @@ async function sourcePhrases(rootDir) {
99
107
  ts.forEachChild(node, visit)
100
108
  }
101
109
  visit(file)
110
+ if (report) {
111
+ for (const { line, message } of warnings.sort((a, b) => a.line - b.line))
112
+ console.warn(`[RDX] ${sourceFile}:${line}: ${message}`)
113
+ }
114
+ phrases.delete(metadataKey)
102
115
  return { phrases, warnings }
103
116
  }
104
117
 
118
+ function parseCatalog(raw) {
119
+ const parsed = JSON.parse(raw)
120
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
121
+ throw new Error('Expected a JSON object.')
122
+ return parsed
123
+ }
124
+
125
+ export async function syncHomepageTranslations(project) {
126
+ const { rootDir, config } = project
127
+ const locales = project.sources.find((source) => source.id === 'current')
128
+ ?.localization?.locales
129
+ if (!locales)
130
+ throw new Error('Enable the i18n plugin before running rdx i18n sync.')
131
+ if (!config.homepage?.enabled || !config.homepage.translations)
132
+ throw new Error(
133
+ 'Set homepage.enabled and homepage.translations to true before running rdx i18n sync.'
134
+ )
135
+
136
+ const { phrases } = await sourcePhrases(rootDir, true)
137
+ if (!phrases.size) {
138
+ console.log(`[RDX] No literal homepage phrases found in ${sourceFile}.`)
139
+ return
140
+ }
141
+ const root = await realpath(rootDir)
142
+ const updates = []
143
+ for (const { folder } of locales) {
144
+ if (!folder) continue
145
+ const file = `locale/${folder}/home.json`
146
+ const directory = await realpath(path.join(root, 'locale', folder))
147
+ const relative = path.relative(root, directory)
148
+ if (
149
+ !relative ||
150
+ relative === '..' ||
151
+ relative.startsWith(`..${path.sep}`) ||
152
+ path.isAbsolute(relative)
153
+ )
154
+ throw new Error(`${file} must remain inside the project.`)
155
+ const target = path.join(directory, 'home.json')
156
+ const info = await lstat(target).catch((error) => {
157
+ if (!missingFile(error)) throw error
158
+ })
159
+ if (info && !info.isFile())
160
+ throw new Error(`${file} must be a regular file.`)
161
+ const raw = info ? await readFile(target, 'utf8') : undefined
162
+ let messages = {}
163
+ if (raw !== undefined) {
164
+ try {
165
+ messages = parseCatalog(raw)
166
+ } catch {
167
+ throw new Error(
168
+ `Cannot sync ${file}: expected a JSON object of homepage phrases.`
169
+ )
170
+ }
171
+ }
172
+ const missing = [...phrases].filter(
173
+ (phrase) => !Object.hasOwn(messages, phrase)
174
+ )
175
+ if (missing.length)
176
+ updates.push({
177
+ file,
178
+ target,
179
+ info,
180
+ raw,
181
+ count: missing.length,
182
+ messages: Object.fromEntries([
183
+ ...Object.entries(messages),
184
+ ...missing.map((phrase) => [phrase, '']),
185
+ ]),
186
+ })
187
+ }
188
+
189
+ for (const { file, target, info, raw, count, messages } of updates) {
190
+ const content = JSON.stringify(messages, null, 2) + '\n'
191
+ if (raw === undefined) {
192
+ await writeFile(target, content, { flag: 'wx' })
193
+ } else {
194
+ const temporary = `${target}.${randomUUID()}.tmp`
195
+ try {
196
+ await writeFile(temporary, content, { flag: 'wx', mode: info.mode })
197
+ if (
198
+ !(await lstat(target)).isFile() ||
199
+ (await readFile(target, 'utf8')) !== raw
200
+ )
201
+ throw new Error(
202
+ `${file} changed during sync. Run rdx i18n sync again.`
203
+ )
204
+ await rename(temporary, target)
205
+ } finally {
206
+ await rm(temporary, { force: true })
207
+ }
208
+ }
209
+ console.log(
210
+ `[RDX] ${file}: added ${count} ${count === 1 ? 'phrase' : 'phrases'}.`
211
+ )
212
+ }
213
+ if (!updates.length) console.log('[RDX] Homepage catalogs are up to date.')
214
+ }
215
+
105
216
  export async function readHomepageTranslations(
106
217
  project,
107
218
  { report = false } = {}
108
219
  ) {
109
220
  const { rootDir, localization } = project
110
- const { phrases, warnings } = await sourcePhrases(rootDir)
111
- if (report) {
112
- for (const { line, message } of warnings.sort((a, b) => a.line - b.line))
113
- console.warn(`[RDX] ${sourceFile}:${line}: ${message}`)
114
- }
115
- phrases.delete(metadataKey)
221
+ const { phrases, warnings } = await sourcePhrases(rootDir, report)
116
222
  const catalogs = []
117
223
  for (const locale of localization?.locales ?? []) {
118
224
  if (!locale.folder) continue
@@ -121,10 +227,7 @@ export async function readHomepageTranslations(
121
227
  let messages = {}
122
228
  if (raw !== undefined) {
123
229
  try {
124
- const parsed = JSON.parse(raw)
125
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
126
- throw new Error('Expected a JSON object.')
127
- messages = parsed
230
+ messages = parseCatalog(raw)
128
231
  catalogs.push({ file, locale: locale.path })
129
232
  } catch {
130
233
  console.warn(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexibase/rdx-next",
3
- "version": "6.0.1",
3
+ "version": "6.1.0",
4
4
  "description": "The Next.js runtime for RDX documentation projects.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -82,8 +82,8 @@
82
82
  "react": "19.3.1",
83
83
  "react-dom": "19.3.1",
84
84
  "@rdx/eslint-config": "1.0.0",
85
- "@rdx/typescript-config": "1.0.0",
86
- "@lexibase/rdx-ui": "3.5.5"
85
+ "@lexibase/rdx-ui": "3.5.6",
86
+ "@rdx/typescript-config": "1.0.0"
87
87
  },
88
88
  "scripts": {
89
89
  "dev": "tsup src/index.ts src/runtime/index.tsx --format esm --dts --watch --external @lexibase/rdx-core --external @lexibase/rdx-types --external @lexibase/rdx-ui --external next --external react --external react-dom --external next-mdx-remote",