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 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.62.0'); // This string is replaced with the actual version at build time by rollup
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
- .action((options) => init.runInit({ ci: !!options.ci }));
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), or null.
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.62.0'); // This string is replaced with the actual version at build time by rollup
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
- .action((options) => runInit({ ci: !!options.ci }));
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), or null.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.62.0",
3
+ "version": "1.63.0",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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;AA2c7B,OAAO,EAAE,OAAO,EAAE,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
@@ -27,5 +27,6 @@
27
27
  */
28
28
  export declare function runInit(options?: {
29
29
  ci?: boolean;
30
+ inlang?: boolean;
30
31
  }): Promise<void>;
31
32
  //# sourceMappingURL=init.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAoEA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,OAAO,CAAE,OAAO,GAAE;IAAE,EAAE,CAAC,EAAE,OAAO,CAAA;CAAO,iBAsL5D"}
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), or null.
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;;;;;GAKG;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"}
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"}