@uniweb/build 0.3.2 → 0.4.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/package.json +4 -4
- package/src/i18n/collections.js +86 -8
- package/src/i18n/freeform-manifest.js +307 -0
- package/src/i18n/freeform.js +336 -0
- package/src/i18n/index.js +131 -7
- package/src/i18n/merge.js +102 -6
- package/src/site/content-collector.js +45 -12
- package/src/site/icons.js +180 -0
- package/src/site/plugin.js +46 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"sharp": "^0.33.2"
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
|
-
"@uniweb/content-reader": "1.
|
|
54
|
-
"@uniweb/runtime": "0.
|
|
53
|
+
"@uniweb/content-reader": "1.1.1",
|
|
54
|
+
"@uniweb/runtime": "0.5.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"@tailwindcss/vite": "^4.0.0",
|
|
61
61
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
62
62
|
"vite-plugin-svgr": "^4.0.0",
|
|
63
|
-
"@uniweb/core": "0.
|
|
63
|
+
"@uniweb/core": "0.3.1"
|
|
64
64
|
},
|
|
65
65
|
"peerDependenciesMeta": {
|
|
66
66
|
"vite": {
|
package/src/i18n/collections.js
CHANGED
|
@@ -9,6 +9,7 @@ import { readFile, writeFile, readdir, mkdir } from 'fs/promises'
|
|
|
9
9
|
import { existsSync } from 'fs'
|
|
10
10
|
import { join } from 'path'
|
|
11
11
|
import { computeHash } from './hash.js'
|
|
12
|
+
import { loadFreeformCollectionItem } from './freeform.js'
|
|
12
13
|
|
|
13
14
|
export const COLLECTIONS_DIR = 'collections'
|
|
14
15
|
|
|
@@ -198,13 +199,16 @@ function addUnit(units, source, field, context) {
|
|
|
198
199
|
* Merge translations into collection data and write locale-specific files
|
|
199
200
|
* @param {string} siteRoot - Site root directory
|
|
200
201
|
* @param {Object} options - Options
|
|
202
|
+
* @param {boolean} [options.freeformEnabled=true] - Enable free-form translation support
|
|
201
203
|
* @returns {Promise<Object>} Map of locale to output paths
|
|
202
204
|
*/
|
|
203
205
|
export async function buildLocalizedCollections(siteRoot, options = {}) {
|
|
204
206
|
const {
|
|
205
207
|
locales = [],
|
|
206
208
|
outputDir = join(siteRoot, 'dist'),
|
|
207
|
-
collectionsLocalesDir = join(siteRoot, 'locales', COLLECTIONS_DIR)
|
|
209
|
+
collectionsLocalesDir = join(siteRoot, 'locales', COLLECTIONS_DIR),
|
|
210
|
+
localesDir = join(siteRoot, 'locales'),
|
|
211
|
+
freeformEnabled = true
|
|
208
212
|
} = options
|
|
209
213
|
|
|
210
214
|
const dataDir = join(siteRoot, 'public', 'data')
|
|
@@ -240,6 +244,10 @@ export async function buildLocalizedCollections(siteRoot, options = {}) {
|
|
|
240
244
|
}
|
|
241
245
|
}
|
|
242
246
|
|
|
247
|
+
// Check if free-form translations exist for this locale
|
|
248
|
+
const freeformDir = join(localesDir, 'freeform', locale)
|
|
249
|
+
const hasFreeform = freeformEnabled && existsSync(freeformDir)
|
|
250
|
+
|
|
243
251
|
// Create locale data directory
|
|
244
252
|
const localeDataDir = join(outputDir, locale, 'data')
|
|
245
253
|
await mkdir(localeDataDir, { recursive: true })
|
|
@@ -262,9 +270,15 @@ export async function buildLocalizedCollections(siteRoot, options = {}) {
|
|
|
262
270
|
continue
|
|
263
271
|
}
|
|
264
272
|
|
|
265
|
-
// Translate each item
|
|
266
|
-
const translatedItems =
|
|
267
|
-
|
|
273
|
+
// Translate each item (with free-form support)
|
|
274
|
+
const translatedItems = await Promise.all(
|
|
275
|
+
items.map(item =>
|
|
276
|
+
translateItemAsync(item, collectionName, translations, {
|
|
277
|
+
locale,
|
|
278
|
+
localesDir,
|
|
279
|
+
freeformEnabled: hasFreeform
|
|
280
|
+
})
|
|
281
|
+
)
|
|
268
282
|
)
|
|
269
283
|
|
|
270
284
|
const destPath = join(localeDataDir, file)
|
|
@@ -280,13 +294,69 @@ export async function buildLocalizedCollections(siteRoot, options = {}) {
|
|
|
280
294
|
}
|
|
281
295
|
|
|
282
296
|
/**
|
|
283
|
-
* Apply translations to a collection item
|
|
297
|
+
* Apply translations to a collection item (async, with free-form support)
|
|
298
|
+
*
|
|
299
|
+
* Resolution order:
|
|
300
|
+
* 1. Check for free-form translation (complete or partial replacement)
|
|
301
|
+
* 2. Fall back to hash-based translation
|
|
302
|
+
*
|
|
303
|
+
* @param {Object} item - Collection item
|
|
304
|
+
* @param {string} collectionName - Name of the collection
|
|
305
|
+
* @param {Object} translations - Hash-based translations
|
|
306
|
+
* @param {Object} options - Options
|
|
307
|
+
* @returns {Promise<Object>} Translated item
|
|
284
308
|
*/
|
|
285
|
-
function
|
|
309
|
+
async function translateItemAsync(item, collectionName, translations, options = {}) {
|
|
310
|
+
const { locale, localesDir, freeformEnabled } = options
|
|
286
311
|
const translated = { ...item }
|
|
287
312
|
const slug = item.slug || 'unknown'
|
|
288
313
|
const context = { collection: collectionName, item: slug }
|
|
289
314
|
|
|
315
|
+
// Check for free-form translation first
|
|
316
|
+
if (freeformEnabled && locale && localesDir) {
|
|
317
|
+
const freeform = await loadFreeformCollectionItem(item, collectionName, locale, localesDir)
|
|
318
|
+
|
|
319
|
+
if (freeform) {
|
|
320
|
+
// Merge free-form data (supports partial: frontmatter only, body only, or both)
|
|
321
|
+
if (freeform.frontmatter) {
|
|
322
|
+
// Merge frontmatter fields from free-form
|
|
323
|
+
Object.assign(translated, freeform.frontmatter)
|
|
324
|
+
}
|
|
325
|
+
if (freeform.content) {
|
|
326
|
+
// Replace content entirely with free-form content
|
|
327
|
+
translated.content = freeform.content
|
|
328
|
+
// Skip hash-based content translation since we have free-form
|
|
329
|
+
return translateItemFields(translated, context, translations)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Fall back to hash-based translation (or continue after partial free-form)
|
|
335
|
+
return translateItemSync(translated, collectionName, translations)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Apply translations to a collection item (sync, hash-based only)
|
|
340
|
+
*/
|
|
341
|
+
function translateItemSync(item, collectionName, translations) {
|
|
342
|
+
const translated = { ...item }
|
|
343
|
+
const slug = item.slug || 'unknown'
|
|
344
|
+
const context = { collection: collectionName, item: slug }
|
|
345
|
+
|
|
346
|
+
// Translate all fields including content
|
|
347
|
+
return translateItemFields(translated, context, translations, true)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Translate item fields (frontmatter and optionally content)
|
|
352
|
+
*
|
|
353
|
+
* @param {Object} translated - Item being translated
|
|
354
|
+
* @param {Object} context - Translation context
|
|
355
|
+
* @param {Object} translations - Hash-based translations
|
|
356
|
+
* @param {boolean} includeContent - Whether to translate ProseMirror content
|
|
357
|
+
* @returns {Object} Translated item
|
|
358
|
+
*/
|
|
359
|
+
function translateItemFields(translated, context, translations, includeContent = false) {
|
|
290
360
|
// Translate frontmatter fields
|
|
291
361
|
const translatableFields = ['title', 'description', 'excerpt', 'summary', 'subtitle']
|
|
292
362
|
|
|
@@ -320,8 +390,8 @@ function translateItem(item, collectionName, translations) {
|
|
|
320
390
|
})
|
|
321
391
|
}
|
|
322
392
|
|
|
323
|
-
// Translate ProseMirror content
|
|
324
|
-
if (translated.content?.type === 'doc') {
|
|
393
|
+
// Translate ProseMirror content (only if requested)
|
|
394
|
+
if (includeContent && translated.content?.type === 'doc') {
|
|
325
395
|
translated.content = translateProseMirrorDoc(
|
|
326
396
|
translated.content,
|
|
327
397
|
context,
|
|
@@ -332,6 +402,14 @@ function translateItem(item, collectionName, translations) {
|
|
|
332
402
|
return translated
|
|
333
403
|
}
|
|
334
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Apply translations to a collection item (legacy sync function)
|
|
407
|
+
* @deprecated Use translateItemAsync for free-form support
|
|
408
|
+
*/
|
|
409
|
+
function translateItem(item, collectionName, translations) {
|
|
410
|
+
return translateItemSync(item, collectionName, translations)
|
|
411
|
+
}
|
|
412
|
+
|
|
335
413
|
/**
|
|
336
414
|
* Translate a ProseMirror document
|
|
337
415
|
*/
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Free-form Translation Manifest Management
|
|
3
|
+
*
|
|
4
|
+
* Tracks source content hashes for free-form translations to detect
|
|
5
|
+
* when source content changes (staleness). The manifest lives at:
|
|
6
|
+
* locales/freeform/{locale}/.manifest.json
|
|
7
|
+
*
|
|
8
|
+
* Manifest format:
|
|
9
|
+
* {
|
|
10
|
+
* "pages/about/hero.md": { "hash": "a1b2c3d4", "recorded": "2025-01-28" },
|
|
11
|
+
* "page-ids/installation/intro.md": { "hash": "b2c3d4e5", "recorded": "2025-01-25" },
|
|
12
|
+
* "collections/articles/getting-started.md": { "hash": "c3d4e5f6", "recorded": "2025-01-27" }
|
|
13
|
+
* }
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFile, writeFile, mkdir } from 'fs/promises'
|
|
17
|
+
import { existsSync } from 'fs'
|
|
18
|
+
import { join, dirname } from 'path'
|
|
19
|
+
import { createHash } from 'crypto'
|
|
20
|
+
|
|
21
|
+
const MANIFEST_FILENAME = '.manifest.json'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Compute a hash of a ProseMirror document for staleness detection
|
|
25
|
+
*
|
|
26
|
+
* Extracts text content and computes a hash that changes when
|
|
27
|
+
* the content changes (ignoring formatting-only changes).
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} doc - ProseMirror document
|
|
30
|
+
* @returns {string} 8-character hex hash
|
|
31
|
+
*/
|
|
32
|
+
export function computeSourceHash(doc) {
|
|
33
|
+
if (!doc) return ''
|
|
34
|
+
|
|
35
|
+
// Extract all text content from the document
|
|
36
|
+
const textContent = extractTextFromDoc(doc)
|
|
37
|
+
|
|
38
|
+
// Hash the normalized text
|
|
39
|
+
return createHash('sha256')
|
|
40
|
+
.update(textContent)
|
|
41
|
+
.digest('hex')
|
|
42
|
+
.slice(0, 8)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Recursively extract text from a ProseMirror document
|
|
47
|
+
* @param {Object} node - ProseMirror node
|
|
48
|
+
* @returns {string} Concatenated text content
|
|
49
|
+
*/
|
|
50
|
+
function extractTextFromDoc(node) {
|
|
51
|
+
if (!node) return ''
|
|
52
|
+
|
|
53
|
+
if (node.type === 'text') {
|
|
54
|
+
return node.text || ''
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!node.content || !Array.isArray(node.content)) {
|
|
58
|
+
return ''
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return node.content.map(extractTextFromDoc).join(' ')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the manifest path for a locale
|
|
66
|
+
* @param {string} localeDir - Path to locale's freeform directory
|
|
67
|
+
* @returns {string} Path to manifest file
|
|
68
|
+
*/
|
|
69
|
+
function getManifestPath(localeDir) {
|
|
70
|
+
return join(localeDir, MANIFEST_FILENAME)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Load the manifest for a locale
|
|
75
|
+
*
|
|
76
|
+
* @param {string} localeDir - Path to locale's freeform directory (locales/freeform/{locale})
|
|
77
|
+
* @returns {Promise<Object>} Manifest object (empty if not found)
|
|
78
|
+
*/
|
|
79
|
+
export async function loadManifest(localeDir) {
|
|
80
|
+
const manifestPath = getManifestPath(localeDir)
|
|
81
|
+
|
|
82
|
+
if (!existsSync(manifestPath)) {
|
|
83
|
+
return {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const content = await readFile(manifestPath, 'utf-8')
|
|
88
|
+
return JSON.parse(content)
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.warn(`[i18n] Failed to load manifest ${manifestPath}: ${err.message}`)
|
|
91
|
+
return {}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Save the manifest for a locale
|
|
97
|
+
*
|
|
98
|
+
* @param {string} localeDir - Path to locale's freeform directory
|
|
99
|
+
* @param {Object} manifest - Manifest object to save
|
|
100
|
+
*/
|
|
101
|
+
export async function saveManifest(localeDir, manifest) {
|
|
102
|
+
const manifestPath = getManifestPath(localeDir)
|
|
103
|
+
|
|
104
|
+
// Ensure directory exists
|
|
105
|
+
if (!existsSync(localeDir)) {
|
|
106
|
+
await mkdir(localeDir, { recursive: true })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Record a source hash in the manifest
|
|
114
|
+
*
|
|
115
|
+
* Called when a new free-form translation is created to record
|
|
116
|
+
* the hash of the source content at that time.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} localeDir - Path to locale's freeform directory
|
|
119
|
+
* @param {string} relativePath - Path relative to locale dir (e.g., 'pages/about/hero.md')
|
|
120
|
+
* @param {string} sourceHash - Hash of the source content
|
|
121
|
+
* @returns {Promise<void>}
|
|
122
|
+
*/
|
|
123
|
+
export async function recordHash(localeDir, relativePath, sourceHash) {
|
|
124
|
+
const manifest = await loadManifest(localeDir)
|
|
125
|
+
|
|
126
|
+
manifest[relativePath] = {
|
|
127
|
+
hash: sourceHash,
|
|
128
|
+
recorded: new Date().toISOString().split('T')[0] // YYYY-MM-DD
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await saveManifest(localeDir, manifest)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if a translation is stale (source content changed)
|
|
136
|
+
*
|
|
137
|
+
* @param {string} localeDir - Path to locale's freeform directory
|
|
138
|
+
* @param {string} relativePath - Path relative to locale dir
|
|
139
|
+
* @param {string} currentSourceHash - Current hash of source content
|
|
140
|
+
* @returns {Promise<Object>} { isStale, recordedHash, recordedDate, currentHash }
|
|
141
|
+
*/
|
|
142
|
+
export async function checkStaleness(localeDir, relativePath, currentSourceHash) {
|
|
143
|
+
const manifest = await loadManifest(localeDir)
|
|
144
|
+
const entry = manifest[relativePath]
|
|
145
|
+
|
|
146
|
+
if (!entry) {
|
|
147
|
+
// No recorded hash - translation was never registered
|
|
148
|
+
return {
|
|
149
|
+
isStale: false, // Not stale, just unregistered
|
|
150
|
+
isNew: true,
|
|
151
|
+
recordedHash: null,
|
|
152
|
+
recordedDate: null,
|
|
153
|
+
currentHash: currentSourceHash
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const isStale = entry.hash !== currentSourceHash
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
isStale,
|
|
161
|
+
isNew: false,
|
|
162
|
+
recordedHash: entry.hash,
|
|
163
|
+
recordedDate: entry.recorded,
|
|
164
|
+
currentHash: currentSourceHash
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Update the hash for a translation (after translator reviews changes)
|
|
170
|
+
*
|
|
171
|
+
* @param {string} localeDir - Path to locale's freeform directory
|
|
172
|
+
* @param {string} relativePath - Path relative to locale dir
|
|
173
|
+
* @param {string} newHash - New hash to record
|
|
174
|
+
* @returns {Promise<void>}
|
|
175
|
+
*/
|
|
176
|
+
export async function updateHash(localeDir, relativePath, newHash) {
|
|
177
|
+
return recordHash(localeDir, relativePath, newHash)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Remove entries from the manifest
|
|
182
|
+
*
|
|
183
|
+
* Used when translations are deleted.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} localeDir - Path to locale's freeform directory
|
|
186
|
+
* @param {string[]} paths - Paths to remove
|
|
187
|
+
* @returns {Promise<number>} Number of entries removed
|
|
188
|
+
*/
|
|
189
|
+
export async function removeManifestEntries(localeDir, paths) {
|
|
190
|
+
const manifest = await loadManifest(localeDir)
|
|
191
|
+
let removed = 0
|
|
192
|
+
|
|
193
|
+
for (const path of paths) {
|
|
194
|
+
if (manifest[path]) {
|
|
195
|
+
delete manifest[path]
|
|
196
|
+
removed++
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (removed > 0) {
|
|
201
|
+
await saveManifest(localeDir, manifest)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return removed
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Rename entries in the manifest
|
|
209
|
+
*
|
|
210
|
+
* Used when translations are moved/renamed.
|
|
211
|
+
*
|
|
212
|
+
* @param {string} localeDir - Path to locale's freeform directory
|
|
213
|
+
* @param {string[]} oldPaths - Old paths
|
|
214
|
+
* @param {string[]} newPaths - New paths (same order as oldPaths)
|
|
215
|
+
* @returns {Promise<number>} Number of entries renamed
|
|
216
|
+
*/
|
|
217
|
+
export async function renameManifestEntries(localeDir, oldPaths, newPaths) {
|
|
218
|
+
if (oldPaths.length !== newPaths.length) {
|
|
219
|
+
throw new Error('oldPaths and newPaths must have the same length')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const manifest = await loadManifest(localeDir)
|
|
223
|
+
let renamed = 0
|
|
224
|
+
|
|
225
|
+
for (let i = 0; i < oldPaths.length; i++) {
|
|
226
|
+
const oldPath = oldPaths[i]
|
|
227
|
+
const newPath = newPaths[i]
|
|
228
|
+
|
|
229
|
+
if (manifest[oldPath]) {
|
|
230
|
+
manifest[newPath] = manifest[oldPath]
|
|
231
|
+
delete manifest[oldPath]
|
|
232
|
+
renamed++
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (renamed > 0) {
|
|
237
|
+
await saveManifest(localeDir, manifest)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return renamed
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get all stale translations for a locale
|
|
245
|
+
*
|
|
246
|
+
* Compares manifest hashes against current source content.
|
|
247
|
+
*
|
|
248
|
+
* @param {string} localeDir - Path to locale's freeform directory
|
|
249
|
+
* @param {Object} sourceHashes - Map of relativePath → currentHash
|
|
250
|
+
* @returns {Promise<Object[]>} Array of { path, recordedHash, recordedDate, currentHash }
|
|
251
|
+
*/
|
|
252
|
+
export async function getStaleTranslations(localeDir, sourceHashes) {
|
|
253
|
+
const manifest = await loadManifest(localeDir)
|
|
254
|
+
const stale = []
|
|
255
|
+
|
|
256
|
+
for (const [path, entry] of Object.entries(manifest)) {
|
|
257
|
+
const currentHash = sourceHashes[path]
|
|
258
|
+
|
|
259
|
+
// If we have a current hash and it differs, it's stale
|
|
260
|
+
if (currentHash && currentHash !== entry.hash) {
|
|
261
|
+
stale.push({
|
|
262
|
+
path,
|
|
263
|
+
recordedHash: entry.hash,
|
|
264
|
+
recordedDate: entry.recorded,
|
|
265
|
+
currentHash
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return stale
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get orphaned translations (translations without source)
|
|
275
|
+
*
|
|
276
|
+
* @param {string} localeDir - Path to locale's freeform directory
|
|
277
|
+
* @param {Set<string>} validPaths - Set of valid translation paths that have source content
|
|
278
|
+
* @returns {Promise<Object[]>} Array of { path, recordedHash, recordedDate }
|
|
279
|
+
*/
|
|
280
|
+
export async function getOrphanedTranslations(localeDir, validPaths) {
|
|
281
|
+
const manifest = await loadManifest(localeDir)
|
|
282
|
+
const orphaned = []
|
|
283
|
+
|
|
284
|
+
for (const [path, entry] of Object.entries(manifest)) {
|
|
285
|
+
if (!validPaths.has(path)) {
|
|
286
|
+
orphaned.push({
|
|
287
|
+
path,
|
|
288
|
+
recordedHash: entry.hash,
|
|
289
|
+
recordedDate: entry.recorded
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return orphaned
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get unregistered translations (translations without manifest entry)
|
|
299
|
+
*
|
|
300
|
+
* @param {string} localeDir - Path to locale's freeform directory
|
|
301
|
+
* @param {string[]} translationPaths - All translation file paths found
|
|
302
|
+
* @returns {Promise<string[]>} Paths that exist as files but not in manifest
|
|
303
|
+
*/
|
|
304
|
+
export async function getUnregisteredTranslations(localeDir, translationPaths) {
|
|
305
|
+
const manifest = await loadManifest(localeDir)
|
|
306
|
+
return translationPaths.filter(path => !manifest[path])
|
|
307
|
+
}
|