codexspec 0.6.2__tar.gz → 0.6.4__tar.gz

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.
Files changed (58) hide show
  1. {codexspec-0.6.2 → codexspec-0.6.4}/PKG-INFO +41 -16
  2. {codexspec-0.6.2 → codexspec-0.6.4}/README.md +40 -15
  3. {codexspec-0.6.2 → codexspec-0.6.4}/pyproject.toml +1 -1
  4. {codexspec-0.6.2 → codexspec-0.6.4}/src/codexspec/__init__.py +181 -71
  5. {codexspec-0.6.2 → codexspec-0.6.4}/src/codexspec/commands/installer.py +49 -1
  6. {codexspec-0.6.2 → codexspec-0.6.4}/src/codexspec/i18n.py +71 -117
  7. {codexspec-0.6.2 → codexspec-0.6.4}/src/codexspec/translator.py +40 -72
  8. codexspec-0.6.4/templates/translations/de.json +151 -0
  9. {codexspec-0.6.2 → codexspec-0.6.4}/templates/translations/en.json +2 -0
  10. {codexspec-0.6.2 → codexspec-0.6.4}/templates/translations/es.json +2 -0
  11. {codexspec-0.6.2 → codexspec-0.6.4}/templates/translations/fr.json +2 -0
  12. {codexspec-0.6.2 → codexspec-0.6.4}/templates/translations/ja.json +2 -0
  13. {codexspec-0.6.2 → codexspec-0.6.4}/templates/translations/ko.json +2 -0
  14. {codexspec-0.6.2 → codexspec-0.6.4}/templates/translations/pt-BR.json +2 -0
  15. {codexspec-0.6.2 → codexspec-0.6.4}/templates/translations/zh-CN.json +2 -0
  16. codexspec-0.6.2/templates/translations/de.json +0 -149
  17. {codexspec-0.6.2 → codexspec-0.6.4}/.gitignore +0 -0
  18. {codexspec-0.6.2 → codexspec-0.6.4}/LICENSE +0 -0
  19. {codexspec-0.6.2 → codexspec-0.6.4}/codexspec-icon.svg +0 -0
  20. {codexspec-0.6.2 → codexspec-0.6.4}/codexspec-logo-dark.svg +0 -0
  21. {codexspec-0.6.2 → codexspec-0.6.4}/codexspec-logo-light.svg +0 -0
  22. {codexspec-0.6.2 → codexspec-0.6.4}/scripts/bash/check-i18n-completeness.sh +0 -0
  23. {codexspec-0.6.2 → codexspec-0.6.4}/scripts/bash/check-i18n-structure.sh +0 -0
  24. {codexspec-0.6.2 → codexspec-0.6.4}/scripts/bash/check-prerequisites.sh +0 -0
  25. {codexspec-0.6.2 → codexspec-0.6.4}/scripts/bash/common.sh +0 -0
  26. {codexspec-0.6.2 → codexspec-0.6.4}/scripts/bash/create-new-feature.sh +0 -0
  27. {codexspec-0.6.2 → codexspec-0.6.4}/scripts/powershell/check-prerequisites.ps1 +0 -0
  28. {codexspec-0.6.2 → codexspec-0.6.4}/scripts/powershell/common.ps1 +0 -0
  29. {codexspec-0.6.2 → codexspec-0.6.4}/scripts/powershell/create-new-feature.ps1 +0 -0
  30. {codexspec-0.6.2 → codexspec-0.6.4}/src/codexspec/commands/__init__.py +0 -0
  31. {codexspec-0.6.2 → codexspec-0.6.4}/src/codexspec/idea.md +0 -0
  32. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/analyze.md +0 -0
  33. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/checklist.md +0 -0
  34. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/clarify.md +0 -0
  35. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/commit-staged.md +0 -0
  36. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/config.md +0 -0
  37. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/constitution.md +0 -0
  38. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/generate-spec.md +0 -0
  39. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/implement-tasks.md +0 -0
  40. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/plan-to-tasks.md +0 -0
  41. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/pr.md +0 -0
  42. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/quick.md +0 -0
  43. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/review-code.md +0 -0
  44. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/review-plan.md +0 -0
  45. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/review-spec.md +0 -0
  46. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/review-tasks.md +0 -0
  47. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/spec-to-plan.md +0 -0
  48. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/specify.md +0 -0
  49. {codexspec-0.6.2 → codexspec-0.6.4}/templates/commands/tasks-to-issues.md +0 -0
  50. {codexspec-0.6.2 → codexspec-0.6.4}/templates/docs/checklist-template.md +0 -0
  51. {codexspec-0.6.2 → codexspec-0.6.4}/templates/docs/constitution-template.md +0 -0
  52. {codexspec-0.6.2 → codexspec-0.6.4}/templates/docs/plan-template-detailed.md +0 -0
  53. {codexspec-0.6.2 → codexspec-0.6.4}/templates/docs/plan-template-simple.md +0 -0
  54. {codexspec-0.6.2 → codexspec-0.6.4}/templates/docs/requirements-template.md +0 -0
  55. {codexspec-0.6.2 → codexspec-0.6.4}/templates/docs/spec-template-detailed.md +0 -0
  56. {codexspec-0.6.2 → codexspec-0.6.4}/templates/docs/spec-template-simple.md +0 -0
  57. {codexspec-0.6.2 → codexspec-0.6.4}/templates/docs/tasks-template-detailed.md +0 -0
  58. {codexspec-0.6.2 → codexspec-0.6.4}/templates/docs/tasks-template-simple.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codexspec
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Summary: CodexSpec - A Requirements-First SDD toolkit for Claude Code
5
5
  Project-URL: Homepage, https://github.com/Zts0hg/codexspec
6
6
  Project-URL: Repository, https://github.com/Zts0hg/codexspec
@@ -528,12 +528,15 @@ Implementation follows **conditional TDD workflow**:
528
528
 
529
529
  | Option | Description |
530
530
  | --------------- | ------------------------------------- |
531
- | `PROJECT_NAME` | Project directory name |
532
- | `--here`, `-h` | Initialize in current directory |
531
+ | `PROJECT_NAME` | Project directory name (`.` or `--here` for current dir) |
532
+ | `--here`, `-h` | Initialize in the current directory |
533
533
  | `--ai`, `-a` | AI assistant to use (default: claude) |
534
- | `--lang`, `-l` | Output language (e.g., en, zh-CN, ja) |
535
- | `--force`, `-f` | Force overwrite existing files |
536
- | `--no-git` | Skip git initialization |
534
+ | `--lang`, `-l` | Output (base) language; interaction/document/commit fall back to it (e.g., en, zh-CN, ja) |
535
+ | `--interaction-lang` | Interaction language (LLM dialogue + CLI output); overrides `--lang` |
536
+ | `--document-lang` | Document language (generated spec/plan/tasks); overrides `--lang` |
537
+ | `--commit-lang` | Commit-message language; overrides `--lang` |
538
+ | `--force`, `-f` | Overwrite files + auto-confirm prompts; never regenerates `config.yml` |
539
+ | `--no-git` | Skip git repository initialization |
537
540
  | `--debug`, `-d` | Enable debug output |
538
541
 
539
542
  </details>
@@ -543,8 +546,10 @@ Implementation follows **conditional TDD workflow**:
543
546
 
544
547
  | Option | Description |
545
548
  | ------------------------- | ---------------------------- |
546
- | `--set-lang`, `-l` | Set output language |
547
- | `--set-commit-lang`, `-c` | Set commit message language |
549
+ | `--set-lang`, `-l` | Set the output (base) language |
550
+ | `--set-interaction-lang` | Set the interaction language |
551
+ | `--set-document-lang` | Set the document language |
552
+ | `--set-commit-lang`, `-c` | Set the commit-message language |
548
553
  | `--list-langs` | List all supported languages |
549
554
 
550
555
  </details>
@@ -628,28 +633,46 @@ CodexSpec is inspired by GitHub spec-kit with key differences:
628
633
 
629
634
  CodexSpec supports multiple languages through **LLM dynamic translation**. No translation templates to maintain - Claude translates content at runtime based on your language configuration.
630
635
 
636
+ ### Language Dimensions
637
+
638
+ CodexSpec splits language into four independently-configurable dimensions. `output` is the base; the others override it and fall back to it (then `en`) when unset — so you can converse with Claude in one language while keeping generated artifacts or commit messages in another.
639
+
640
+ | Dimension | `config.yml` key | Set at init | Set later | Controls | Falls back to |
641
+ |-----------|------------------|-------------|-----------|----------|---------------|
642
+ | Output (base) | `output` | `--lang` | `config --set-lang` | base for the other three | `en` |
643
+ | Interaction | `interaction` | `--interaction-lang` | `config --set-interaction-lang` | LLM dialogue + CLI output | output → `en` |
644
+ | Document | `document` | `--document-lang` | `config --set-document-lang` | generated spec/plan/tasks | output → `en` |
645
+ | Commit | `commit` | `--commit-lang` | `config --set-commit-lang` | git commit messages | output → `en` |
646
+ | Templates | `templates` | — | — | template source (always `en`) | — |
647
+
631
648
  ### Setting Language
632
649
 
633
650
  **During initialization:**
634
651
 
635
652
  ```bash
636
- # Create Chinese output project
653
+ # Chinese output (sets the output base)
637
654
  codexspec init my-project --lang zh-CN
638
655
 
639
- # Create Japanese output project
640
- codexspec init my-project --lang ja
656
+ # Fully non-interactive: zh-CN base, English commit messages
657
+ codexspec init my-project --lang zh-CN --commit-lang en
658
+
659
+ # Set every dimension explicitly (scriptable, no prompts)
660
+ codexspec init my-project \
661
+ --interaction-lang zh-CN --document-lang en --commit-lang en
641
662
  ```
642
663
 
664
+ First-time init in a TTY without `--lang` (and without all three dimension flags) prompts for a base language; in a non-TTY (CI/scripts) it defaults to `en`. Re-running `init` preserves any language key you did not specify.
665
+
643
666
  **After initialization:**
644
667
 
645
668
  ```bash
646
669
  # View current configuration
647
670
  codexspec config
648
671
 
649
- # Change output language
672
+ # Change a single dimension
650
673
  codexspec config --set-lang zh-CN
651
-
652
- # Set commit message language
674
+ codexspec config --set-interaction-lang zh-CN
675
+ codexspec config --set-document-lang en
653
676
  codexspec config --set-commit-lang en
654
677
  ```
655
678
 
@@ -680,8 +703,10 @@ codexspec config --set-commit-lang en
680
703
  version: "1.0"
681
704
 
682
705
  language:
683
- output: "zh-CN" # Output language
684
- commit: "zh-CN" # Commit message language (defaults to output)
706
+ output: "zh-CN" # Base language; the three below fall back to it, then "en"
707
+ interaction: "zh-CN" # LLM dialogue + codexspec CLI output (optional → defaults to output)
708
+ document: "en" # Generated requirements/spec/plan/tasks (optional → defaults to output)
709
+ commit: "en" # Git commit messages (optional → defaults to output)
685
710
  templates: "en" # Keep as "en"
686
711
 
687
712
  project:
@@ -483,12 +483,15 @@ Implementation follows **conditional TDD workflow**:
483
483
 
484
484
  | Option | Description |
485
485
  | --------------- | ------------------------------------- |
486
- | `PROJECT_NAME` | Project directory name |
487
- | `--here`, `-h` | Initialize in current directory |
486
+ | `PROJECT_NAME` | Project directory name (`.` or `--here` for current dir) |
487
+ | `--here`, `-h` | Initialize in the current directory |
488
488
  | `--ai`, `-a` | AI assistant to use (default: claude) |
489
- | `--lang`, `-l` | Output language (e.g., en, zh-CN, ja) |
490
- | `--force`, `-f` | Force overwrite existing files |
491
- | `--no-git` | Skip git initialization |
489
+ | `--lang`, `-l` | Output (base) language; interaction/document/commit fall back to it (e.g., en, zh-CN, ja) |
490
+ | `--interaction-lang` | Interaction language (LLM dialogue + CLI output); overrides `--lang` |
491
+ | `--document-lang` | Document language (generated spec/plan/tasks); overrides `--lang` |
492
+ | `--commit-lang` | Commit-message language; overrides `--lang` |
493
+ | `--force`, `-f` | Overwrite files + auto-confirm prompts; never regenerates `config.yml` |
494
+ | `--no-git` | Skip git repository initialization |
492
495
  | `--debug`, `-d` | Enable debug output |
493
496
 
494
497
  </details>
@@ -498,8 +501,10 @@ Implementation follows **conditional TDD workflow**:
498
501
 
499
502
  | Option | Description |
500
503
  | ------------------------- | ---------------------------- |
501
- | `--set-lang`, `-l` | Set output language |
502
- | `--set-commit-lang`, `-c` | Set commit message language |
504
+ | `--set-lang`, `-l` | Set the output (base) language |
505
+ | `--set-interaction-lang` | Set the interaction language |
506
+ | `--set-document-lang` | Set the document language |
507
+ | `--set-commit-lang`, `-c` | Set the commit-message language |
503
508
  | `--list-langs` | List all supported languages |
504
509
 
505
510
  </details>
@@ -583,28 +588,46 @@ CodexSpec is inspired by GitHub spec-kit with key differences:
583
588
 
584
589
  CodexSpec supports multiple languages through **LLM dynamic translation**. No translation templates to maintain - Claude translates content at runtime based on your language configuration.
585
590
 
591
+ ### Language Dimensions
592
+
593
+ CodexSpec splits language into four independently-configurable dimensions. `output` is the base; the others override it and fall back to it (then `en`) when unset — so you can converse with Claude in one language while keeping generated artifacts or commit messages in another.
594
+
595
+ | Dimension | `config.yml` key | Set at init | Set later | Controls | Falls back to |
596
+ |-----------|------------------|-------------|-----------|----------|---------------|
597
+ | Output (base) | `output` | `--lang` | `config --set-lang` | base for the other three | `en` |
598
+ | Interaction | `interaction` | `--interaction-lang` | `config --set-interaction-lang` | LLM dialogue + CLI output | output → `en` |
599
+ | Document | `document` | `--document-lang` | `config --set-document-lang` | generated spec/plan/tasks | output → `en` |
600
+ | Commit | `commit` | `--commit-lang` | `config --set-commit-lang` | git commit messages | output → `en` |
601
+ | Templates | `templates` | — | — | template source (always `en`) | — |
602
+
586
603
  ### Setting Language
587
604
 
588
605
  **During initialization:**
589
606
 
590
607
  ```bash
591
- # Create Chinese output project
608
+ # Chinese output (sets the output base)
592
609
  codexspec init my-project --lang zh-CN
593
610
 
594
- # Create Japanese output project
595
- codexspec init my-project --lang ja
611
+ # Fully non-interactive: zh-CN base, English commit messages
612
+ codexspec init my-project --lang zh-CN --commit-lang en
613
+
614
+ # Set every dimension explicitly (scriptable, no prompts)
615
+ codexspec init my-project \
616
+ --interaction-lang zh-CN --document-lang en --commit-lang en
596
617
  ```
597
618
 
619
+ First-time init in a TTY without `--lang` (and without all three dimension flags) prompts for a base language; in a non-TTY (CI/scripts) it defaults to `en`. Re-running `init` preserves any language key you did not specify.
620
+
598
621
  **After initialization:**
599
622
 
600
623
  ```bash
601
624
  # View current configuration
602
625
  codexspec config
603
626
 
604
- # Change output language
627
+ # Change a single dimension
605
628
  codexspec config --set-lang zh-CN
606
-
607
- # Set commit message language
629
+ codexspec config --set-interaction-lang zh-CN
630
+ codexspec config --set-document-lang en
608
631
  codexspec config --set-commit-lang en
609
632
  ```
610
633
 
@@ -635,8 +658,10 @@ codexspec config --set-commit-lang en
635
658
  version: "1.0"
636
659
 
637
660
  language:
638
- output: "zh-CN" # Output language
639
- commit: "zh-CN" # Commit message language (defaults to output)
661
+ output: "zh-CN" # Base language; the three below fall back to it, then "en"
662
+ interaction: "zh-CN" # LLM dialogue + codexspec CLI output (optional → defaults to output)
663
+ document: "en" # Generated requirements/spec/plan/tasks (optional → defaults to output)
664
+ commit: "en" # Git commit messages (optional → defaults to output)
640
665
  templates: "en" # Keep as "en"
641
666
 
642
667
  project:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codexspec"
3
- version = "0.6.2"
3
+ version = "0.6.4"
4
4
  description = "CodexSpec - A Requirements-First SDD toolkit for Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -26,20 +26,19 @@ from .commands.installer import (
26
26
  install_commands_to_subdir,
27
27
  migrate_old_commands,
28
28
  should_update_commands,
29
+ update_installed_command_frontmatter,
29
30
  )
30
31
  from .i18n import (
31
32
  generate_config_content,
32
- get_commit_language,
33
+ get_explicit_language_key,
33
34
  get_interaction_language,
34
35
  get_language_name,
35
36
  get_supported_languages,
36
37
  is_supported_language,
37
38
  normalize_locale,
38
- update_config_language,
39
39
  update_language_field,
40
- update_output_language,
41
40
  )
42
- from .translator import translate
41
+ from .translator import SUPPORTED_LANGUAGES, translate
43
42
 
44
43
  # Version info
45
44
  __version__ = "0.6.0"
@@ -273,6 +272,7 @@ def config(
273
272
  else:
274
273
  console.print("[red]Failed to update language setting[/red]")
275
274
  raise typer.Exit(1)
275
+ _rerender_command_frontmatter(config_file)
276
276
  return
277
277
 
278
278
  # Handle set commit language
@@ -351,6 +351,7 @@ def config(
351
351
  else:
352
352
  console.print("[red]Failed to update interaction language setting[/red]")
353
353
  raise typer.Exit(1)
354
+ _rerender_command_frontmatter(config_file)
354
355
  return
355
356
 
356
357
  # Handle set document language
@@ -445,13 +446,35 @@ def init(
445
446
  None,
446
447
  "--lang",
447
448
  "-l",
448
- help="Output language (e.g., en, zh-CN, ja). Interactive prompt if not specified in TTY.",
449
+ help=(
450
+ "Output (base) language (e.g., en, zh-CN, ja). interaction/document/commit "
451
+ "fall back to it. Prompted on first-time init if not given in a TTY."
452
+ ),
453
+ ),
454
+ interaction_lang: Optional[str] = typer.Option(
455
+ None,
456
+ "--interaction-lang",
457
+ help=("Interaction language (LLM dialogue + codexspec CLI output); overrides --lang for this dimension."),
458
+ ),
459
+ document_lang: Optional[str] = typer.Option(
460
+ None,
461
+ "--document-lang",
462
+ help="Document language (generated requirements/spec/plan/tasks); overrides --lang.",
463
+ ),
464
+ commit_lang: Optional[str] = typer.Option(
465
+ None,
466
+ "--commit-lang",
467
+ help="Commit-message language; overrides --lang.",
449
468
  ),
450
469
  force: bool = typer.Option(
451
470
  False,
452
471
  "--force",
453
472
  "-f",
454
- help="Force merge/overwrite when initializing in current directory",
473
+ help=(
474
+ "Overwrite existing files and auto-confirm prompts (skips the language "
475
+ "prompt and command-update/migration confirms). Language config is updated "
476
+ "surgically, never fully regenerated."
477
+ ),
455
478
  ),
456
479
  no_git: bool = typer.Option(
457
480
  False,
@@ -478,37 +501,104 @@ def init(
478
501
  codexspec init . --ai claude
479
502
  codexspec init --here --ai claude
480
503
  codexspec init . --force --ai claude
504
+ codexspec init my-project --interaction-lang en --document-lang zh-CN --commit-lang en
481
505
  """
482
- # Handle interactive language selection when --lang not specified
483
- if lang is None:
484
- if sys.stdin.isatty():
485
- # TTY environment: show interactive prompt
486
- try:
487
- lang = prompt_language_selection()
488
- except KeyboardInterrupt:
489
- console.print()
490
- console.print("[yellow]Selection cancelled, using default language (en)[/yellow]")
491
- lang = "en"
492
- else:
493
- # Non-TTY environment: use default
494
- lang = "en"
495
-
496
- # Normalize language code early for use throughout the function
497
- normalized_lang = normalize_locale(lang)
498
-
499
- # Determine target directory
506
+ # When init() is invoked directly (not via the Typer CLI), typer.Option defaults
507
+ # arrive as OptionInfo sentinels rather than None. Coerce the language params to
508
+ # Optional[str] so the resolution logic below is robust in both call paths.
509
+ lang = lang if isinstance(lang, str) else None
510
+ interaction_lang = interaction_lang if isinstance(interaction_lang, str) else None
511
+ document_lang = document_lang if isinstance(document_lang, str) else None
512
+ commit_lang = commit_lang if isinstance(commit_lang, str) else None
513
+
514
+ # Determine target directory first — needed to detect an existing config.yml
515
+ # before resolving the language base (REQ-007).
500
516
  if here or project_name == ".":
501
517
  target_dir = Path.cwd()
502
518
  elif project_name:
503
519
  target_dir = Path.cwd() / project_name
504
520
  else:
505
- console.print(f"[red]{translate('cli.init.error_no_project_name', normalized_lang)}[/red]")
521
+ console.print(f"[red]{translate('cli.init.error_no_project_name', 'en')}[/red]")
506
522
  raise typer.Exit(1)
507
523
 
524
+ config_file = target_dir / ".codexspec" / "config.yml"
525
+ config_exists = config_file.exists()
526
+
527
+ def _normalize_lang_option(raw: str) -> str:
528
+ """Normalize a language flag value; warn (do not error) if unsupported."""
529
+ normalized = normalize_locale(raw)
530
+ if not is_supported_language(normalized):
531
+ console.print(f"[yellow]Warning: '{raw}' is not in the list of commonly supported languages.[/yellow]")
532
+ console.print(
533
+ "It may still work if Claude supports it. "
534
+ "Run [cyan]codexspec config --list-langs[/cyan] to see supported languages."
535
+ )
536
+ return normalized
537
+
538
+ # Per-dimension overrides: each flag maps to exactly one config key, with no
539
+ # CLI-level precedence between them (DEC-001).
540
+ lang_overrides: dict[str, str] = {}
541
+ if interaction_lang is not None:
542
+ lang_overrides["interaction"] = _normalize_lang_option(interaction_lang)
543
+ if document_lang is not None:
544
+ lang_overrides["document"] = _normalize_lang_option(document_lang)
545
+ if commit_lang is not None:
546
+ lang_overrides["commit"] = _normalize_lang_option(commit_lang)
547
+
548
+ # The output base is determinable if --lang was given OR all three specific
549
+ # dimensions are explicit (output is then irrelevant). Otherwise it must be
550
+ # prompted (first-time, TTY) or defaulted (first-time, non-TTY); on an
551
+ # existing config with no flag, everything is preserved (REQ-007).
552
+ output_value: Optional[str] = None
553
+ if lang is not None:
554
+ output_value = _normalize_lang_option(lang)
555
+ base_determinable = output_value is not None or (
556
+ "interaction" in lang_overrides and "document" in lang_overrides and "commit" in lang_overrides
557
+ )
558
+ prompted_base = False
559
+ if base_determinable:
560
+ # Output base already resolved (or None when only dimension flags were
561
+ # given); no prompt needed to resolve it.
562
+ pass
563
+ elif config_exists:
564
+ # Existing project: preserve every unspecified key (REQ-007); no prompt.
565
+ pass
566
+ elif sys.stdin.isatty():
567
+ try:
568
+ output_value = normalize_locale(prompt_language_selection())
569
+ prompted_base = True
570
+ except KeyboardInterrupt:
571
+ console.print()
572
+ console.print("[yellow]Selection cancelled, using default language (en)[/yellow]")
573
+ output_value = "en"
574
+ else:
575
+ output_value = "en"
576
+
577
+ # Render/CLI-message language = the effective interaction language for THIS
578
+ # run (frontmatter descriptions are an interaction-language artifact). This is
579
+ # deliberately decoupled from output_value/prompted_base above, which only
580
+ # drive config writing and the first-time prompt. Precedence: explicit
581
+ # --interaction-lang -> explicitly-configured interaction -> --lang (output
582
+ # base) -> explicitly-configured output -> "en". Existing interaction is
583
+ # deliberately stronger than --lang; existing output is only a fallback when
584
+ # this run did not provide a new base.
585
+ if "interaction" in lang_overrides:
586
+ normalized_lang = lang_overrides["interaction"]
587
+ elif config_exists and (explicit_interaction := get_explicit_language_key(config_file, "interaction")) is not None:
588
+ normalized_lang = explicit_interaction
589
+ elif output_value is not None:
590
+ normalized_lang = output_value
591
+ elif config_exists and (explicit_output := get_explicit_language_key(config_file, "output")) is not None:
592
+ normalized_lang = explicit_output
593
+ else:
594
+ normalized_lang = "en"
595
+
508
596
  if debug:
509
597
  console.print(f"[dim]Target directory: {target_dir}[/dim]")
510
598
  console.print(f"[dim]AI assistant: {ai}[/dim]")
511
- console.print(f"[dim]Language: {lang} (normalized: {normalized_lang})[/dim]")
599
+ console.print(
600
+ f"[dim]Language base: {output_value} (messages: {normalized_lang}); overrides: {lang_overrides}[/dim]"
601
+ )
512
602
 
513
603
  # Check if directory exists and handle accordingly
514
604
  if target_dir.exists() and not here and project_name != ".":
@@ -591,7 +681,7 @@ def init(
591
681
  console.print(f"[dim]{translate('cli.init.migration_old_structure', normalized_lang)}[/dim]")
592
682
  console.print(f"[dim]{translate('cli.init.migration_new_structure', normalized_lang)}[/dim]")
593
683
 
594
- if Confirm.ask(translate("cli.init.migration_confirm", normalized_lang), default=True):
684
+ if force or Confirm.ask(translate("cli.init.migration_confirm", normalized_lang), default=True):
595
685
  if migrate_old_commands(claude_dir, old_files):
596
686
  console.print(f"[green]{translate('cli.init.migration_complete', normalized_lang)}[/green]")
597
687
  migration_happened = True
@@ -603,14 +693,14 @@ def init(
603
693
  # Install or update commands (translate after migration if user wants)
604
694
  if migration_happened:
605
695
  # Migration moved files, ask if user wants to update/translate them
606
- if Confirm.ask(translate("cli.init.update_confirm", normalized_lang), default=True):
696
+ if force or Confirm.ask(translate("cli.init.update_confirm", normalized_lang), default=True):
607
697
  count = install_commands_to_subdir(
608
698
  codexspec_commands_dir, templates_dir, force=True, language=normalized_lang
609
699
  )
610
700
  console.print(f"[green]{translate('cli.init.commands_updated', normalized_lang, count=count)}[/green]")
611
701
  elif should_update_commands(codexspec_dir):
612
702
  console.print()
613
- if Confirm.ask(translate("cli.init.update_confirm", normalized_lang), default=True):
703
+ if force or Confirm.ask(translate("cli.init.update_confirm", normalized_lang), default=True):
614
704
  count = install_commands_to_subdir(
615
705
  codexspec_commands_dir, templates_dir, force=True, language=normalized_lang
616
706
  )
@@ -636,53 +726,50 @@ def init(
636
726
  msg = translate("cli.init.created_file", normalized_lang, file=".codexspec/memory/constitution.md")
637
727
  console.print(f"[green]{msg}[/green]")
638
728
 
639
- # Create config.yml with language settings
729
+ # Apply language settings surgically (DEC-001 / CON-005 / REQ-005 / REQ-010).
730
+ # Build the full {key: value} map to write: the output base (if resolved) plus
731
+ # the per-dimension overrides. config.yml is never fully regenerated -- not
732
+ # even under --force -- and keys the user did not specify are always preserved.
640
733
  config_file = codexspec_dir / "config.yml"
641
- if not config_file.exists() or force:
642
- config_content = generate_config_content(
643
- language=normalized_lang,
644
- created=datetime.now().strftime("%Y-%m-%d"),
645
- )
646
- config_file.write_text(config_content, encoding="utf-8")
647
- lang_name = get_language_name(normalized_lang)
648
- msg = translate(
649
- "cli.init.created_file",
650
- normalized_lang,
651
- file=f".codexspec/config.yml (language: {lang_name})",
652
- )
653
- console.print(f"[green]{msg}[/green]")
654
- elif lang != "en": # User explicitly provided --lang (not default)
655
- # Check if commit language differs from selected language
656
- current_commit_lang = get_commit_language(config_file)
657
- if current_commit_lang and current_commit_lang != normalized_lang:
658
- # Prompt user to update commit language
734
+ keys_to_write: dict[str, str] = {}
735
+ if output_value is not None:
736
+ keys_to_write["output"] = output_value
737
+ keys_to_write.update(lang_overrides)
738
+
739
+ wrote_keys: list[str] = []
740
+ if keys_to_write:
741
+ if not config_exists:
742
+ # Fresh project: emit only the chosen keys (sparse config).
743
+ config_content = generate_config_content(
744
+ output=keys_to_write.get("output"),
745
+ interaction=keys_to_write.get("interaction"),
746
+ document=keys_to_write.get("document"),
747
+ commit=keys_to_write.get("commit"),
748
+ created=datetime.now().strftime("%Y-%m-%d"),
749
+ )
750
+ config_file.write_text(config_content, encoding="utf-8")
751
+ wrote_keys = list(keys_to_write.keys())
752
+ lang_name = get_language_name(normalized_lang)
659
753
  msg = translate(
660
- "cli.init.commit_lang_differs",
754
+ "cli.init.created_file",
661
755
  normalized_lang,
662
- current=current_commit_lang,
663
- selected=normalized_lang,
756
+ file=f".codexspec/config.yml (language: {lang_name})",
664
757
  )
665
- if Confirm.ask(msg, default=True):
666
- update_config_language(config_file, normalized_lang)
667
- console.print(
668
- f"[green]{translate('cli.init.commit_lang_updated', normalized_lang, lang=normalized_lang)}[/green]"
669
- )
670
- else:
671
- # Update only output language, keep commit language
672
- update_output_language(config_file, normalized_lang)
673
- console.print(
674
- f"[dim]{translate('cli.init.commit_lang_kept', normalized_lang, lang=current_commit_lang)}[/dim]"
675
- )
758
+ console.print(f"[green]{msg}[/green]")
676
759
  else:
677
- # No difference or commit lang not set, update both
678
- if update_config_language(config_file, normalized_lang):
679
- lang_name = get_language_name(normalized_lang)
680
- msg = translate(
681
- "cli.init.language_updated",
682
- normalized_lang,
683
- lang_name=lang_name,
684
- )
685
- console.print(f"[green]{msg}[/green]")
760
+ # Existing project: update each chosen key in place, preserving the rest.
761
+ for key, value in keys_to_write.items():
762
+ if update_language_field(config_file, key, value):
763
+ wrote_keys.append(key)
764
+
765
+ # Non-blocking notice of which language keys were set (REQ-010).
766
+ if wrote_keys:
767
+ details = ", ".join(f"{k}={keys_to_write[k]}" for k in wrote_keys)
768
+ console.print(f"[dim]{translate('cli.init.language_key_set', normalized_lang, details=details)}[/dim]")
769
+
770
+ # Discoverability hint after a first-time interactive base selection (REQ-009).
771
+ if prompted_base:
772
+ console.print(f"[dim]{translate('cli.init.language_dimensions_hint', normalized_lang)}[/dim]")
686
773
 
687
774
  # Create CLAUDE.md
688
775
  claude_md = target_dir / "CLAUDE.md"
@@ -738,6 +825,29 @@ def init(
738
825
  console.print(f"[yellow]{translate('cli.init.important_action', normalized_lang)}[/yellow]")
739
826
 
740
827
 
828
+ def _rerender_command_frontmatter(config_file: Path) -> None:
829
+ """Re-render installed command frontmatter in the current interaction language.
830
+
831
+ Called after a ``config`` language change so ``.claude/commands/codexspec/*.md``
832
+ descriptions follow the new interaction language immediately, mirroring what
833
+ ``init`` does. No-op when commands are not installed. Languages without a
834
+ pre-translated cache are not re-rendered here (that path would invoke the
835
+ ``claude`` CLI once per command); the user is advised to run ``codexspec init``.
836
+ """
837
+ commands_subdir = Path.cwd() / ".claude" / "commands" / COMMANDS_SUBDIR
838
+ if not commands_subdir.exists():
839
+ return
840
+ language = get_interaction_language(config_file) # re-read AFTER the config write
841
+ if language != "en" and language not in SUPPORTED_LANGUAGES:
842
+ console.print(f"[dim]Run [cyan]codexspec init[/cyan] to update command descriptions for {language}.[/dim]")
843
+ return
844
+ templates_dir = get_templates_dir() / "commands"
845
+ if not templates_dir.exists():
846
+ return
847
+ update_installed_command_frontmatter(commands_subdir, templates_dir, language=language)
848
+ console.print(f"[dim]Updated command descriptions to: {get_language_name(language)}[/dim]")
849
+
850
+
741
851
  def _print_command_summary(language: str = "en") -> None:
742
852
  """Print a summary of installed commands grouped by category."""
743
853
  metadata = get_commands_metadata()
@@ -8,7 +8,12 @@ import shutil
8
8
  from pathlib import Path
9
9
  from typing import Optional, TypedDict
10
10
 
11
- from codexspec.translator import extract_frontmatter_fields, load_translation_cache, translate_template_frontmatter
11
+ from codexspec.translator import (
12
+ apply_translations_to_template,
13
+ extract_frontmatter_fields,
14
+ load_translation_cache,
15
+ translate_template_frontmatter,
16
+ )
12
17
 
13
18
  # Constants
14
19
  COMMANDS_SUBDIR = "codexspec" # Subdirectory name for commands
@@ -348,6 +353,49 @@ def install_commands_to_subdir(
348
353
  return installed_count
349
354
 
350
355
 
356
+ def update_installed_command_frontmatter(
357
+ target_dir: Path,
358
+ templates_dir: Path,
359
+ language: str = "en",
360
+ translations_dir: Optional[Path] = None,
361
+ ) -> int:
362
+ """Update frontmatter translations for already-installed command files.
363
+
364
+ This preserves command bodies and any non-translated frontmatter while
365
+ refreshing the language-specific ``description`` and ``argument-hint`` fields.
366
+ Missing installed commands are intentionally skipped.
367
+ """
368
+ if not target_dir.exists() or not templates_dir.exists():
369
+ return 0
370
+
371
+ translation_cache = None
372
+ if language != "en":
373
+ translation_cache = load_translation_cache(language, translations_dir)
374
+
375
+ updated_count = 0
376
+ for template_file in templates_dir.glob("*.md"):
377
+ target_path = target_dir / template_file.name
378
+ if not target_path.exists():
379
+ continue
380
+
381
+ template_content = template_file.read_text(encoding="utf-8")
382
+ template_name = template_file.stem
383
+ rendered_template = translate_template_frontmatter(template_content, template_name, language, translation_cache)
384
+ fields = {
385
+ key: value for key, value in extract_frontmatter_fields(rendered_template).items() if value is not None
386
+ }
387
+ if not fields:
388
+ continue
389
+
390
+ current_content = target_path.read_text(encoding="utf-8")
391
+ updated_content = apply_translations_to_template(current_content, fields)
392
+ if updated_content != current_content:
393
+ target_path.write_text(updated_content, encoding="utf-8")
394
+ updated_count += 1
395
+
396
+ return updated_count
397
+
398
+
351
399
  def should_update_commands(codexspec_dir: Path) -> bool:
352
400
  """Check if commands need updating (subdirectory already exists).
353
401