localingos 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "localingos",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "CLI tool to sync translations with Localingos",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -197,6 +197,10 @@ export function generateExtractPrompt(config) {
197
197
  const outputPattern = config.outputPattern || '{locale}.json';
198
198
  const languageName = getLanguageName(sourceLocale);
199
199
 
200
+ const descriptionsFile = config.sourceFile
201
+ ? config.sourceFile.replace(/\.json$/, '.descriptions.json')
202
+ : `./src/i18n/${sourceLocale}.descriptions.json`;
203
+
200
204
  const formatExample = format === 'json-flat'
201
205
  ? `{
202
206
  "page_name.section.element": "Source text in ${languageName} here"
@@ -209,6 +213,18 @@ export function generateExtractPrompt(config) {
209
213
  }
210
214
  }`;
211
215
 
216
+ const descriptionsExample = format === 'json-flat'
217
+ ? `{
218
+ "page_name.section.element": "Context for translators — where this text appears and what it means"
219
+ }`
220
+ : `{
221
+ "page_name": {
222
+ "section": {
223
+ "element": "Context for translators — where this text appears and what it means"
224
+ }
225
+ }
226
+ }`;
227
+
212
228
  // ─── Build framework-aware i18n setup instructions ───────────────────
213
229
  let i18nSetupInstructions;
214
230
  if (i18nLib) {
@@ -289,11 +305,21 @@ export const HeroMessages = defineMessages({
289
305
  text: 'Get Started',
290
306
  description: 'Primary CTA button in the hero section',
291
307
  },
308
+ GREETING: {
309
+ id: 'homepage.hero.greeting',
310
+ text: 'Hello, {{name}}!',
311
+ description: 'Personalized greeting — {{name}} is a placeholder',
312
+ },
292
313
  });
293
314
  \`\`\`
294
315
 
295
316
  **Rules for \`*${msgExt}\` files:**
296
- - Each entry has \`id\` (dot-separated key), \`text\` (the ${languageName} source text), and optional \`description\`
317
+ - Each entry has \`id\` (dot-separated key), \`text\` (the ${languageName} source text), and **required** \`description\`
318
+ - The \`description\` field is critical for translation quality — it tells the AI translator the context. Include:
319
+ - Where the text appears (e.g. "Header on the billing page", "Placeholder in the search input")
320
+ - What surrounds it (e.g. "Shown when user has no active subscription", "Button below the pricing cards")
321
+ - What any variables represent (e.g. "{{days}} is the number of trial days remaining")
322
+ - Any technical terms that need context (e.g. "CLI refers to the command-line interface tool")
297
323
  - Use UPPER_SNAKE_CASE for the Record keys (e.g. \`TITLE\`, \`BTN_SUBMIT\`, \`ERROR_REQUIRED\`)
298
324
  - The \`id\` must be unique across the entire project — no two entries in any \`*${msgExt}\` file can share the same \`id\`
299
325
  - Group keys by \`{page}.{section}.{element}\` using lowercase_snake_case
@@ -320,11 +346,29 @@ import { HeroMessages } from './Hero.messages';
320
346
  ### Step 4: Create the extraction script
321
347
 
322
348
  Create \`scripts/extract-messages.js\` — a deterministic Node.js script that:
323
- 1. Finds all \`**/*${msgExt}\` files in the source directory
324
- 2. Parses each file to extract \`id\` and \`text\` from every \`MessageDefinition\`
325
- 3. Builds nested JSON from all dot-path ids, using \`text\` as the ${languageName} value
326
- 4. Merges into the existing \`${sourceFile}\` **never overwrites existing values**, only adds new keys
327
- 5. Reports: new keys added, existing keys preserved, duplicate ids (which should error and abort)
349
+ 1. Reads the source locale and output paths from \`.localingos.json\` (fields: \`sourceLocale\`, \`sourceFile\`, \`outputDir\`). Fall back to \`sourceLocale: "${sourceLocale}"\`, \`sourceFile: "${sourceFile}"\`, \`outputDir: "${outputDir}"\` if the config file or fields are missing.
350
+ 2. Finds all \`**/*${msgExt}\` files in the source directory
351
+ 3. Parses each file to extract \`id\`, \`text\`, and \`description\` from every \`MessageDefinition\`
352
+ 4. Builds nested JSON from all dot-path ids, using \`text\` as the ${languageName} value, and writes it to the configured \`sourceFile\` path
353
+ 5. Builds a **separate descriptions file** at the same path but with a \`.descriptions.json\` suffix (e.g. \`${descriptionsFile}\`) using the same nested structure, but with \`description\` as the value. Only include keys that have a non-empty \`description\`. If no descriptions exist at all, still write an empty \`{}\` file.
354
+ 6. Merges into the existing source file — **never overwrites existing values**, only adds new keys
355
+ 7. Reports: new keys added, existing keys preserved, duplicate ids (which should error and abort)
356
+
357
+ **IMPORTANT**: Do NOT hardcode the output file name (e.g. \`en-US.json\`). The script must derive it from the \`sourceFile\` field in \`.localingos.json\` so it works for any configured locale.
358
+
359
+ The descriptions file uses the **same key structure** as the source file, but values are the description strings instead of the source text:
360
+
361
+ Source file (\`${sourceFile}\`):
362
+ \`\`\`json
363
+ ${formatExample}
364
+ \`\`\`
365
+
366
+ Descriptions file (\`${descriptionsFile}\`):
367
+ \`\`\`json
368
+ ${descriptionsExample}
369
+ \`\`\`
370
+
371
+ The descriptions file is consumed by \`localingos sync\` to provide context to the AI translator. It is NOT loaded by the i18n runtime — it is only used during the sync/push step. The i18n library only reads the source file and translation files.
328
372
 
329
373
  Wire it as an npm script:
330
374
  \`\`\`json
@@ -336,13 +380,14 @@ Wire it as an npm script:
336
380
  \`\`\`
337
381
 
338
382
  The script must:
339
- - Parse \`*${msgExt}\` files using regex (not TypeScript compilation) — look for \`id:\` and \`text:\` string literals within each \`{ ... }\` block
383
+ - Parse \`*${msgExt}\` files using regex (not TypeScript compilation) — look for \`id:\`, \`text:\`, and \`description:\` string literals within each \`{ ... }\` block
340
384
  - Support single-quoted, double-quoted, and backtick string literals
341
385
  - Convert dot-separated ids to nested JSON: \`'homepage.hero.title'\` → \`{ homepage: { hero: { title: "..." } } }\`
386
+ - Write the source text to \`${sourceFile}\` and descriptions to \`${descriptionsFile}\` (both using the same nested key structure)
342
387
  - Error on duplicate ids across different files
343
- - With \`--prune\` flag: also remove keys from the JSON that no longer exist in any \`*${msgExt}\` file
388
+ - With \`--prune\` flag: also remove keys from both JSON files that no longer exist in any \`*${msgExt}\` file
344
389
  - When NOT using \`--prune\`, if stale keys are detected, print: \`Run "npm run i18n:extract -- --prune" to remove them\`
345
- - With \`--check\` flag: exit with error code if the JSON would change (for CI use)
390
+ - With \`--check\` flag: exit with error code if either JSON file would change (for CI use)
346
391
 
347
392
  ---
348
393
 
@@ -361,16 +406,16 @@ ${i18nSetupInstructions}`;
361
406
  ${i18nSetupInstructions}
362
407
  ${i18nLib ? `3` : `2`}. Replace **every** hardcoded user-facing string in the source code with the appropriate i18n translation function call (e.g., \`t('key')\`, \`$t('key')\`, \`intl.formatMessage()\`, \`AppLocalizations.of(context).key\`, or whatever is idiomatic for the framework)
363
408
  ${i18nLib ? `4` : `3`}. Make sure the i18n import/hook/injection is added to every file that uses translation calls
364
- ${i18nLib ? `5` : `4`}. Preserve any dynamic values using interpolation: \`t('greeting', { name: userName })\``;
409
+ ${i18nLib ? `5` : `4`}. Preserve any dynamic values using interpolation: `t('greeting', { name: userName })` with `{{name}}` in the translation string`;
365
410
  }
366
411
 
367
412
  // ─── Assemble the full prompt ────────────────────────────────────────
368
413
  const prompt = `## Task: Internationalize this project — extract all translatable strings and wire up i18n end-to-end
369
414
 
370
415
  Scan this codebase and perform a **complete internationalization (i18n) setup**. This means:
371
- 1. Extract every user-facing string into structured message definition files
416
+ 1. Extract every user-facing string into structured message definition files (with descriptions for translator context)
372
417
  2. Replace every hardcoded string in the source code with a translation function call referencing the message definition
373
- 3. Create a deterministic extraction script that builds the source locale JSON from the message files
418
+ 3. Create a deterministic extraction script that builds the source locale JSON and a descriptions sidecar JSON from the message files
374
419
  4. Set up the i18n framework so the app loads translations at runtime
375
420
 
376
421
  The source language for this project is **${languageName}** (\`${sourceLocale}\`). All extracted strings must be in ${languageName}.
@@ -416,18 +461,25 @@ ${messageDefinitionSection}${nonJsI18nInstructions}
416
461
  ---
417
462
 
418
463
  ### Critical requirements:
419
- - **Extract ALL user-facing strings** — do not leave any hardcoded text in the source code
464
+ - **Work file by file** — iterate through each component file one at a time. For each file:
465
+ 1. Scan for ALL hardcoded strings: JSX text content AND string attributes (\`placeholder=\`, \`label=\`, \`header=\`, \`description=\`, \`loadingText=\`, \`emptyText=\`, \`errorText=\`, \`constraintText=\`, \`title=\`, \`ariaLabel=\`, \`alt=\`)
466
+ 2. Create or update the co-located \`*${msgExt}\` file with all extracted strings, each with a meaningful \`description\`
467
+ 3. Update the component to use \`t()\` calls for every extracted string
468
+ 4. Move to the next file
469
+ - **Extract ALL user-facing strings** — do not leave any hardcoded text in the source code. Check both JSX children AND component props/attributes
420
470
  - **Replace ALL hardcoded strings** with translation function calls — the app must render text from the i18n source file, not from inline strings${isJsProject ? `
421
471
  - **Never use raw string keys** — always reference through the Messages object: \`t(ComponentMessages.KEY)\`, not \`t('raw.key')\`` : ''}${isJsProject ? `
422
472
  - **Every component with translatable text must have a co-located \`*${msgExt}\` file**` : ''}
423
- - If a string contains dynamic values like \`\${name}\` or \`{count}\`, use the i18n library's interpolation syntax: \`"Hello, {name}"\` in the source file, \`t('key', { name })\` in the code
473
+ - **Every message MUST have a \`description\` field** this is not optional. The description provides context for AI translators and directly impacts translation quality. A missing description means a worse translation.
474
+ - If a string contains dynamic values like \`\${name}\` or \`{count}\`, use **double-brace** interpolation syntax: \`"Hello, {{name}}"\` in the source file, \`t('key', { name })\` in the code. CRITICAL: i18next requires \`{{double braces}}\` in translation strings — single \`{braces}\` will NOT interpolate.
424
475
  - Maintain the original text exactly as-is in the source file (in ${languageName})${isJsProject ? `
425
- - After creating all message files, run the extraction script to generate \`${sourceFile}\` from the message definitions` : ''}
476
+ - After creating all message files, run the extraction script to generate \`${sourceFile}\` and \`${descriptionsFile}\` from the message definitions` : ''}
426
477
  - After this is done, running \`localingos sync\` will push the source strings to Localingos and pull back translations for other languages — the app should already be wired to load those translated files from \`${outputDir}/\`
427
478
  - **Add a language/locale selector** to the app UI in the most appropriate, visible location (e.g., navigation bar, header, settings page, or footer). The selector must:
428
479
  - Display the available locales based on the translation files present in \`${outputDir}/\`
429
480
  - Allow users to switch languages at runtime
430
481
  - Persist the user's language preference (e.g., localStorage)
482
+ - On first visit (no saved preference), auto-detect the user's locale from \`navigator.language\`. If it matches a supported locale, use it. If only the base language matches (e.g. browser is "de" and you support "de-DE"), use the closest match. Otherwise fall back to \`${sourceLocale}\`.
431
483
  - Use the project's i18n framework to change the active locale (e.g., \`i18n.changeLanguage(locale)\` for i18next)
432
484
  - Show locale names in their native language when possible (e.g., "Español" not "Spanish", "Deutsch" not "German")
433
485
  - If a locale selector component already exists in the project, update it rather than creating a duplicate`;
@@ -465,7 +517,7 @@ ${messageDefinitionSection}${nonJsI18nInstructions}
465
517
  }
466
518
 
467
519
  if (isJsProject) {
468
- console.log(chalk.blue(`\nAfter the AI has created all *${msgExt} files and the extraction script, run "npm run i18n:extract" to generate ${sourceFile}, then "localingos sync" to push and get translations.\n`));
520
+ console.log(chalk.blue(`\nAfter the AI has created all *${msgExt} files and the extraction script, run "npm run i18n:extract" to generate ${sourceFile} and the descriptions file, then "localingos sync" to push and get translations.\n`));
469
521
  } else {
470
522
  console.log(chalk.blue(`\nAfter the AI has internationalized your code, run "localingos sync" to push source strings and get translations.\n`));
471
523
  }
@@ -4,9 +4,20 @@ import path from 'path';
4
4
  export function extract(filePath) {
5
5
  const raw = fs.readFileSync(filePath, 'utf-8');
6
6
  const json = JSON.parse(raw);
7
+
8
+ // Load descriptions sidecar file if it exists
9
+ const descPath = filePath.replace(/\.json$/, '.descriptions.json');
10
+ let descriptions = {};
11
+ if (fs.existsSync(descPath)) {
12
+ try {
13
+ descriptions = JSON.parse(fs.readFileSync(descPath, 'utf-8'));
14
+ } catch (e) { /* ignore malformed descriptions file */ }
15
+ }
16
+
7
17
  return Object.entries(json).map(([key, value]) => ({
8
18
  id: key,
9
- text: String(value)
19
+ text: String(value),
20
+ ...(descriptions[key] ? { description: String(descriptions[key]) } : {})
10
21
  }));
11
22
  }
12
23
 
@@ -41,7 +41,24 @@ function unflatten(translations) {
41
41
  export function extract(filePath) {
42
42
  const raw = fs.readFileSync(filePath, 'utf-8');
43
43
  const json = JSON.parse(raw);
44
- return flatten(json);
44
+ const entries = flatten(json);
45
+
46
+ // Load descriptions sidecar file if it exists
47
+ const descPath = filePath.replace(/\.json$/, '.descriptions.json');
48
+ let descriptions = {};
49
+ if (fs.existsSync(descPath)) {
50
+ try {
51
+ const descEntries = flatten(JSON.parse(fs.readFileSync(descPath, 'utf-8')));
52
+ for (const d of descEntries) {
53
+ descriptions[d.id] = d.text;
54
+ }
55
+ } catch (e) { /* ignore malformed descriptions file */ }
56
+ }
57
+
58
+ return entries.map(e => ({
59
+ ...e,
60
+ ...(descriptions[e.id] ? { description: descriptions[e.id] } : {})
61
+ }));
45
62
  }
46
63
 
47
64
  export function write(translations, outputDir, outputPattern) {