@lingo.dev/compiler 0.3.6 → 0.3.8
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/README.md +55 -2
- package/build/metadata/manager.cjs +76 -107
- package/build/metadata/manager.mjs +76 -104
- package/build/metadata/manager.mjs.map +1 -1
- package/build/plugin/build-translator.cjs +6 -6
- package/build/plugin/build-translator.mjs +6 -6
- package/build/plugin/build-translator.mjs.map +1 -1
- package/build/plugin/next-compiler-loader.cjs +1 -2
- package/build/plugin/next-compiler-loader.mjs +2 -3
- package/build/plugin/next-compiler-loader.mjs.map +1 -1
- package/build/plugin/next.cjs +1 -3
- package/build/plugin/next.mjs +1 -3
- package/build/plugin/next.mjs.map +1 -1
- package/build/plugin/unplugin.cjs +1 -2
- package/build/plugin/unplugin.mjs +2 -3
- package/build/plugin/unplugin.mjs.map +1 -1
- package/build/react/server/ServerLingoProvider.d.cts +2 -2
- package/build/react/server/ServerLingoProvider.d.mts +2 -2
- package/build/react/shared/LingoProvider.d.cts +2 -2
- package/build/react/shared/LingoProvider.d.mts +2 -2
- package/build/react/shared/LocaleSwitcher.d.cts +2 -2
- package/build/react/shared/LocaleSwitcher.d.mts +2 -2
- package/build/translation-server/translation-server.cjs +16 -4
- package/build/translation-server/translation-server.mjs +17 -5
- package/build/translation-server/translation-server.mjs.map +1 -1
- package/build/translators/pluralization/service.cjs +3 -3
- package/build/translators/pluralization/service.mjs +3 -3
- package/build/translators/pluralization/service.mjs.map +1 -1
- package/build/translators/translation-service.cjs +10 -12
- package/build/translators/translation-service.mjs +10 -12
- package/build/translators/translation-service.mjs.map +1 -1
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -330,6 +330,58 @@ LINGO_BUILD_MODE=cache-only npm run build
|
|
|
330
330
|
2. **CI**: Generate real translations with `buildMode: "translate"` and real API keys
|
|
331
331
|
3. **Production Build**: Use `buildMode: "cache-only"` (no API keys needed)
|
|
332
332
|
|
|
333
|
+
## React Client API
|
|
334
|
+
|
|
335
|
+
The compiler provides hooks and components for managing locale in your React components.
|
|
336
|
+
|
|
337
|
+
### `useLingoContext()`
|
|
338
|
+
|
|
339
|
+
Access the translation context to get the current locale and change it.
|
|
340
|
+
|
|
341
|
+
**Returns:**
|
|
342
|
+
- `locale` (string): Current locale code
|
|
343
|
+
- `setLocale` (function): Change the locale
|
|
344
|
+
- `translations` (object): Translation dictionary
|
|
345
|
+
- `isLoading` (boolean): Whether translations are loading
|
|
346
|
+
|
|
347
|
+
```tsx
|
|
348
|
+
"use client";
|
|
349
|
+
import { useLingoContext } from "@lingo.dev/compiler/react";
|
|
350
|
+
|
|
351
|
+
export function LanguageSwitcher() {
|
|
352
|
+
const { locale, setLocale } = useLingoContext();
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<select value={locale} onChange={(e) => setLocale(e.target.value)}>
|
|
356
|
+
<option value="en">English</option>
|
|
357
|
+
<option value="es">Español</option>
|
|
358
|
+
<option value="de">Deutsch</option>
|
|
359
|
+
</select>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### `LocaleSwitcher` Component
|
|
365
|
+
|
|
366
|
+
A pre-built dropdown component for switching locales (no hooks needed):
|
|
367
|
+
|
|
368
|
+
```tsx
|
|
369
|
+
"use client";
|
|
370
|
+
import { LocaleSwitcher } from "@lingo.dev/compiler/react";
|
|
371
|
+
|
|
372
|
+
export function Header() {
|
|
373
|
+
return (
|
|
374
|
+
<LocaleSwitcher
|
|
375
|
+
locales={[
|
|
376
|
+
{ code: "en", label: "English" },
|
|
377
|
+
{ code: "es", label: "Español" },
|
|
378
|
+
{ code: "de", label: "Deutsch" },
|
|
379
|
+
]}
|
|
380
|
+
/>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
333
385
|
## Custom Locale Resolvers
|
|
334
386
|
|
|
335
387
|
Customize how locales are detected and persisted by providing custom resolver files:
|
|
@@ -395,9 +447,10 @@ The compiler is organized into several key modules:
|
|
|
395
447
|
|
|
396
448
|
#### `src/metadata/` - Translation metadata management
|
|
397
449
|
|
|
398
|
-
- **`manager.ts`** - CRUD operations for
|
|
399
|
-
-
|
|
450
|
+
- **`manager.ts`** - CRUD operations for LMDB metadata database
|
|
451
|
+
- Uses LMDB for high-performance key-value storage with built-in concurrency
|
|
400
452
|
- Manages translation entries with hash-based identifiers
|
|
453
|
+
- Stores metadata in `.lingo/metadata-dev/` (development) or `.lingo/metadata-build/` (production)
|
|
401
454
|
|
|
402
455
|
#### `src/translators/` - Translation provider abstraction
|
|
403
456
|
|
|
@@ -1,131 +1,100 @@
|
|
|
1
1
|
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
2
|
const require_logger = require('../utils/logger.cjs');
|
|
3
|
-
const require_timeout = require('../utils/timeout.cjs');
|
|
4
3
|
const require_path_helpers = require('../utils/path-helpers.cjs');
|
|
5
|
-
let fs_promises = require("fs/promises");
|
|
6
|
-
fs_promises = require_rolldown_runtime.__toESM(fs_promises);
|
|
7
4
|
let path = require("path");
|
|
8
5
|
path = require_rolldown_runtime.__toESM(path);
|
|
9
6
|
let fs = require("fs");
|
|
10
7
|
fs = require_rolldown_runtime.__toESM(fs);
|
|
11
|
-
let
|
|
12
|
-
proper_lockfile = require_rolldown_runtime.__toESM(proper_lockfile);
|
|
8
|
+
let lmdb = require("lmdb");
|
|
13
9
|
|
|
14
10
|
//#region src/metadata/manager.ts
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
function loadMetadata(path$2) {
|
|
25
|
-
return new MetadataManager(path$2).loadMetadata();
|
|
26
|
-
}
|
|
27
|
-
function cleanupExistingMetadata(metadataFilePath) {
|
|
28
|
-
require_logger.logger.debug(`Attempting to cleanup metadata file: ${metadataFilePath}`);
|
|
11
|
+
const METADATA_DIR_DEV = "metadata-dev";
|
|
12
|
+
const METADATA_DIR_BUILD = "metadata-build";
|
|
13
|
+
/**
|
|
14
|
+
* Opens an LMDB connection for a single operation.
|
|
15
|
+
*
|
|
16
|
+
* lmdb-js deduplicates open() calls to the same path (ref-counted at C++ level),
|
|
17
|
+
* so this is cheap. Each open() also clears stale readers from terminated workers.
|
|
18
|
+
*/
|
|
19
|
+
function openDatabaseConnection(dbPath, noSync) {
|
|
29
20
|
try {
|
|
30
|
-
fs.default.
|
|
31
|
-
|
|
21
|
+
fs.default.mkdirSync(dbPath, { recursive: true });
|
|
22
|
+
return (0, lmdb.open)({
|
|
23
|
+
path: dbPath,
|
|
24
|
+
compression: true,
|
|
25
|
+
noSync
|
|
26
|
+
});
|
|
32
27
|
} catch (error) {
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
29
|
+
throw new Error(`Failed to open LMDB at ${dbPath}: ${message}`);
|
|
35
30
|
}
|
|
36
31
|
}
|
|
37
32
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* @param config - Config with sourceRoot, lingoDir, and environment
|
|
41
|
-
* @returns Absolute path to metadata file
|
|
33
|
+
* Closes the LMDB connection. Also prevents EBUSY/EPERM on Windows during
|
|
34
|
+
* directory cleanup.
|
|
42
35
|
*/
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
constructor(filePath) {
|
|
49
|
-
this.filePath = filePath;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Load metadata from disk
|
|
53
|
-
* Creates empty metadata if file doesn't exist
|
|
54
|
-
* Times out after 15 seconds to prevent indefinite hangs
|
|
55
|
-
*/
|
|
56
|
-
async loadMetadata() {
|
|
57
|
-
try {
|
|
58
|
-
const content = await require_timeout.withTimeout(fs_promises.default.readFile(this.filePath, "utf-8"), require_timeout.DEFAULT_TIMEOUTS.METADATA, "Load metadata");
|
|
59
|
-
return JSON.parse(content);
|
|
60
|
-
} catch (error) {
|
|
61
|
-
if (error.code === "ENOENT") return createEmptyMetadata();
|
|
62
|
-
throw error;
|
|
63
|
-
}
|
|
36
|
+
async function closeDatabaseConnection(db, dbPath) {
|
|
37
|
+
try {
|
|
38
|
+
await db.close();
|
|
39
|
+
} catch (e) {
|
|
40
|
+
require_logger.logger.debug(`Error closing database at ${dbPath}: ${e}`);
|
|
64
41
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const base = path.default.basename(this.filePath);
|
|
77
|
-
const tmpPath = path.default.join(dir, `.${base}.tmp-${process.pid}-${Date.now()}`);
|
|
78
|
-
const json = JSON.stringify(metadata, null, 2);
|
|
79
|
-
await require_timeout.withTimeout(fs_promises.default.writeFile(tmpPath, json, "utf-8"), require_timeout.DEFAULT_TIMEOUTS.METADATA, "Save metadata (tmp write)");
|
|
80
|
-
try {
|
|
81
|
-
await require_timeout.withTimeout(fs_promises.default.rename(tmpPath, this.filePath), require_timeout.DEFAULT_TIMEOUTS.METADATA, "Save metadata (atomic rename)");
|
|
82
|
-
} catch (error) {
|
|
83
|
-
if (error && typeof error === "object" && "code" in error && error.code === "EPERM") {
|
|
84
|
-
await require_timeout.withTimeout(fs_promises.default.writeFile(this.filePath, json, "utf-8"), require_timeout.DEFAULT_TIMEOUTS.METADATA, "Save metadata (EPERM fallback direct write)");
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
throw error;
|
|
88
|
-
} finally {
|
|
89
|
-
await fs_promises.default.unlink(tmpPath).catch(() => {});
|
|
90
|
-
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Opens a database connection, runs the callback, and ensures the connection
|
|
45
|
+
* is closed afterwards.
|
|
46
|
+
*/
|
|
47
|
+
async function runWithDbConnection(dbPath, noSync, fn) {
|
|
48
|
+
const db = openDatabaseConnection(dbPath, noSync);
|
|
49
|
+
try {
|
|
50
|
+
return fn(db);
|
|
51
|
+
} finally {
|
|
52
|
+
await closeDatabaseConnection(db, dbPath);
|
|
91
53
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
retries: {
|
|
109
|
-
retries: 20,
|
|
110
|
-
minTimeout: 50,
|
|
111
|
-
maxTimeout: 2e3
|
|
112
|
-
},
|
|
113
|
-
stale: 5e3
|
|
54
|
+
}
|
|
55
|
+
function readEntriesFromDb(db) {
|
|
56
|
+
const entries = {};
|
|
57
|
+
for (const { key, value } of db.getRange()) entries[key] = value;
|
|
58
|
+
return entries;
|
|
59
|
+
}
|
|
60
|
+
async function loadMetadata(dbPath, noSync = false) {
|
|
61
|
+
return runWithDbConnection(dbPath, noSync, readEntriesFromDb);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Persists translation entries to LMDB in a single atomic transaction.
|
|
65
|
+
*/
|
|
66
|
+
async function saveMetadata(dbPath, entries, noSync = false) {
|
|
67
|
+
return runWithDbConnection(dbPath, noSync, (db) => {
|
|
68
|
+
db.transactionSync(() => {
|
|
69
|
+
for (const entry of entries) db.putSync(entry.hash, entry);
|
|
114
70
|
});
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function cleanupExistingMetadata(metadataDbPath) {
|
|
74
|
+
require_logger.logger.debug(`Cleaning up metadata database: ${metadataDbPath}`);
|
|
75
|
+
try {
|
|
76
|
+
fs.default.rmSync(metadataDbPath, {
|
|
77
|
+
recursive: true,
|
|
78
|
+
force: true
|
|
79
|
+
});
|
|
80
|
+
require_logger.logger.info(`🧹 Cleaned up metadata database: ${metadataDbPath}`);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
83
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
84
|
+
if (code === "ENOENT") {
|
|
85
|
+
require_logger.logger.debug(`Metadata database already deleted or doesn't exist: ${metadataDbPath}`);
|
|
86
|
+
return;
|
|
122
87
|
}
|
|
88
|
+
require_logger.logger.warn(`Failed to cleanup metadata database: ${message}`);
|
|
123
89
|
}
|
|
124
|
-
}
|
|
90
|
+
}
|
|
91
|
+
function getMetadataPath(config) {
|
|
92
|
+
const dirname = config.environment === "development" ? METADATA_DIR_DEV : METADATA_DIR_BUILD;
|
|
93
|
+
return path.default.join(require_path_helpers.getLingoDir(config), dirname);
|
|
94
|
+
}
|
|
125
95
|
|
|
126
96
|
//#endregion
|
|
127
|
-
exports.MetadataManager = MetadataManager;
|
|
128
97
|
exports.cleanupExistingMetadata = cleanupExistingMetadata;
|
|
129
|
-
exports.createEmptyMetadata = createEmptyMetadata;
|
|
130
98
|
exports.getMetadataPath = getMetadataPath;
|
|
131
|
-
exports.loadMetadata = loadMetadata;
|
|
99
|
+
exports.loadMetadata = loadMetadata;
|
|
100
|
+
exports.saveMetadata = saveMetadata;
|
|
@@ -1,123 +1,95 @@
|
|
|
1
1
|
import { logger } from "../utils/logger.mjs";
|
|
2
|
-
import { DEFAULT_TIMEOUTS, withTimeout } from "../utils/timeout.mjs";
|
|
3
2
|
import { getLingoDir } from "../utils/path-helpers.mjs";
|
|
4
|
-
import fs from "fs/promises";
|
|
5
3
|
import path from "path";
|
|
6
|
-
import fs
|
|
7
|
-
import
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import { open } from "lmdb";
|
|
8
6
|
|
|
9
7
|
//#region src/metadata/manager.ts
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
function loadMetadata(path$1) {
|
|
20
|
-
return new MetadataManager(path$1).loadMetadata();
|
|
21
|
-
}
|
|
22
|
-
function cleanupExistingMetadata(metadataFilePath) {
|
|
23
|
-
logger.debug(`Attempting to cleanup metadata file: ${metadataFilePath}`);
|
|
8
|
+
const METADATA_DIR_DEV = "metadata-dev";
|
|
9
|
+
const METADATA_DIR_BUILD = "metadata-build";
|
|
10
|
+
/**
|
|
11
|
+
* Opens an LMDB connection for a single operation.
|
|
12
|
+
*
|
|
13
|
+
* lmdb-js deduplicates open() calls to the same path (ref-counted at C++ level),
|
|
14
|
+
* so this is cheap. Each open() also clears stale readers from terminated workers.
|
|
15
|
+
*/
|
|
16
|
+
function openDatabaseConnection(dbPath, noSync) {
|
|
24
17
|
try {
|
|
25
|
-
fs
|
|
26
|
-
|
|
18
|
+
fs.mkdirSync(dbPath, { recursive: true });
|
|
19
|
+
return open({
|
|
20
|
+
path: dbPath,
|
|
21
|
+
compression: true,
|
|
22
|
+
noSync
|
|
23
|
+
});
|
|
27
24
|
} catch (error) {
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
throw new Error(`Failed to open LMDB at ${dbPath}: ${message}`);
|
|
30
27
|
}
|
|
31
28
|
}
|
|
32
29
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* @param config - Config with sourceRoot, lingoDir, and environment
|
|
36
|
-
* @returns Absolute path to metadata file
|
|
30
|
+
* Closes the LMDB connection. Also prevents EBUSY/EPERM on Windows during
|
|
31
|
+
* directory cleanup.
|
|
37
32
|
*/
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
constructor(filePath) {
|
|
44
|
-
this.filePath = filePath;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Load metadata from disk
|
|
48
|
-
* Creates empty metadata if file doesn't exist
|
|
49
|
-
* Times out after 15 seconds to prevent indefinite hangs
|
|
50
|
-
*/
|
|
51
|
-
async loadMetadata() {
|
|
52
|
-
try {
|
|
53
|
-
const content = await withTimeout(fs.readFile(this.filePath, "utf-8"), DEFAULT_TIMEOUTS.METADATA, "Load metadata");
|
|
54
|
-
return JSON.parse(content);
|
|
55
|
-
} catch (error) {
|
|
56
|
-
if (error.code === "ENOENT") return createEmptyMetadata();
|
|
57
|
-
throw error;
|
|
58
|
-
}
|
|
33
|
+
async function closeDatabaseConnection(db, dbPath) {
|
|
34
|
+
try {
|
|
35
|
+
await db.close();
|
|
36
|
+
} catch (e) {
|
|
37
|
+
logger.debug(`Error closing database at ${dbPath}: ${e}`);
|
|
59
38
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const base = path.basename(this.filePath);
|
|
72
|
-
const tmpPath = path.join(dir, `.${base}.tmp-${process.pid}-${Date.now()}`);
|
|
73
|
-
const json = JSON.stringify(metadata, null, 2);
|
|
74
|
-
await withTimeout(fs.writeFile(tmpPath, json, "utf-8"), DEFAULT_TIMEOUTS.METADATA, "Save metadata (tmp write)");
|
|
75
|
-
try {
|
|
76
|
-
await withTimeout(fs.rename(tmpPath, this.filePath), DEFAULT_TIMEOUTS.METADATA, "Save metadata (atomic rename)");
|
|
77
|
-
} catch (error) {
|
|
78
|
-
if (error && typeof error === "object" && "code" in error && error.code === "EPERM") {
|
|
79
|
-
await withTimeout(fs.writeFile(this.filePath, json, "utf-8"), DEFAULT_TIMEOUTS.METADATA, "Save metadata (EPERM fallback direct write)");
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
throw error;
|
|
83
|
-
} finally {
|
|
84
|
-
await fs.unlink(tmpPath).catch(() => {});
|
|
85
|
-
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Opens a database connection, runs the callback, and ensures the connection
|
|
42
|
+
* is closed afterwards.
|
|
43
|
+
*/
|
|
44
|
+
async function runWithDbConnection(dbPath, noSync, fn) {
|
|
45
|
+
const db = openDatabaseConnection(dbPath, noSync);
|
|
46
|
+
try {
|
|
47
|
+
return fn(db);
|
|
48
|
+
} finally {
|
|
49
|
+
await closeDatabaseConnection(db, dbPath);
|
|
86
50
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
retries: {
|
|
104
|
-
retries: 20,
|
|
105
|
-
minTimeout: 50,
|
|
106
|
-
maxTimeout: 2e3
|
|
107
|
-
},
|
|
108
|
-
stale: 5e3
|
|
51
|
+
}
|
|
52
|
+
function readEntriesFromDb(db) {
|
|
53
|
+
const entries = {};
|
|
54
|
+
for (const { key, value } of db.getRange()) entries[key] = value;
|
|
55
|
+
return entries;
|
|
56
|
+
}
|
|
57
|
+
async function loadMetadata(dbPath, noSync = false) {
|
|
58
|
+
return runWithDbConnection(dbPath, noSync, readEntriesFromDb);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Persists translation entries to LMDB in a single atomic transaction.
|
|
62
|
+
*/
|
|
63
|
+
async function saveMetadata(dbPath, entries, noSync = false) {
|
|
64
|
+
return runWithDbConnection(dbPath, noSync, (db) => {
|
|
65
|
+
db.transactionSync(() => {
|
|
66
|
+
for (const entry of entries) db.putSync(entry.hash, entry);
|
|
109
67
|
});
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function cleanupExistingMetadata(metadataDbPath) {
|
|
71
|
+
logger.debug(`Cleaning up metadata database: ${metadataDbPath}`);
|
|
72
|
+
try {
|
|
73
|
+
fs.rmSync(metadataDbPath, {
|
|
74
|
+
recursive: true,
|
|
75
|
+
force: true
|
|
76
|
+
});
|
|
77
|
+
logger.info(`🧹 Cleaned up metadata database: ${metadataDbPath}`);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
80
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
+
if (code === "ENOENT") {
|
|
82
|
+
logger.debug(`Metadata database already deleted or doesn't exist: ${metadataDbPath}`);
|
|
83
|
+
return;
|
|
117
84
|
}
|
|
85
|
+
logger.warn(`Failed to cleanup metadata database: ${message}`);
|
|
118
86
|
}
|
|
119
|
-
}
|
|
87
|
+
}
|
|
88
|
+
function getMetadataPath(config) {
|
|
89
|
+
const dirname = config.environment === "development" ? METADATA_DIR_DEV : METADATA_DIR_BUILD;
|
|
90
|
+
return path.join(getLingoDir(config), dirname);
|
|
91
|
+
}
|
|
120
92
|
|
|
121
93
|
//#endregion
|
|
122
|
-
export {
|
|
94
|
+
export { cleanupExistingMetadata, getMetadataPath, loadMetadata, saveMetadata };
|
|
123
95
|
//# sourceMappingURL=manager.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.mjs","names":["
|
|
1
|
+
{"version":3,"file":"manager.mjs","names":["entries: MetadataSchema"],"sources":["../../src/metadata/manager.ts"],"sourcesContent":["import fs from \"fs\";\nimport path from \"path\";\nimport { open, type RootDatabase } from \"lmdb\";\nimport type { MetadataSchema, PathConfig, TranslationEntry } from \"../types\";\nimport { getLingoDir } from \"../utils/path-helpers\";\nimport { logger } from \"../utils/logger\";\n\nconst METADATA_DIR_DEV = \"metadata-dev\";\nconst METADATA_DIR_BUILD = \"metadata-build\";\n\n/**\n * Opens an LMDB connection for a single operation.\n *\n * lmdb-js deduplicates open() calls to the same path (ref-counted at C++ level),\n * so this is cheap. Each open() also clears stale readers from terminated workers.\n */\nfunction openDatabaseConnection(dbPath: string, noSync: boolean): RootDatabase {\n try {\n fs.mkdirSync(dbPath, { recursive: true });\n return open({\n path: dbPath,\n compression: true,\n noSync,\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`Failed to open LMDB at ${dbPath}: ${message}`);\n }\n}\n\n/**\n * Closes the LMDB connection. Also prevents EBUSY/EPERM on Windows during\n * directory cleanup.\n */\nasync function closeDatabaseConnection(\n db: RootDatabase,\n dbPath: string,\n): Promise<void> {\n try {\n await db.close();\n } catch (e) {\n logger.debug(`Error closing database at ${dbPath}: ${e}`);\n }\n}\n\n/**\n * Opens a database connection, runs the callback, and ensures the connection\n * is closed afterwards.\n */\nasync function runWithDbConnection<T>(\n dbPath: string,\n noSync: boolean,\n fn: (db: RootDatabase) => T,\n): Promise<T> {\n const db = openDatabaseConnection(dbPath, noSync);\n try {\n return fn(db);\n } finally {\n await closeDatabaseConnection(db, dbPath);\n }\n}\n\nfunction readEntriesFromDb(db: RootDatabase): MetadataSchema {\n const entries: MetadataSchema = {};\n\n for (const { key, value } of db.getRange()) {\n entries[key as string] = value as TranslationEntry;\n }\n\n return entries;\n}\n\nexport async function loadMetadata(\n dbPath: string,\n noSync = false,\n): Promise<MetadataSchema> {\n return runWithDbConnection(dbPath, noSync, readEntriesFromDb);\n}\n\n/**\n * Persists translation entries to LMDB in a single atomic transaction.\n */\nexport async function saveMetadata(\n dbPath: string,\n entries: TranslationEntry[],\n noSync = false,\n): Promise<void> {\n return runWithDbConnection(dbPath, noSync, (db) => {\n db.transactionSync(() => {\n for (const entry of entries) {\n db.putSync(entry.hash, entry);\n }\n });\n });\n}\n\nexport function cleanupExistingMetadata(metadataDbPath: string): void {\n logger.debug(`Cleaning up metadata database: ${metadataDbPath}`);\n\n try {\n fs.rmSync(metadataDbPath, { recursive: true, force: true });\n logger.info(`🧹 Cleaned up metadata database: ${metadataDbPath}`);\n } catch (error) {\n const code =\n error instanceof Error && \"code\" in error\n ? (error as NodeJS.ErrnoException).code\n : undefined;\n const message = error instanceof Error ? error.message : String(error);\n\n if (code === \"ENOENT\") {\n logger.debug(\n `Metadata database already deleted or doesn't exist: ${metadataDbPath}`,\n );\n return;\n }\n\n logger.warn(`Failed to cleanup metadata database: ${message}`);\n }\n}\n\nexport function getMetadataPath(config: PathConfig): string {\n const dirname =\n config.environment === \"development\"\n ? METADATA_DIR_DEV\n : METADATA_DIR_BUILD;\n return path.join(getLingoDir(config), dirname);\n}\n"],"mappings":";;;;;;;AAOA,MAAM,mBAAmB;AACzB,MAAM,qBAAqB;;;;;;;AAQ3B,SAAS,uBAAuB,QAAgB,QAA+B;AAC7E,KAAI;AACF,KAAG,UAAU,QAAQ,EAAE,WAAW,MAAM,CAAC;AACzC,SAAO,KAAK;GACV,MAAM;GACN,aAAa;GACb;GACD,CAAC;UACK,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AACtE,QAAM,IAAI,MAAM,0BAA0B,OAAO,IAAI,UAAU;;;;;;;AAQnE,eAAe,wBACb,IACA,QACe;AACf,KAAI;AACF,QAAM,GAAG,OAAO;UACT,GAAG;AACV,SAAO,MAAM,6BAA6B,OAAO,IAAI,IAAI;;;;;;;AAQ7D,eAAe,oBACb,QACA,QACA,IACY;CACZ,MAAM,KAAK,uBAAuB,QAAQ,OAAO;AACjD,KAAI;AACF,SAAO,GAAG,GAAG;WACL;AACR,QAAM,wBAAwB,IAAI,OAAO;;;AAI7C,SAAS,kBAAkB,IAAkC;CAC3D,MAAMA,UAA0B,EAAE;AAElC,MAAK,MAAM,EAAE,KAAK,WAAW,GAAG,UAAU,CACxC,SAAQ,OAAiB;AAG3B,QAAO;;AAGT,eAAsB,aACpB,QACA,SAAS,OACgB;AACzB,QAAO,oBAAoB,QAAQ,QAAQ,kBAAkB;;;;;AAM/D,eAAsB,aACpB,QACA,SACA,SAAS,OACM;AACf,QAAO,oBAAoB,QAAQ,SAAS,OAAO;AACjD,KAAG,sBAAsB;AACvB,QAAK,MAAM,SAAS,QAClB,IAAG,QAAQ,MAAM,MAAM,MAAM;IAE/B;GACF;;AAGJ,SAAgB,wBAAwB,gBAA8B;AACpE,QAAO,MAAM,kCAAkC,iBAAiB;AAEhE,KAAI;AACF,KAAG,OAAO,gBAAgB;GAAE,WAAW;GAAM,OAAO;GAAM,CAAC;AAC3D,SAAO,KAAK,oCAAoC,iBAAiB;UAC1D,OAAO;EACd,MAAM,OACJ,iBAAiB,SAAS,UAAU,QAC/B,MAAgC,OACjC;EACN,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AAEtE,MAAI,SAAS,UAAU;AACrB,UAAO,MACL,uDAAuD,iBACxD;AACD;;AAGF,SAAO,KAAK,wCAAwC,UAAU;;;AAIlE,SAAgB,gBAAgB,QAA4B;CAC1D,MAAM,UACJ,OAAO,gBAAgB,gBACnB,mBACA;AACN,QAAO,KAAK,KAAK,YAAY,OAAO,EAAE,QAAQ"}
|
|
@@ -28,15 +28,15 @@ async function processBuildTranslations(options) {
|
|
|
28
28
|
const { config, publicOutputPath, metadataFilePath } = options;
|
|
29
29
|
const buildMode = process.env.LINGO_BUILD_MODE || config.buildMode;
|
|
30
30
|
require_logger.logger.info(`🌍 Build mode: ${buildMode}`);
|
|
31
|
-
const metadata = await require_manager.loadMetadata(metadataFilePath);
|
|
32
|
-
if (!metadata || Object.keys(metadata
|
|
31
|
+
const metadata = await require_manager.loadMetadata(metadataFilePath, true);
|
|
32
|
+
if (!metadata || Object.keys(metadata).length === 0) {
|
|
33
33
|
require_logger.logger.info("No translations to process (metadata is empty)");
|
|
34
34
|
return {
|
|
35
35
|
success: true,
|
|
36
36
|
stats: {}
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
const totalEntries = Object.keys(metadata
|
|
39
|
+
const totalEntries = Object.keys(metadata).length;
|
|
40
40
|
require_logger.logger.info(`📊 Found ${totalEntries} translatable entries`);
|
|
41
41
|
const cache = require_cache_factory.createCache(config);
|
|
42
42
|
if (buildMode === "cache-only") {
|
|
@@ -107,7 +107,7 @@ async function processBuildTranslations(options) {
|
|
|
107
107
|
* @throws Error if cache is incomplete or missing
|
|
108
108
|
*/
|
|
109
109
|
async function validateCache(config, metadata, cache) {
|
|
110
|
-
const allHashes = Object.keys(metadata
|
|
110
|
+
const allHashes = Object.keys(metadata);
|
|
111
111
|
const missingLocales = [];
|
|
112
112
|
const incompleteLocales = [];
|
|
113
113
|
const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
|
|
@@ -138,7 +138,7 @@ async function validateCache(config, metadata, cache) {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
function buildCacheStats(config, metadata) {
|
|
141
|
-
const totalEntries = Object.keys(metadata
|
|
141
|
+
const totalEntries = Object.keys(metadata).length;
|
|
142
142
|
const stats = {};
|
|
143
143
|
const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
|
|
144
144
|
for (const locale of allLocales) stats[locale] = {
|
|
@@ -151,7 +151,7 @@ function buildCacheStats(config, metadata) {
|
|
|
151
151
|
async function copyStaticFiles(config, publicOutputPath, metadata, cache) {
|
|
152
152
|
require_logger.logger.info(`📦 Generating static translation files in ${publicOutputPath}`);
|
|
153
153
|
await fs_promises.default.mkdir(publicOutputPath, { recursive: true });
|
|
154
|
-
const usedHashes = new Set(Object.keys(metadata
|
|
154
|
+
const usedHashes = new Set(Object.keys(metadata));
|
|
155
155
|
require_logger.logger.info(`📊 Filtering translations to ${usedHashes.size} used hash(es)`);
|
|
156
156
|
const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
|
|
157
157
|
for (const locale of allLocales) {
|
|
@@ -25,15 +25,15 @@ async function processBuildTranslations(options) {
|
|
|
25
25
|
const { config, publicOutputPath, metadataFilePath } = options;
|
|
26
26
|
const buildMode = process.env.LINGO_BUILD_MODE || config.buildMode;
|
|
27
27
|
logger.info(`🌍 Build mode: ${buildMode}`);
|
|
28
|
-
const metadata = await loadMetadata(metadataFilePath);
|
|
29
|
-
if (!metadata || Object.keys(metadata
|
|
28
|
+
const metadata = await loadMetadata(metadataFilePath, true);
|
|
29
|
+
if (!metadata || Object.keys(metadata).length === 0) {
|
|
30
30
|
logger.info("No translations to process (metadata is empty)");
|
|
31
31
|
return {
|
|
32
32
|
success: true,
|
|
33
33
|
stats: {}
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
-
const totalEntries = Object.keys(metadata
|
|
36
|
+
const totalEntries = Object.keys(metadata).length;
|
|
37
37
|
logger.info(`📊 Found ${totalEntries} translatable entries`);
|
|
38
38
|
const cache = createCache(config);
|
|
39
39
|
if (buildMode === "cache-only") {
|
|
@@ -104,7 +104,7 @@ async function processBuildTranslations(options) {
|
|
|
104
104
|
* @throws Error if cache is incomplete or missing
|
|
105
105
|
*/
|
|
106
106
|
async function validateCache(config, metadata, cache) {
|
|
107
|
-
const allHashes = Object.keys(metadata
|
|
107
|
+
const allHashes = Object.keys(metadata);
|
|
108
108
|
const missingLocales = [];
|
|
109
109
|
const incompleteLocales = [];
|
|
110
110
|
const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
|
|
@@ -135,7 +135,7 @@ async function validateCache(config, metadata, cache) {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
function buildCacheStats(config, metadata) {
|
|
138
|
-
const totalEntries = Object.keys(metadata
|
|
138
|
+
const totalEntries = Object.keys(metadata).length;
|
|
139
139
|
const stats = {};
|
|
140
140
|
const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
|
|
141
141
|
for (const locale of allLocales) stats[locale] = {
|
|
@@ -148,7 +148,7 @@ function buildCacheStats(config, metadata) {
|
|
|
148
148
|
async function copyStaticFiles(config, publicOutputPath, metadata, cache) {
|
|
149
149
|
logger.info(`📦 Generating static translation files in ${publicOutputPath}`);
|
|
150
150
|
await fs.mkdir(publicOutputPath, { recursive: true });
|
|
151
|
-
const usedHashes = new Set(Object.keys(metadata
|
|
151
|
+
const usedHashes = new Set(Object.keys(metadata));
|
|
152
152
|
logger.info(`📊 Filtering translations to ${usedHashes.size} used hash(es)`);
|
|
153
153
|
const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
|
|
154
154
|
for (const locale of allLocales) {
|