i18next-cli 1.62.0 → 1.63.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/README.md +29 -0
- package/dist/cjs/cli.js +3 -2
- package/dist/cjs/init.js +16 -0
- package/dist/cjs/instrumenter/core/instrumenter.js +2 -1
- package/dist/cjs/utils/inlang-scaffold.js +184 -0
- package/dist/esm/cli.js +3 -2
- package/dist/esm/init.js +16 -0
- package/dist/esm/instrumenter/core/instrumenter.js +2 -1
- package/dist/esm/utils/inlang-scaffold.js +182 -0
- package/package.json +1 -1
- package/types/cli.d.ts.map +1 -1
- package/types/init.d.ts +1 -0
- package/types/init.d.ts.map +1 -1
- package/types/instrumenter/core/instrumenter.d.ts +2 -1
- package/types/instrumenter/core/instrumenter.d.ts.map +1 -1
- package/types/utils/inlang-scaffold.d.ts +28 -0
- package/types/utils/inlang-scaffold.d.ts.map +1 -0
package/README.md
CHANGED
|
@@ -118,6 +118,13 @@ npx i18next-cli init
|
|
|
118
118
|
also auto-detects `CI=true` and falls back to printing the URL on headless
|
|
119
119
|
Linux (no `DISPLAY`/`WAYLAND_DISPLAY`), so this flag is rarely needed
|
|
120
120
|
explicitly.
|
|
121
|
+
- `--inlang`: Also scaffold an [inlang](https://inlang.com) project
|
|
122
|
+
(`project.inlang/settings.json`) so inlang tooling — the
|
|
123
|
+
[Sherlock](https://inlang.com/m/r7kp499g/app-inlang-ideExtension) VS Code
|
|
124
|
+
extension, the [Fink](https://fink.inlang.com) web editor for translators,
|
|
125
|
+
and the [Paraglide](https://inlang.com/m/gerre34r/library-inlang-paraglideJs)
|
|
126
|
+
compiler — works directly on your translation files. Skips the
|
|
127
|
+
corresponding wizard question.
|
|
121
128
|
|
|
122
129
|
The wizard asks for the config file type, locales, source-file glob, output
|
|
123
130
|
path, and finally **"Translation backend?"** with three options:
|
|
@@ -131,6 +138,28 @@ path, and finally **"Translation backend?"** with three options:
|
|
|
131
138
|
mode); add it later via a `LOCIZE_API_KEY` environment variable.
|
|
132
139
|
- **Other / skip** — same as "Local files only" for the wizard's purposes.
|
|
133
140
|
|
|
141
|
+
The wizard then offers to **set up inlang tooling** (default: no — or pass
|
|
142
|
+
`--inlang` to skip the question). If accepted, it scaffolds a
|
|
143
|
+
`project.inlang/settings.json` that points the
|
|
144
|
+
[inlang i18next plugin](https://inlang.com/m/3i8bor92/plugin-inlang-i18next)
|
|
145
|
+
at your existing translation files: `baseLocale`/`locales` come from your
|
|
146
|
+
config, and `pathPattern` is derived from `extract.output` (the namespaced
|
|
147
|
+
object form when your layout uses `{{namespace}}`, with namespaces discovered
|
|
148
|
+
from the primary language's files; a plain pattern otherwise). It also adds
|
|
149
|
+
the Sherlock extension to `.vscode/extensions.json` recommendations (merging
|
|
150
|
+
comment-aware, never clobbering existing entries). Your i18next JSON files
|
|
151
|
+
remain the single source of truth — inlang tools read and write them in
|
|
152
|
+
place, so there is no second catalog to drift. An existing
|
|
153
|
+
`project.inlang/settings.json` is never overwritten; re-running `init` is
|
|
154
|
+
safe. Requires JSON resource files. The plugin is pinned to an exact verified
|
|
155
|
+
version (`@inlang/plugin-i18next@6.2.0`) — bump the `modules` URL in
|
|
156
|
+
`settings.json` to pick up newer plugin releases. Only `settings.json` is
|
|
157
|
+
scaffolded by design: `project.inlang/` is the
|
|
158
|
+
[unpacked (git-friendly)](https://inlang.com/docs/unpacked-project) project
|
|
159
|
+
form, and inlang tools generate and manage its remaining files (`.gitignore`,
|
|
160
|
+
`README.md`, `cache/`) on first use — so expect a few new files there after
|
|
161
|
+
opening the project with Sherlock or Paraglide.
|
|
162
|
+
|
|
134
163
|
### `extract`
|
|
135
164
|
Parses source files, extracts keys, and updates your JSON translation files.
|
|
136
165
|
|
package/dist/cjs/cli.js
CHANGED
|
@@ -37,7 +37,7 @@ const program = new commander.Command();
|
|
|
37
37
|
program
|
|
38
38
|
.name('i18next-cli')
|
|
39
39
|
.description('A unified, high-performance i18next CLI.')
|
|
40
|
-
.version('1.
|
|
40
|
+
.version('1.63.0'); // This string is replaced with the actual version at build time by rollup
|
|
41
41
|
// new: global config override option
|
|
42
42
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
43
43
|
program
|
|
@@ -197,7 +197,8 @@ program
|
|
|
197
197
|
.command('init')
|
|
198
198
|
.description('Create a new i18next.config.ts/js file with an interactive setup wizard.')
|
|
199
199
|
.option('--ci', 'Skip the browser launch when a backend (e.g. Locize) is selected. The signup URL is printed instead.')
|
|
200
|
-
.
|
|
200
|
+
.option('--inlang', 'Also scaffold an inlang project (project.inlang/settings.json) so inlang tooling (Sherlock, Fink, Paraglide) works on the translation files. Skips the corresponding wizard question.')
|
|
201
|
+
.action((options) => init.runInit({ ci: !!options.ci, inlang: !!options.inlang }));
|
|
201
202
|
program
|
|
202
203
|
.command('lint')
|
|
203
204
|
.description('Find potential issues like hardcoded strings in your codebase.')
|
package/dist/cjs/init.js
CHANGED
|
@@ -5,6 +5,7 @@ var promises = require('node:fs/promises');
|
|
|
5
5
|
var node_path = require('node:path');
|
|
6
6
|
var heuristicConfig = require('./heuristic-config.js');
|
|
7
7
|
var locizeOnboarding = require('./utils/locize-onboarding.js');
|
|
8
|
+
var inlangScaffold = require('./utils/inlang-scaffold.js');
|
|
8
9
|
|
|
9
10
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
11
|
|
|
@@ -160,6 +161,14 @@ async function runInit(options = {}) {
|
|
|
160
161
|
],
|
|
161
162
|
default: 'local',
|
|
162
163
|
},
|
|
164
|
+
{
|
|
165
|
+
type: 'confirm',
|
|
166
|
+
name: 'inlang',
|
|
167
|
+
message: 'Also set up inlang tooling (Sherlock VS Code extension, Fink editor, Paraglide) on these translation files?',
|
|
168
|
+
default: false,
|
|
169
|
+
// Skip the question when already requested via the --inlang flag.
|
|
170
|
+
when: () => !options.inlang,
|
|
171
|
+
},
|
|
163
172
|
]);
|
|
164
173
|
let locizeConfig;
|
|
165
174
|
if (answers.backend === 'locize') {
|
|
@@ -253,6 +262,13 @@ module.exports = ${toJs(configObject)}`;
|
|
|
253
262
|
const outputPath = node_path.resolve(process.cwd(), fileName);
|
|
254
263
|
await promises.writeFile(outputPath, fileContent.trim());
|
|
255
264
|
console.log(`✅ Configuration file created at: ${outputPath}`);
|
|
265
|
+
if (options.inlang || answers.inlang) {
|
|
266
|
+
await inlangScaffold.scaffoldInlangProject({
|
|
267
|
+
locales: answers.locales,
|
|
268
|
+
primaryLanguage: answers.locales[0],
|
|
269
|
+
output: answers.output,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
256
272
|
if (locizeConfig) {
|
|
257
273
|
console.log('\nNext steps for Locize:');
|
|
258
274
|
console.log(' 1. Push your local translations to Locize:');
|
|
@@ -1470,7 +1470,8 @@ const I18N_INIT_FILE_NAMES = [
|
|
|
1470
1470
|
* Searches the common locations (`src/` and the project root) for an existing
|
|
1471
1471
|
* i18n initialization file.
|
|
1472
1472
|
*
|
|
1473
|
-
* @returns The path of the first init file found (relative to cwd
|
|
1473
|
+
* @returns The path of the first init file found (relative to cwd, native
|
|
1474
|
+
* platform separators), or null.
|
|
1474
1475
|
*/
|
|
1475
1476
|
async function findExistingI18nInitFile() {
|
|
1476
1477
|
const cwd = process.cwd();
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var promises = require('node:fs/promises');
|
|
4
|
+
var node_path = require('node:path');
|
|
5
|
+
var jsoncParser = require('jsonc-parser');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The plugin that teaches inlang tools (Sherlock, Fink, Paraglide) to read and
|
|
9
|
+
* write i18next JSON resource files directly.
|
|
10
|
+
*
|
|
11
|
+
* Pinned to an exact version on purpose: 6.2.0 is the first release with
|
|
12
|
+
* verified round-trip support for plurals, context, `_zero` and ordinal keys,
|
|
13
|
+
* and jsDelivr serves floating range URLs (`@6`) from edge caches that can
|
|
14
|
+
* lag releases by days. Bump deliberately when newer verified versions ship.
|
|
15
|
+
*/
|
|
16
|
+
const INLANG_PLUGIN_MODULE = 'https://cdn.jsdelivr.net/npm/@inlang/plugin-i18next@6.2.0/dist/index.js';
|
|
17
|
+
/** VS Code marketplace id of the inlang Sherlock extension. */
|
|
18
|
+
const SHERLOCK_EXTENSION_ID = 'inlang.vs-code-extension';
|
|
19
|
+
/**
|
|
20
|
+
* Scaffolds an inlang project (`project.inlang/settings.json`) next to the
|
|
21
|
+
* i18next configuration so that inlang tooling (Sherlock VS Code extension,
|
|
22
|
+
* Fink editor, Paraglide compiler) operates on the EXISTING i18next JSON
|
|
23
|
+
* files. The i18next files remain the single source of truth — the scaffold
|
|
24
|
+
* is just the adapter.
|
|
25
|
+
*
|
|
26
|
+
* Behavior:
|
|
27
|
+
* - Derives `plugin.inlang.i18next.pathPattern` from the `extract.output`
|
|
28
|
+
* template: the namespaced object form when the template contains a
|
|
29
|
+
* `{{namespace}}` placeholder (namespaces are discovered from the files of
|
|
30
|
+
* the primary language), the plain string form otherwise.
|
|
31
|
+
* - Never overwrites an existing `project.inlang/settings.json`.
|
|
32
|
+
* - Adds the Sherlock extension to `.vscode/extensions.json` recommendations
|
|
33
|
+
* (creating or comment-preservingly merging the file).
|
|
34
|
+
*/
|
|
35
|
+
async function scaffoldInlangProject(options) {
|
|
36
|
+
const { locales, output } = options;
|
|
37
|
+
const baseLocale = options.primaryLanguage || locales[0] || 'en';
|
|
38
|
+
if (typeof output !== 'string') {
|
|
39
|
+
console.log('⚠️ Skipping inlang setup: extract.output is a function, so the file layout cannot be derived automatically. Create project.inlang/settings.json manually (see https://inlang.com/m/3i8bor92/plugin-inlang-i18next).');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Normalize Windows separators (heuristic-detected templates may be built
|
|
43
|
+
// with path.join) — settings.json patterns must be POSIX for portability.
|
|
44
|
+
// {{lng}} is a supported alias for {{language}}.
|
|
45
|
+
const template = output.replace(/\\/g, '/').replace(/\{\{lng\}\}/g, '{{language}}');
|
|
46
|
+
if (!template.endsWith('.json')) {
|
|
47
|
+
console.log('⚠️ Skipping inlang setup: the inlang i18next plugin supports JSON resource files only, but extract.output points to non-JSON files.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const settingsDir = node_path.resolve(process.cwd(), 'project.inlang');
|
|
51
|
+
const settingsPath = node_path.resolve(settingsDir, 'settings.json');
|
|
52
|
+
if (await fileExists(settingsPath)) {
|
|
53
|
+
console.log('ℹ️ project.inlang/settings.json already exists — leaving it untouched.');
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const pathPattern = template.includes('{{namespace}}')
|
|
57
|
+
? await deriveNamespacedPathPattern(template, baseLocale, options.defaultNS)
|
|
58
|
+
: toInlangPattern(template);
|
|
59
|
+
const settings = {
|
|
60
|
+
$schema: 'https://inlang.com/schema/project-settings',
|
|
61
|
+
baseLocale,
|
|
62
|
+
locales,
|
|
63
|
+
modules: [INLANG_PLUGIN_MODULE],
|
|
64
|
+
'plugin.inlang.i18next': { pathPattern },
|
|
65
|
+
};
|
|
66
|
+
await promises.mkdir(settingsDir, { recursive: true });
|
|
67
|
+
await promises.writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
68
|
+
console.log(`✅ inlang project created at: ${settingsPath}`);
|
|
69
|
+
console.log(' Your i18next JSON files stay the single source of truth — inlang tools read and write them directly.');
|
|
70
|
+
console.log(` • Sherlock (VS Code): install the recommended "${SHERLOCK_EXTENSION_ID}" extension`);
|
|
71
|
+
console.log(' • Fink (web editor for translators): https://fink.inlang.com');
|
|
72
|
+
console.log(' • Paraglide (compiled i18n): npx @inlang/paraglide-js compile --project ./project.inlang');
|
|
73
|
+
}
|
|
74
|
+
await recommendSherlockExtension();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Converts an i18next-cli output template into an inlang `pathPattern`:
|
|
78
|
+
* `{{language}}` becomes `{locale}`, and relative paths are prefixed with
|
|
79
|
+
* `./` as required by the plugin's settings schema (which also permits
|
|
80
|
+
* `../`-relative and absolute paths, so those pass through unchanged).
|
|
81
|
+
*/
|
|
82
|
+
function toInlangPattern(template) {
|
|
83
|
+
const pattern = template.replace(/\{\{language\}\}/g, '{locale}');
|
|
84
|
+
if (pattern.startsWith('./') || pattern.startsWith('../') || pattern.startsWith('/')) {
|
|
85
|
+
return pattern;
|
|
86
|
+
}
|
|
87
|
+
return `./${pattern}`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Builds the namespaced (object) form of `pathPattern` by discovering the
|
|
91
|
+
* project's namespaces from the existing resource files of the primary
|
|
92
|
+
* language. Falls back to the default namespace when no files exist yet
|
|
93
|
+
* (e.g. `init` ran before the first `extract`).
|
|
94
|
+
*/
|
|
95
|
+
async function deriveNamespacedPathPattern(template, baseLocale, defaultNS) {
|
|
96
|
+
const namespaces = await discoverNamespaces(template, baseLocale);
|
|
97
|
+
if (namespaces.length === 0) {
|
|
98
|
+
namespaces.push(typeof defaultNS === 'string' ? defaultNS : 'translation');
|
|
99
|
+
}
|
|
100
|
+
const pathPattern = {};
|
|
101
|
+
for (const ns of namespaces.sort()) {
|
|
102
|
+
pathPattern[ns] = toInlangPattern(template.replace(/\{\{namespace\}\}/g, ns));
|
|
103
|
+
}
|
|
104
|
+
return pathPattern;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Discovers namespace names by listing the directory entries that match the
|
|
108
|
+
* `{{namespace}}` segment of the output template, resolved for the primary
|
|
109
|
+
* language. Works for namespaces in the file name
|
|
110
|
+
* (`locales/en/{{namespace}}.json`) as well as in a directory segment
|
|
111
|
+
* (`locales/{{namespace}}/en.json`).
|
|
112
|
+
*/
|
|
113
|
+
async function discoverNamespaces(template, baseLocale) {
|
|
114
|
+
const resolved = template.replace(/\{\{language\}\}/g, baseLocale);
|
|
115
|
+
const segments = resolved.split('/');
|
|
116
|
+
const nsIndex = segments.findIndex(segment => segment.includes('{{namespace}}'));
|
|
117
|
+
if (nsIndex === -1)
|
|
118
|
+
return [];
|
|
119
|
+
const baseDir = node_path.resolve(process.cwd(), segments.slice(0, nsIndex).join('/'));
|
|
120
|
+
const [prefix, suffix = ''] = segments[nsIndex].split('{{namespace}}');
|
|
121
|
+
try {
|
|
122
|
+
const entries = await promises.readdir(baseDir);
|
|
123
|
+
return entries
|
|
124
|
+
.filter(entry => entry.startsWith(prefix) &&
|
|
125
|
+
entry.endsWith(suffix) &&
|
|
126
|
+
entry.length > prefix.length + suffix.length)
|
|
127
|
+
.map(entry => entry.slice(prefix.length, entry.length - suffix.length));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Adds the Sherlock VS Code extension to `.vscode/extensions.json`
|
|
135
|
+
* recommendations. Creates the file when missing; otherwise merges into the
|
|
136
|
+
* existing one while preserving comments and formatting (JSONC-aware). Bails
|
|
137
|
+
* gracefully — with a notice, never an error — when the existing file cannot
|
|
138
|
+
* be parsed.
|
|
139
|
+
*/
|
|
140
|
+
async function recommendSherlockExtension() {
|
|
141
|
+
const extensionsPath = node_path.resolve(process.cwd(), '.vscode', 'extensions.json');
|
|
142
|
+
let text;
|
|
143
|
+
try {
|
|
144
|
+
text = await promises.readFile(extensionsPath, 'utf-8');
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// File doesn't exist yet — create it.
|
|
148
|
+
}
|
|
149
|
+
if (text === undefined || text.trim() === '') {
|
|
150
|
+
await promises.mkdir(node_path.dirname(extensionsPath), { recursive: true });
|
|
151
|
+
await promises.writeFile(extensionsPath, JSON.stringify({ recommendations: [SHERLOCK_EXTENSION_ID] }, null, 2) + '\n');
|
|
152
|
+
console.log('✅ Added the Sherlock extension to .vscode/extensions.json recommendations.');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const errors = [];
|
|
156
|
+
const current = jsoncParser.parse(text, errors, { allowTrailingComma: true });
|
|
157
|
+
if (errors.length > 0 || typeof current !== 'object' || current === null || Array.isArray(current)) {
|
|
158
|
+
console.log('⚠️ Could not parse .vscode/extensions.json — please add "inlang.vs-code-extension" to its recommendations manually.');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const recommendations = Array.isArray(current.recommendations) ? current.recommendations : [];
|
|
162
|
+
const alreadyRecommended = recommendations.some(entry => typeof entry === 'string' && entry.toLowerCase() === SHERLOCK_EXTENSION_ID);
|
|
163
|
+
if (alreadyRecommended)
|
|
164
|
+
return;
|
|
165
|
+
const formattingOptions = { insertSpaces: true, tabSize: 2, eol: '\n' };
|
|
166
|
+
const edits = Array.isArray(current.recommendations)
|
|
167
|
+
// Append to the existing array (preserves comments and formatting).
|
|
168
|
+
? jsoncParser.modify(text, ['recommendations', recommendations.length], SHERLOCK_EXTENSION_ID, { isArrayInsertion: true, formattingOptions })
|
|
169
|
+
// No recommendations key yet — add one.
|
|
170
|
+
: jsoncParser.modify(text, ['recommendations'], [SHERLOCK_EXTENSION_ID], { formattingOptions });
|
|
171
|
+
await promises.writeFile(extensionsPath, jsoncParser.applyEdits(text, edits));
|
|
172
|
+
console.log('✅ Added the Sherlock extension to .vscode/extensions.json recommendations.');
|
|
173
|
+
}
|
|
174
|
+
async function fileExists(path) {
|
|
175
|
+
try {
|
|
176
|
+
await promises.readFile(path);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
exports.scaffoldInlangProject = scaffoldInlangProject;
|
package/dist/esm/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ const program = new Command();
|
|
|
31
31
|
program
|
|
32
32
|
.name('i18next-cli')
|
|
33
33
|
.description('A unified, high-performance i18next CLI.')
|
|
34
|
-
.version('1.
|
|
34
|
+
.version('1.63.0'); // This string is replaced with the actual version at build time by rollup
|
|
35
35
|
// new: global config override option
|
|
36
36
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
37
37
|
program
|
|
@@ -191,7 +191,8 @@ program
|
|
|
191
191
|
.command('init')
|
|
192
192
|
.description('Create a new i18next.config.ts/js file with an interactive setup wizard.')
|
|
193
193
|
.option('--ci', 'Skip the browser launch when a backend (e.g. Locize) is selected. The signup URL is printed instead.')
|
|
194
|
-
.
|
|
194
|
+
.option('--inlang', 'Also scaffold an inlang project (project.inlang/settings.json) so inlang tooling (Sherlock, Fink, Paraglide) works on the translation files. Skips the corresponding wizard question.')
|
|
195
|
+
.action((options) => runInit({ ci: !!options.ci, inlang: !!options.inlang }));
|
|
195
196
|
program
|
|
196
197
|
.command('lint')
|
|
197
198
|
.description('Find potential issues like hardcoded strings in your codebase.')
|
package/dist/esm/init.js
CHANGED
|
@@ -3,6 +3,7 @@ import { writeFile, readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { detectConfig } from './heuristic-config.js';
|
|
5
5
|
import { openBrowser, promptLocizeCredentials } from './utils/locize-onboarding.js';
|
|
6
|
+
import { scaffoldInlangProject } from './utils/inlang-scaffold.js';
|
|
6
7
|
|
|
7
8
|
const LOCIZE_SIGNUP_URL = 'https://www.locize.app/register?from=i18next_cli__init-wizard';
|
|
8
9
|
/**
|
|
@@ -154,6 +155,14 @@ async function runInit(options = {}) {
|
|
|
154
155
|
],
|
|
155
156
|
default: 'local',
|
|
156
157
|
},
|
|
158
|
+
{
|
|
159
|
+
type: 'confirm',
|
|
160
|
+
name: 'inlang',
|
|
161
|
+
message: 'Also set up inlang tooling (Sherlock VS Code extension, Fink editor, Paraglide) on these translation files?',
|
|
162
|
+
default: false,
|
|
163
|
+
// Skip the question when already requested via the --inlang flag.
|
|
164
|
+
when: () => !options.inlang,
|
|
165
|
+
},
|
|
157
166
|
]);
|
|
158
167
|
let locizeConfig;
|
|
159
168
|
if (answers.backend === 'locize') {
|
|
@@ -247,6 +256,13 @@ module.exports = ${toJs(configObject)}`;
|
|
|
247
256
|
const outputPath = resolve(process.cwd(), fileName);
|
|
248
257
|
await writeFile(outputPath, fileContent.trim());
|
|
249
258
|
console.log(`✅ Configuration file created at: ${outputPath}`);
|
|
259
|
+
if (options.inlang || answers.inlang) {
|
|
260
|
+
await scaffoldInlangProject({
|
|
261
|
+
locales: answers.locales,
|
|
262
|
+
primaryLanguage: answers.locales[0],
|
|
263
|
+
output: answers.output,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
250
266
|
if (locizeConfig) {
|
|
251
267
|
console.log('\nNext steps for Locize:');
|
|
252
268
|
console.log(' 1. Push your local translations to Locize:');
|
|
@@ -1464,7 +1464,8 @@ const I18N_INIT_FILE_NAMES = [
|
|
|
1464
1464
|
* Searches the common locations (`src/` and the project root) for an existing
|
|
1465
1465
|
* i18n initialization file.
|
|
1466
1466
|
*
|
|
1467
|
-
* @returns The path of the first init file found (relative to cwd
|
|
1467
|
+
* @returns The path of the first init file found (relative to cwd, native
|
|
1468
|
+
* platform separators), or null.
|
|
1468
1469
|
*/
|
|
1469
1470
|
async function findExistingI18nInitFile() {
|
|
1470
1471
|
const cwd = process.cwd();
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { mkdir, writeFile, readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { resolve, dirname } from 'node:path';
|
|
3
|
+
import { parse, modify, applyEdits } from 'jsonc-parser';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The plugin that teaches inlang tools (Sherlock, Fink, Paraglide) to read and
|
|
7
|
+
* write i18next JSON resource files directly.
|
|
8
|
+
*
|
|
9
|
+
* Pinned to an exact version on purpose: 6.2.0 is the first release with
|
|
10
|
+
* verified round-trip support for plurals, context, `_zero` and ordinal keys,
|
|
11
|
+
* and jsDelivr serves floating range URLs (`@6`) from edge caches that can
|
|
12
|
+
* lag releases by days. Bump deliberately when newer verified versions ship.
|
|
13
|
+
*/
|
|
14
|
+
const INLANG_PLUGIN_MODULE = 'https://cdn.jsdelivr.net/npm/@inlang/plugin-i18next@6.2.0/dist/index.js';
|
|
15
|
+
/** VS Code marketplace id of the inlang Sherlock extension. */
|
|
16
|
+
const SHERLOCK_EXTENSION_ID = 'inlang.vs-code-extension';
|
|
17
|
+
/**
|
|
18
|
+
* Scaffolds an inlang project (`project.inlang/settings.json`) next to the
|
|
19
|
+
* i18next configuration so that inlang tooling (Sherlock VS Code extension,
|
|
20
|
+
* Fink editor, Paraglide compiler) operates on the EXISTING i18next JSON
|
|
21
|
+
* files. The i18next files remain the single source of truth — the scaffold
|
|
22
|
+
* is just the adapter.
|
|
23
|
+
*
|
|
24
|
+
* Behavior:
|
|
25
|
+
* - Derives `plugin.inlang.i18next.pathPattern` from the `extract.output`
|
|
26
|
+
* template: the namespaced object form when the template contains a
|
|
27
|
+
* `{{namespace}}` placeholder (namespaces are discovered from the files of
|
|
28
|
+
* the primary language), the plain string form otherwise.
|
|
29
|
+
* - Never overwrites an existing `project.inlang/settings.json`.
|
|
30
|
+
* - Adds the Sherlock extension to `.vscode/extensions.json` recommendations
|
|
31
|
+
* (creating or comment-preservingly merging the file).
|
|
32
|
+
*/
|
|
33
|
+
async function scaffoldInlangProject(options) {
|
|
34
|
+
const { locales, output } = options;
|
|
35
|
+
const baseLocale = options.primaryLanguage || locales[0] || 'en';
|
|
36
|
+
if (typeof output !== 'string') {
|
|
37
|
+
console.log('⚠️ Skipping inlang setup: extract.output is a function, so the file layout cannot be derived automatically. Create project.inlang/settings.json manually (see https://inlang.com/m/3i8bor92/plugin-inlang-i18next).');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Normalize Windows separators (heuristic-detected templates may be built
|
|
41
|
+
// with path.join) — settings.json patterns must be POSIX for portability.
|
|
42
|
+
// {{lng}} is a supported alias for {{language}}.
|
|
43
|
+
const template = output.replace(/\\/g, '/').replace(/\{\{lng\}\}/g, '{{language}}');
|
|
44
|
+
if (!template.endsWith('.json')) {
|
|
45
|
+
console.log('⚠️ Skipping inlang setup: the inlang i18next plugin supports JSON resource files only, but extract.output points to non-JSON files.');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const settingsDir = resolve(process.cwd(), 'project.inlang');
|
|
49
|
+
const settingsPath = resolve(settingsDir, 'settings.json');
|
|
50
|
+
if (await fileExists(settingsPath)) {
|
|
51
|
+
console.log('ℹ️ project.inlang/settings.json already exists — leaving it untouched.');
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const pathPattern = template.includes('{{namespace}}')
|
|
55
|
+
? await deriveNamespacedPathPattern(template, baseLocale, options.defaultNS)
|
|
56
|
+
: toInlangPattern(template);
|
|
57
|
+
const settings = {
|
|
58
|
+
$schema: 'https://inlang.com/schema/project-settings',
|
|
59
|
+
baseLocale,
|
|
60
|
+
locales,
|
|
61
|
+
modules: [INLANG_PLUGIN_MODULE],
|
|
62
|
+
'plugin.inlang.i18next': { pathPattern },
|
|
63
|
+
};
|
|
64
|
+
await mkdir(settingsDir, { recursive: true });
|
|
65
|
+
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
66
|
+
console.log(`✅ inlang project created at: ${settingsPath}`);
|
|
67
|
+
console.log(' Your i18next JSON files stay the single source of truth — inlang tools read and write them directly.');
|
|
68
|
+
console.log(` • Sherlock (VS Code): install the recommended "${SHERLOCK_EXTENSION_ID}" extension`);
|
|
69
|
+
console.log(' • Fink (web editor for translators): https://fink.inlang.com');
|
|
70
|
+
console.log(' • Paraglide (compiled i18n): npx @inlang/paraglide-js compile --project ./project.inlang');
|
|
71
|
+
}
|
|
72
|
+
await recommendSherlockExtension();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Converts an i18next-cli output template into an inlang `pathPattern`:
|
|
76
|
+
* `{{language}}` becomes `{locale}`, and relative paths are prefixed with
|
|
77
|
+
* `./` as required by the plugin's settings schema (which also permits
|
|
78
|
+
* `../`-relative and absolute paths, so those pass through unchanged).
|
|
79
|
+
*/
|
|
80
|
+
function toInlangPattern(template) {
|
|
81
|
+
const pattern = template.replace(/\{\{language\}\}/g, '{locale}');
|
|
82
|
+
if (pattern.startsWith('./') || pattern.startsWith('../') || pattern.startsWith('/')) {
|
|
83
|
+
return pattern;
|
|
84
|
+
}
|
|
85
|
+
return `./${pattern}`;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Builds the namespaced (object) form of `pathPattern` by discovering the
|
|
89
|
+
* project's namespaces from the existing resource files of the primary
|
|
90
|
+
* language. Falls back to the default namespace when no files exist yet
|
|
91
|
+
* (e.g. `init` ran before the first `extract`).
|
|
92
|
+
*/
|
|
93
|
+
async function deriveNamespacedPathPattern(template, baseLocale, defaultNS) {
|
|
94
|
+
const namespaces = await discoverNamespaces(template, baseLocale);
|
|
95
|
+
if (namespaces.length === 0) {
|
|
96
|
+
namespaces.push(typeof defaultNS === 'string' ? defaultNS : 'translation');
|
|
97
|
+
}
|
|
98
|
+
const pathPattern = {};
|
|
99
|
+
for (const ns of namespaces.sort()) {
|
|
100
|
+
pathPattern[ns] = toInlangPattern(template.replace(/\{\{namespace\}\}/g, ns));
|
|
101
|
+
}
|
|
102
|
+
return pathPattern;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Discovers namespace names by listing the directory entries that match the
|
|
106
|
+
* `{{namespace}}` segment of the output template, resolved for the primary
|
|
107
|
+
* language. Works for namespaces in the file name
|
|
108
|
+
* (`locales/en/{{namespace}}.json`) as well as in a directory segment
|
|
109
|
+
* (`locales/{{namespace}}/en.json`).
|
|
110
|
+
*/
|
|
111
|
+
async function discoverNamespaces(template, baseLocale) {
|
|
112
|
+
const resolved = template.replace(/\{\{language\}\}/g, baseLocale);
|
|
113
|
+
const segments = resolved.split('/');
|
|
114
|
+
const nsIndex = segments.findIndex(segment => segment.includes('{{namespace}}'));
|
|
115
|
+
if (nsIndex === -1)
|
|
116
|
+
return [];
|
|
117
|
+
const baseDir = resolve(process.cwd(), segments.slice(0, nsIndex).join('/'));
|
|
118
|
+
const [prefix, suffix = ''] = segments[nsIndex].split('{{namespace}}');
|
|
119
|
+
try {
|
|
120
|
+
const entries = await readdir(baseDir);
|
|
121
|
+
return entries
|
|
122
|
+
.filter(entry => entry.startsWith(prefix) &&
|
|
123
|
+
entry.endsWith(suffix) &&
|
|
124
|
+
entry.length > prefix.length + suffix.length)
|
|
125
|
+
.map(entry => entry.slice(prefix.length, entry.length - suffix.length));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Adds the Sherlock VS Code extension to `.vscode/extensions.json`
|
|
133
|
+
* recommendations. Creates the file when missing; otherwise merges into the
|
|
134
|
+
* existing one while preserving comments and formatting (JSONC-aware). Bails
|
|
135
|
+
* gracefully — with a notice, never an error — when the existing file cannot
|
|
136
|
+
* be parsed.
|
|
137
|
+
*/
|
|
138
|
+
async function recommendSherlockExtension() {
|
|
139
|
+
const extensionsPath = resolve(process.cwd(), '.vscode', 'extensions.json');
|
|
140
|
+
let text;
|
|
141
|
+
try {
|
|
142
|
+
text = await readFile(extensionsPath, 'utf-8');
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// File doesn't exist yet — create it.
|
|
146
|
+
}
|
|
147
|
+
if (text === undefined || text.trim() === '') {
|
|
148
|
+
await mkdir(dirname(extensionsPath), { recursive: true });
|
|
149
|
+
await writeFile(extensionsPath, JSON.stringify({ recommendations: [SHERLOCK_EXTENSION_ID] }, null, 2) + '\n');
|
|
150
|
+
console.log('✅ Added the Sherlock extension to .vscode/extensions.json recommendations.');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const errors = [];
|
|
154
|
+
const current = parse(text, errors, { allowTrailingComma: true });
|
|
155
|
+
if (errors.length > 0 || typeof current !== 'object' || current === null || Array.isArray(current)) {
|
|
156
|
+
console.log('⚠️ Could not parse .vscode/extensions.json — please add "inlang.vs-code-extension" to its recommendations manually.');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const recommendations = Array.isArray(current.recommendations) ? current.recommendations : [];
|
|
160
|
+
const alreadyRecommended = recommendations.some(entry => typeof entry === 'string' && entry.toLowerCase() === SHERLOCK_EXTENSION_ID);
|
|
161
|
+
if (alreadyRecommended)
|
|
162
|
+
return;
|
|
163
|
+
const formattingOptions = { insertSpaces: true, tabSize: 2, eol: '\n' };
|
|
164
|
+
const edits = Array.isArray(current.recommendations)
|
|
165
|
+
// Append to the existing array (preserves comments and formatting).
|
|
166
|
+
? modify(text, ['recommendations', recommendations.length], SHERLOCK_EXTENSION_ID, { isArrayInsertion: true, formattingOptions })
|
|
167
|
+
// No recommendations key yet — add one.
|
|
168
|
+
: modify(text, ['recommendations'], [SHERLOCK_EXTENSION_ID], { formattingOptions });
|
|
169
|
+
await writeFile(extensionsPath, applyEdits(text, edits));
|
|
170
|
+
console.log('✅ Added the Sherlock extension to .vscode/extensions.json recommendations.');
|
|
171
|
+
}
|
|
172
|
+
async function fileExists(path) {
|
|
173
|
+
try {
|
|
174
|
+
await readFile(path);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export { scaffoldInlangProject };
|
package/package.json
CHANGED
package/types/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAqBnC,QAAA,MAAM,OAAO,SAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAqBnC,QAAA,MAAM,OAAO,SAAgB,CAAA;AA4c7B,OAAO,EAAE,OAAO,EAAE,CAAA"}
|
package/types/init.d.ts
CHANGED
package/types/init.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAqEA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,OAAO,CAAE,OAAO,GAAE;IAAE,EAAE,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,iBAsM9E"}
|
|
@@ -33,7 +33,8 @@ export declare function isProjectUsingTypeScript(): Promise<boolean>;
|
|
|
33
33
|
* Searches the common locations (`src/` and the project root) for an existing
|
|
34
34
|
* i18n initialization file.
|
|
35
35
|
*
|
|
36
|
-
* @returns The path of the first init file found (relative to cwd
|
|
36
|
+
* @returns The path of the first init file found (relative to cwd, native
|
|
37
|
+
* platform separators), or null.
|
|
37
38
|
*/
|
|
38
39
|
export declare function findExistingI18nInitFile(): Promise<string | null>;
|
|
39
40
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instrumenter.d.ts","sourceRoot":"","sources":["../../../src/instrumenter/core/instrumenter.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,eAAe,EAA6B,sBAAsB,EAAiE,MAAM,gBAAgB,CAAA;AAU1N;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,mBAAmB,EAC5B,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,sBAAsB,CAAC,CAoNjC;AAivCD;;GAEG;AACH,wBAAsB,mBAAmB,IAAK,OAAO,CAAC,OAAO,CAAC,CAU7D;AAID,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,aAAa,GAAG,MAAM,GAAG,SAAS,CAAA;AA6B/E;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,IAAK,OAAO,CAAC,kBAAkB,CAAC,CA8B7E;AAED;;GAEG;AACH,wBAAsB,wBAAwB,IAAK,OAAO,CAAC,OAAO,CAAC,CAOlE;AAwBD
|
|
1
|
+
{"version":3,"file":"instrumenter.d.ts","sourceRoot":"","sources":["../../../src/instrumenter/core/instrumenter.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,eAAe,EAA6B,sBAAsB,EAAiE,MAAM,gBAAgB,CAAA;AAU1N;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,mBAAmB,EAC5B,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,sBAAsB,CAAC,CAoNjC;AAivCD;;GAEG;AACH,wBAAsB,mBAAmB,IAAK,OAAO,CAAC,OAAO,CAAC,CAU7D;AAID,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,aAAa,GAAG,MAAM,GAAG,SAAS,CAAA;AA6B/E;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,IAAK,OAAO,CAAC,kBAAkB,CAAC,CA8B7E;AAED;;GAEG;AACH,wBAAsB,wBAAwB,IAAK,OAAO,CAAC,OAAO,CAAC,CAOlE;AAwBD;;;;;;GAMG;AACH,wBAAsB,wBAAwB,IAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAWxE;AAmYD;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,eAAe,EAAE,EAC7B,MAAM,EAAE,oBAAoB,EAC5B,SAAS,CAAC,EAAE,MAAM,EAClB,MAAM,GAAE,MAA4B,GACnC,OAAO,CAAC,IAAI,CAAC,CAoDf"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface InlangScaffoldOptions {
|
|
2
|
+
/** All project locales (the first one is used as `baseLocale` unless `primaryLanguage` is set). */
|
|
3
|
+
locales: string[];
|
|
4
|
+
/** The base/source locale. Defaults to the first entry of `locales`. */
|
|
5
|
+
primaryLanguage?: string;
|
|
6
|
+
/** The `extract.output` template, e.g. `public/locales/{{language}}/{{namespace}}.json`. */
|
|
7
|
+
output: string | ((language: string, namespace?: string) => string);
|
|
8
|
+
/** Default namespace used as fallback when no resource files exist yet (default: 'translation'). */
|
|
9
|
+
defaultNS?: string | false;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Scaffolds an inlang project (`project.inlang/settings.json`) next to the
|
|
13
|
+
* i18next configuration so that inlang tooling (Sherlock VS Code extension,
|
|
14
|
+
* Fink editor, Paraglide compiler) operates on the EXISTING i18next JSON
|
|
15
|
+
* files. The i18next files remain the single source of truth — the scaffold
|
|
16
|
+
* is just the adapter.
|
|
17
|
+
*
|
|
18
|
+
* Behavior:
|
|
19
|
+
* - Derives `plugin.inlang.i18next.pathPattern` from the `extract.output`
|
|
20
|
+
* template: the namespaced object form when the template contains a
|
|
21
|
+
* `{{namespace}}` placeholder (namespaces are discovered from the files of
|
|
22
|
+
* the primary language), the plain string form otherwise.
|
|
23
|
+
* - Never overwrites an existing `project.inlang/settings.json`.
|
|
24
|
+
* - Adds the Sherlock extension to `.vscode/extensions.json` recommendations
|
|
25
|
+
* (creating or comment-preservingly merging the file).
|
|
26
|
+
*/
|
|
27
|
+
export declare function scaffoldInlangProject(options: InlangScaffoldOptions): Promise<void>;
|
|
28
|
+
//# sourceMappingURL=inlang-scaffold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inlang-scaffold.d.ts","sourceRoot":"","sources":["../../src/utils/inlang-scaffold.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,qBAAqB;IACpC,mGAAmG;IACnG,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,wEAAwE;IACxE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,4FAA4F;IAC5F,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC,CAAA;IACnE,oGAAoG;IACpG,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;CAC3B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,qBAAqB,CAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgD1F"}
|