monoco-toolkit 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl

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 (43) hide show
  1. monoco/core/config.py +60 -8
  2. monoco/core/feature.py +58 -0
  3. monoco/core/injection.py +196 -0
  4. monoco/core/integrations.py +181 -0
  5. monoco/core/registry.py +36 -0
  6. monoco/core/resources/en/AGENTS.md +8 -0
  7. monoco/core/resources/en/SKILL.md +66 -0
  8. monoco/core/resources/zh/AGENTS.md +8 -0
  9. monoco/core/resources/zh/SKILL.md +66 -0
  10. monoco/core/setup.py +40 -24
  11. monoco/core/skills.py +444 -0
  12. monoco/core/sync.py +224 -0
  13. monoco/core/workspace.py +2 -6
  14. monoco/daemon/services.py +1 -1
  15. monoco/features/config/commands.py +104 -44
  16. monoco/features/i18n/adapter.py +29 -0
  17. monoco/features/i18n/core.py +1 -11
  18. monoco/features/i18n/resources/en/AGENTS.md +8 -0
  19. monoco/features/i18n/resources/en/SKILL.md +94 -0
  20. monoco/features/i18n/resources/zh/AGENTS.md +8 -0
  21. monoco/features/i18n/resources/zh/SKILL.md +94 -0
  22. monoco/features/issue/adapter.py +34 -0
  23. monoco/features/issue/commands.py +8 -8
  24. monoco/features/issue/core.py +5 -16
  25. monoco/features/issue/migration.py +134 -0
  26. monoco/features/issue/models.py +5 -3
  27. monoco/features/issue/resources/en/AGENTS.md +9 -0
  28. monoco/features/issue/resources/en/SKILL.md +51 -0
  29. monoco/features/issue/resources/zh/AGENTS.md +9 -0
  30. monoco/features/issue/resources/zh/SKILL.md +85 -0
  31. monoco/features/spike/adapter.py +30 -0
  32. monoco/features/spike/core.py +3 -20
  33. monoco/features/spike/resources/en/AGENTS.md +7 -0
  34. monoco/features/spike/resources/en/SKILL.md +74 -0
  35. monoco/features/spike/resources/zh/AGENTS.md +7 -0
  36. monoco/features/spike/resources/zh/SKILL.md +74 -0
  37. monoco/main.py +4 -0
  38. {monoco_toolkit-0.1.4.dist-info → monoco_toolkit-0.1.6.dist-info}/METADATA +1 -1
  39. monoco_toolkit-0.1.6.dist-info/RECORD +62 -0
  40. monoco_toolkit-0.1.4.dist-info/RECORD +0 -36
  41. {monoco_toolkit-0.1.4.dist-info → monoco_toolkit-0.1.6.dist-info}/WHEEL +0 -0
  42. {monoco_toolkit-0.1.4.dist-info → monoco_toolkit-0.1.6.dist-info}/entry_points.txt +0 -0
  43. {monoco_toolkit-0.1.4.dist-info → monoco_toolkit-0.1.6.dist-info}/licenses/LICENSE +0 -0
monoco/core/sync.py ADDED
@@ -0,0 +1,224 @@
1
+ import typer
2
+ from pathlib import Path
3
+ from typing import Optional, List
4
+ from monoco.core.registry import FeatureRegistry
5
+ from monoco.core.injection import PromptInjector
6
+ from monoco.core.config import get_config
7
+ from monoco.core.skills import SkillManager
8
+ from monoco.core.integrations import get_active_integrations
9
+ from rich.console import Console
10
+
11
+ console = Console()
12
+
13
+ def _get_targets(root: Path, config, cli_target: Optional[Path]) -> List[Path]:
14
+ """Helper to determine target files."""
15
+ targets = []
16
+
17
+ # 1. CLI Target
18
+ if cli_target:
19
+ targets.append(cli_target)
20
+ return targets
21
+
22
+ # 2. Config Targets
23
+ if config.agent.targets:
24
+ for t in config.agent.targets:
25
+ targets.append(root / t)
26
+ return targets
27
+
28
+ # 3. Registry Defaults (Dynamic Detection)
29
+ integrations = get_active_integrations(
30
+ root,
31
+ config_overrides=config.agent.integrations,
32
+ auto_detect=True
33
+ )
34
+
35
+ if integrations:
36
+ for integration in integrations.values():
37
+ targets.append(root / integration.system_prompt_file)
38
+ else:
39
+ # Fallback to standard Monoco header if nothing is detected
40
+ # but we usually want at least one target for a generic sync.
41
+ defaults = ["GEMINI.md", "CLAUDE.md"]
42
+ targets.extend([root / fname for fname in defaults])
43
+
44
+ return list(set(targets)) # Unique paths
45
+
46
+ def sync_command(
47
+ ctx: typer.Context,
48
+ target: Optional[Path] = typer.Option(None, "--target", "-t", help="Specific file to update (default: auto-detect from config or standard files)"),
49
+ check: bool = typer.Option(False, "--check", help="Dry run check mode")
50
+ ):
51
+ """
52
+ Synchronize Agent Environment (System Prompts & Skills).
53
+ Aggregates prompts from all active features and injects them into the agent configuration files.
54
+ """
55
+ root = Path.cwd() # TODO: Use workspace root detection properly if needed
56
+
57
+ # 0. Load Config
58
+ config = get_config(str(root))
59
+
60
+ # 1. Register Features
61
+ registry = FeatureRegistry()
62
+ registry.load_defaults()
63
+
64
+ # 2. Collect Data
65
+ collected_prompts = {}
66
+
67
+ # Filter features based on config if specified
68
+ all_features = registry.get_features()
69
+ active_features = []
70
+
71
+ if config.agent.includes:
72
+ for f in all_features:
73
+ if f.name in config.agent.includes:
74
+ active_features.append(f)
75
+ else:
76
+ active_features = all_features
77
+
78
+ with console.status("[bold green]Collecting feature integration data...") as status:
79
+ for feature in active_features:
80
+ status.update(f"Scanning Feature: {feature.name}")
81
+ try:
82
+ data = feature.integrate(root, config.model_dump())
83
+ if data:
84
+ if data.system_prompts:
85
+ collected_prompts.update(data.system_prompts)
86
+ except Exception as e:
87
+ console.print(f"[red]Error integrating feature {feature.name}: {e}[/red]")
88
+
89
+ console.print(f"[blue]Collected {len(collected_prompts)} prompts from {len(active_features)} features.[/blue]")
90
+
91
+ # 3. Distribute Skills
92
+ console.print(f"[bold blue]Distributing skills to agent frameworks...[/bold blue]")
93
+
94
+ # Determine language from config
95
+ skill_lang = config.i18n.source_lang if config.i18n.source_lang else 'en'
96
+ console.print(f"[dim] Using language: {skill_lang}[/dim]")
97
+
98
+ # Initialize SkillManager with active features
99
+ skill_manager = SkillManager(root, active_features)
100
+
101
+ # Get active integrations
102
+ integrations = get_active_integrations(
103
+ root,
104
+ config_overrides=config.agent.integrations,
105
+ auto_detect=True
106
+ )
107
+
108
+ if integrations:
109
+ for framework_key, integration in integrations.items():
110
+ skill_target_dir = root / integration.skill_root_dir
111
+ console.print(f"[dim] Distributing to {integration.name} ({skill_target_dir})...[/dim]")
112
+
113
+ try:
114
+ # Distribute only the configured language version
115
+ results = skill_manager.distribute(skill_target_dir, lang=skill_lang, force=False)
116
+ success_count = sum(1 for v in results.values() if v)
117
+ console.print(f"[green] ✓ Distributed {success_count}/{len(results)} skills to {integration.name}[/green]")
118
+ except Exception as e:
119
+ console.print(f"[red] Failed to distribute skills to {integration.name}: {e}[/red]")
120
+ else:
121
+ console.print(f"[yellow]No agent frameworks detected. Skipping skill distribution.[/yellow]")
122
+
123
+ # 4. Determine Targets
124
+ targets = _get_targets(root, config, target)
125
+
126
+ # Ensure targets exist for sync
127
+ final_targets = []
128
+ for t in targets:
129
+ if not t.exists():
130
+ # If explicit target, fail? Or create?
131
+ # If default, create.
132
+ if target:
133
+ # CLI target
134
+ console.print(f"[yellow]Creating {t.name}...[/yellow]")
135
+ try:
136
+ t.touch()
137
+ final_targets.append(t)
138
+ except Exception as e:
139
+ console.print(f"[red]Failed to create {t}: {e}[/red]")
140
+ else:
141
+ # Default/Config target -> only create if it's one of the defaults we manage?
142
+ # For now, let's just create it to be safe, assuming user wants it.
143
+ console.print(f"[yellow]Creating {t.name}...[/yellow]")
144
+ try:
145
+ t.touch()
146
+ final_targets.append(t)
147
+ except Exception as e:
148
+ console.print(f"[red]Failed to create {t}: {e}[/red]")
149
+ else:
150
+ final_targets.append(t)
151
+
152
+ # 5. Inject System Prompts
153
+ for t in final_targets:
154
+ injector = PromptInjector(t)
155
+
156
+ if check:
157
+ console.print(f"[dim][Dry Run] Would check/update {t.name}[/dim]")
158
+ else:
159
+ try:
160
+ changed = injector.inject(collected_prompts)
161
+ if changed:
162
+ console.print(f"[green]✓ Updated {t.name}[/green]")
163
+ else:
164
+ console.print(f"[dim]= {t.name} is up to date[/dim]")
165
+ except Exception as e:
166
+ console.print(f"[red]Failed to update {t.name}: {e}[/red]")
167
+
168
+ def uninstall_command(
169
+ ctx: typer.Context,
170
+ target: Optional[Path] = typer.Option(None, "--target", "-t", help="Specific file to clean (default: auto-detect from config or standard files)")
171
+ ):
172
+ """
173
+ Remove Monoco Managed Block from Agent Environment files and clean up distributed skills.
174
+ """
175
+ root = Path.cwd()
176
+ config = get_config(str(root))
177
+
178
+ # 1. Clean up System Prompts
179
+ targets = _get_targets(root, config, target)
180
+
181
+ for t in targets:
182
+ if not t.exists():
183
+ if target:
184
+ console.print(f"[yellow]Target {t} does not exist.[/yellow]")
185
+ continue
186
+
187
+ injector = PromptInjector(t)
188
+ try:
189
+ changed = injector.remove()
190
+ if changed:
191
+ console.print(f"[green]✓ Removed Monoco Managed Block from {t.name}[/green]")
192
+ else:
193
+ console.print(f"[dim]= No Monoco Block found in {t.name}[/dim]")
194
+ except Exception as e:
195
+ console.print(f"[red]Failed to uninstall from {t.name}: {e}[/red]")
196
+
197
+ # 2. Clean up Skills
198
+ console.print(f"[bold blue]Cleaning up distributed skills...[/bold blue]")
199
+
200
+ # Load features to get skill list
201
+ registry = FeatureRegistry()
202
+ registry.load_defaults()
203
+ active_features = registry.get_features()
204
+
205
+ skill_manager = SkillManager(root, active_features)
206
+
207
+ # Get active integrations
208
+ integrations = get_active_integrations(
209
+ root,
210
+ config_overrides=config.agent.integrations,
211
+ auto_detect=True
212
+ )
213
+
214
+ if integrations:
215
+ for framework_key, integration in integrations.items():
216
+ skill_target_dir = root / integration.skill_root_dir
217
+ console.print(f"[dim] Cleaning {integration.name} ({skill_target_dir})...[/dim]")
218
+
219
+ try:
220
+ skill_manager.cleanup(skill_target_dir)
221
+ except Exception as e:
222
+ console.print(f"[red] Failed to clean skills from {integration.name}: {e}[/red]")
223
+ else:
224
+ console.print(f"[yellow]No agent frameworks detected. Skipping skill cleanup.[/yellow]")
monoco/core/workspace.py CHANGED
@@ -5,16 +5,12 @@ def is_project_root(path: Path) -> bool:
5
5
  """
6
6
  Check if a directory serves as a Monoco project root.
7
7
  Criteria:
8
- - has monoco.yaml
9
- - OR has .monoco/config.yaml
10
- - OR has Issues/ directory
8
+ - has .monoco/ directory
11
9
  """
12
10
  if not path.is_dir():
13
11
  return False
14
12
 
15
- return (path / "monoco.yaml").exists() or \
16
- (path / ".monoco" / "config.yaml").exists() or \
17
- (path / "Issues").exists()
13
+ return (path / ".monoco").is_dir()
18
14
 
19
15
  def find_projects(workspace_root: Path) -> List[Path]:
20
16
  """
monoco/daemon/services.py CHANGED
@@ -129,7 +129,7 @@ class ProjectManager:
129
129
  def scan(self):
130
130
  """
131
131
  Scans workspace for potential Monoco projects.
132
- A directory is a project if it has monoco.yaml or .monoco/config.yaml or an Issues directory.
132
+ A directory is a project if it has a .monoco/ directory.
133
133
  """
134
134
  logger.info(f"Scanning workspace: {self.workspace_root}")
135
135
  from monoco.core.workspace import find_projects
@@ -1,70 +1,130 @@
1
1
  import typer
2
2
  import yaml
3
+ import json
3
4
  from pathlib import Path
4
- from typing import Optional
5
- from monoco.core.config import get_config, MonocoConfig
6
- from monoco.core.output import print_output
5
+ from typing import Optional, Any
7
6
  from rich.console import Console
7
+ from rich.syntax import Syntax
8
+ from pydantic import ValidationError
9
+
10
+ from monoco.core.config import (
11
+ get_config,
12
+ MonocoConfig,
13
+ ConfigScope,
14
+ load_raw_config,
15
+ save_raw_config,
16
+ get_config_path
17
+ )
8
18
 
9
19
  app = typer.Typer(help="Manage Monoco configuration")
10
20
  console = Console()
11
21
 
22
+ def _parse_value(value: str) -> Any:
23
+ """Parse string value into appropriate type (bool, int, float, str)."""
24
+ if value.lower() in ("true", "yes", "on"):
25
+ return True
26
+ if value.lower() in ("false", "no", "off"):
27
+ return False
28
+ if value.lower() == "null":
29
+ return None
30
+ try:
31
+ return int(value)
32
+ except ValueError:
33
+ try:
34
+ return float(value)
35
+ except ValueError:
36
+ return value
37
+
38
+ @app.command()
39
+ def show(
40
+ output: str = typer.Option("yaml", "--output", "-o", help="Output format: yaml or json"),
41
+ ):
42
+ """Show the currently active (merged) configuration."""
43
+ config = get_config()
44
+ # Pydantic v1/v2 compat: use dict() or model_dump()
45
+ data = config.dict()
46
+
47
+ if output == "json":
48
+ print(json.dumps(data, indent=2))
49
+ else:
50
+ yaml_str = yaml.dump(data, default_flow_style=False)
51
+ syntax = Syntax(yaml_str, "yaml")
52
+ console.print(syntax)
53
+
12
54
  @app.command()
13
- def show():
14
- """Show current configuration."""
15
- settings = get_config()
16
- print_output(settings, title="Current Configuration")
55
+ def get(key: str = typer.Argument(..., help="Configuration key (e.g. project.name)")):
56
+ """Get a specific configuration value."""
57
+ config = get_config()
58
+ data = config.dict()
59
+
60
+ parts = key.split(".")
61
+ current = data
62
+
63
+ for part in parts:
64
+ if isinstance(current, dict) and part in current:
65
+ current = current[part]
66
+ else:
67
+ console.print(f"[red]Key '{key}' not found.[/red]")
68
+ raise typer.Exit(code=1)
69
+
70
+ if isinstance(current, (dict, list)):
71
+ if isinstance(current, dict):
72
+ print(yaml.dump(current, default_flow_style=False))
73
+ else:
74
+ print(json.dumps(current))
75
+ else:
76
+ print(current)
17
77
 
18
78
  @app.command(name="set")
19
79
  def set_val(
20
80
  key: str = typer.Argument(..., help="Config key (e.g. telemetry.enabled)"),
21
81
  value: str = typer.Argument(..., help="Value to set"),
22
- scope: str = typer.Option("global", "--scope", "-s", help="Configuration scope: global or project")
82
+ global_scope: bool = typer.Option(False, "--global", "-g", help="Update global configuration"),
23
83
  ):
24
- """Set a configuration value."""
25
- # This is a simplified implementation of config setting
26
- # In a real system, we'd want to validate the key against the schema
84
+ """Set a configuration value in specific scope (project by default)."""
85
+ scope = ConfigScope.GLOBAL if global_scope else ConfigScope.PROJECT
27
86
 
28
- if scope == "global":
29
- config_path = Path.home() / ".monoco" / "config.yaml"
30
- else:
31
- # Check project root
32
- cwd = Path.cwd()
33
- config_path = cwd / ".monoco" / "config.yaml"
34
- if not (cwd / ".monoco").exists():
35
- config_path = cwd / "monoco.yaml"
36
-
37
- config_data = {}
38
- if config_path.exists():
39
- with open(config_path, "r") as f:
40
- config_data = yaml.safe_load(f) or {}
41
-
42
- # Simple nested key support (e.g. telemetry.enabled)
87
+ # 1. Load Raw Config for the target scope
88
+ raw_data = load_raw_config(scope)
89
+
90
+ # 2. Parse Key & Update Data
43
91
  parts = key.split(".")
44
- target = config_data
45
- for part in parts[:-1]:
92
+ target = raw_data
93
+
94
+ # Context management for nested updates
95
+ for i, part in enumerate(parts[:-1]):
46
96
  if part not in target:
47
97
  target[part] = {}
48
98
  target = target[part]
99
+ if not isinstance(target, dict):
100
+ parent_key = ".".join(parts[:i+1])
101
+ console.print(f"[red]Cannot set '{key}': '{parent_key}' is not a dictionary ({type(target)}).[/red]")
102
+ raise typer.Exit(code=1)
103
+
104
+ parsed_val = _parse_value(value)
105
+ target[parts[-1]] = parsed_val
49
106
 
50
- # Type conversion
51
- if value.lower() in ("true", "yes", "on"):
52
- val = True
53
- elif value.lower() in ("false", "no", "off"):
54
- val = False
55
- else:
56
- try:
57
- val = int(value)
58
- except ValueError:
59
- val = value
60
-
61
- target[parts[-1]] = val
107
+ # 3. Validate against Schema
108
+ # We simulate a full load by creating a temporary MonocoConfig with these overrides.
109
+ # Note: This validation is "active" - we want to ensure the resulting config WOULD be valid.
110
+ # However, raw_data is partial. Pydantic models with defaults will accept partials.
111
+ try:
112
+ # We can try to validate just the relevant model part if we knew which one it was.
113
+ # But simpler is to check if MonocoConfig accepts this structure.
114
+ MonocoConfig(**raw_data)
115
+ except ValidationError as e:
116
+ console.print(f"[red]Validation failed for key '{key}':[/red]")
117
+ console.print(e)
118
+ raise typer.Exit(code=1)
119
+ except Exception as e:
120
+ console.print(f"[red]Unexpected validation error: {e}[/red]")
121
+ raise typer.Exit(code=1)
62
122
 
63
- config_path.parent.mkdir(parents=True, exist_ok=True)
64
- with open(config_path, "w") as f:
65
- yaml.dump(config_data, f, default_flow_style=False)
123
+ # 4. Save
124
+ save_raw_config(scope, raw_data)
66
125
 
67
- console.print(f"[green]✓ Set {key} = {val} in {scope} config.[/green]")
126
+ scope_display = "Global" if global_scope else "Project"
127
+ console.print(f"[green]✓ Set {key} = {parsed_val} in {scope_display} config.[/green]")
68
128
 
69
129
  if __name__ == "__main__":
70
130
  app()
@@ -0,0 +1,29 @@
1
+ from pathlib import Path
2
+ from typing import Dict
3
+ from monoco.core.feature import MonocoFeature, IntegrationData
4
+ from monoco.features.i18n import core
5
+
6
+ class I18nFeature(MonocoFeature):
7
+ @property
8
+ def name(self) -> str:
9
+ return "i18n"
10
+
11
+ def initialize(self, root: Path, config: Dict) -> None:
12
+ core.init(root)
13
+
14
+ def integrate(self, root: Path, config: Dict) -> IntegrationData:
15
+ # Determine language from config, default to 'en'
16
+ lang = config.get("i18n", {}).get("source_lang", "en")
17
+ base_dir = Path(__file__).parent / "resources"
18
+
19
+ prompt_file = base_dir / lang / "AGENTS.md"
20
+ if not prompt_file.exists():
21
+ prompt_file = base_dir / "en" / "AGENTS.md"
22
+
23
+ content = ""
24
+ if prompt_file.exists():
25
+ content = prompt_file.read_text(encoding="utf-8").strip()
26
+
27
+ return IntegrationData(
28
+ system_prompts={"Documentation I18n": content}
29
+ )
@@ -154,25 +154,15 @@ i18n is a "first-class citizen" in Monoco.
154
154
  - Run `monoco i18n scan` to verify coverage.
155
155
  """
156
156
 
157
- PROMPT_CONTENT = """### Documentation I18n
158
- Manage internationalization.
159
- - **Scan**: `monoco i18n scan` (Check for missing translations)
160
- - **Structure**:
161
- - Root files: `FILE_ZH.md`
162
- - Subdirs: `folder/zh/file.md`"""
163
157
 
164
158
  def init(root: Path):
165
159
  """Initialize I18n environment (No-op currently as it relies on config)."""
166
160
  # In future, could generate i18n config section if missing.
167
161
  pass
168
162
 
169
- def get_resources() -> Dict[str, Any]:
170
163
  return {
171
164
  "skills": {
172
165
  "i18n": SKILL_CONTENT
173
166
  },
174
- "prompts": {
175
- "i18n": PROMPT_CONTENT
176
- }
167
+ "prompts": {} # Handled by adapter via resource files
177
168
  }
178
-
@@ -0,0 +1,8 @@
1
+ ### Documentation I18n
2
+
3
+ Manage internationalization.
4
+
5
+ - **Scan**: `monoco i18n scan` (Check for missing translations)
6
+ - **Structure**:
7
+ - Root files: `FILE_ZH.md`
8
+ - Subdirs: `folder/zh/file.md`
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: monoco-i18n
3
+ description: Internationalization quality control for documentation. Ensures multi-language documentation stays synchronized.
4
+ ---
5
+
6
+ # Documentation I18n
7
+
8
+ Manage internationalization for Monoco project documentation.
9
+
10
+ ## Overview
11
+
12
+ The I18n feature provides:
13
+
14
+ - **Automatic scanning** for missing translations
15
+ - **Standardized structure** for multi-language documentation
16
+ - **Quality control** to maintain documentation parity
17
+
18
+ ## Key Commands
19
+
20
+ ### Scan for Missing Translations
21
+
22
+ ```bash
23
+ monoco i18n scan
24
+ ```
25
+
26
+ Scans the project for markdown files and reports missing translations.
27
+
28
+ **Output**:
29
+
30
+ - Lists source files without corresponding translations
31
+ - Shows which target languages are missing
32
+ - Respects `.gitignore` and default exclusions
33
+
34
+ ## Configuration
35
+
36
+ I18n settings are configured in `.monoco/config.yaml`:
37
+
38
+ ```yaml
39
+ i18n:
40
+ source_lang: en # Source language code
41
+ target_langs: # Target language codes
42
+ - zh
43
+ - ja
44
+ ```
45
+
46
+ ## Documentation Structure
47
+
48
+ ### Root Files (Suffix Pattern)
49
+
50
+ For files in the project root:
51
+
52
+ - Source: `README.md`
53
+ - Chinese: `README_ZH.md`
54
+ - Japanese: `README_JA.md`
55
+
56
+ ### Subdirectory Files (Directory Pattern)
57
+
58
+ For files in `docs/` or other directories:
59
+
60
+ ```
61
+ docs/
62
+ ├── en/
63
+ │ ├── guide.md
64
+ │ └── api.md
65
+ ├── zh/
66
+ │ ├── guide.md
67
+ │ └── api.md
68
+ └── ja/
69
+ ├── guide.md
70
+ └── api.md
71
+ ```
72
+
73
+ ## Exclusion Rules
74
+
75
+ The following are automatically excluded from i18n scanning:
76
+
77
+ - `.gitignore` patterns (respected automatically)
78
+ - `.references/` directory
79
+ - Build artifacts (`dist/`, `build/`, `node_modules/`)
80
+ - `Issues/` directory
81
+
82
+ ## Best Practices
83
+
84
+ 1. **Create English First**: Write documentation in the source language first
85
+ 2. **Follow Naming Convention**: Use the appropriate pattern (suffix or directory)
86
+ 3. **Run Scan Regularly**: Use `monoco i18n scan` to verify coverage
87
+ 4. **Commit All Languages**: Keep translations in version control
88
+
89
+ ## Workflow
90
+
91
+ 1. Write documentation in source language (e.g., English)
92
+ 2. Create translation files following the naming convention
93
+ 3. Run `monoco i18n scan` to verify all translations exist
94
+ 4. Fix any missing translations reported by the scan
@@ -0,0 +1,8 @@
1
+ ### 文档国际化
2
+
3
+ 管理国际化。
4
+
5
+ - **扫描**: `monoco i18n scan` (检查缺失的翻译)
6
+ - **结构**:
7
+ - 根文件: `FILE_ZH.md`
8
+ - 子目录: `folder/zh/file.md`