Skip to content

Commit 60c7157

Browse files
authored
Merge pull request #545 from lbedner/mandarin-support
Mandarin Support
2 parents 23fe3a8 + 1e0752e commit 60c7157

20 files changed

Lines changed: 1053 additions & 289 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ uvx aegis-stack init task-processor --components scheduler,worker
4747
cd my-api && make serve
4848
```
4949

50+
> **CLI language support:** Use `aegis --lang zh` for Simplified Chinese (简体中文), or set `AEGIS_LANG=zh`.
51+
5052
**Installation alternatives:** See the [Installation Guide](https://lbedner.github.io/aegis-stack/installation/) for `uv tool install`, `pip install`, and development setup.
5153

5254
## Overseer - Built-In System Visibility

aegis/__main__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
from .commands.update import update_command
3535
from .commands.version import version_command
3636
from .core.verbosity import set_verbose
37+
from .i18n import detect_locale, set_locale
38+
from .i18n.locales import AVAILABLE_LOCALES
3739

3840
# Create the main Typer application
3941
app = typer.Typer(
@@ -60,9 +62,23 @@ def main(
6062
"-v",
6163
help="Enable verbose output (show detailed file operations)",
6264
),
65+
lang: str | None = typer.Option(
66+
None,
67+
"--lang",
68+
help="Output language (en, zh). Default: auto-detect from AEGIS_LANG or system locale",
69+
envvar="AEGIS_LANG",
70+
),
6371
) -> None:
6472
"""Aegis Stack CLI - Global options and configuration."""
6573
set_verbose(verbose)
74+
if lang and lang.lower() not in AVAILABLE_LOCALES:
75+
typer.secho(
76+
f"Unsupported language '{lang}'. Available: {', '.join(sorted(AVAILABLE_LOCALES))}",
77+
fg="red",
78+
err=True,
79+
)
80+
raise typer.Exit(1)
81+
set_locale(lang if lang else detect_locale())
6682

6783

6884
# Register commands

aegis/cli/interactive.py

Lines changed: 170 additions & 110 deletions
Large diffs are not rendered by default.

aegis/cli/validators.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import typer
99

10+
from ..i18n import t
11+
1012

1113
def validate_project_name(project_name: str) -> None:
1214
"""Validate project name and raise typer.Exit if invalid."""
@@ -15,23 +17,18 @@ def validate_project_name(project_name: str) -> None:
1517
# Check for invalid characters (only allow letters, numbers, hyphens,
1618
# underscores)
1719
if not re.match(r"^[a-zA-Z0-9_-]+$", project_name):
18-
typer.secho(
19-
"Invalid project name. Only letters, numbers, hyphens, and "
20-
"underscores are allowed.",
21-
fg="red",
22-
err=True,
23-
)
20+
typer.secho(t("validation.invalid_name"), fg="red", err=True)
2421
raise typer.Exit(1)
2522

2623
# Check for reserved names
2724
reserved_names = {"aegis", "aegis-stack"}
2825
if project_name.lower() in reserved_names:
29-
typer.secho(f"'{project_name}' is a reserved name.", fg="red", err=True)
26+
typer.secho(
27+
t("validation.reserved_name", name=project_name), fg="red", err=True
28+
)
3029
raise typer.Exit(1)
3130

3231
# Check length limit
3332
if len(project_name) > 50:
34-
typer.secho(
35-
"Project name too long. Maximum 50 characters allowed.", fg="red", err=True
36-
)
33+
typer.secho(t("validation.name_too_long"), fg="red", err=True)
3734
raise typer.Exit(1)

aegis/commands/add_service.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def add_service_command(
174174
# AI service without bracket syntax - prompt for configuration
175175
from ..cli.interactive import interactive_ai_service_config
176176

177-
backend, framework, providers, rag_enabled = (
177+
backend, framework, providers, rag_enabled, voice_enabled = (
178178
interactive_ai_service_config(base_service)
179179
)
180180

@@ -183,11 +183,14 @@ def add_service_command(
183183
ai_config["framework"] = framework
184184
ai_config["providers"] = providers
185185
ai_config["rag_enabled"] = rag_enabled
186+
ai_config["voice_enabled"] = voice_enabled
186187

187188
# Build bracket syntax and update the service entry
188189
options = [backend, framework] + providers
189190
if rag_enabled:
190191
options.append("rag")
192+
if voice_enabled:
193+
options.append("voice")
191194
service_string = f"{base_service}[{','.join(options)}]"
192195
services_to_add[i] = service_string
193196

aegis/commands/init.py

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from ..core.dependency_resolver import DependencyResolver
3131
from ..core.service_resolver import ServiceResolver
3232
from ..core.template_generator import TemplateGenerator
33+
from ..i18n import t
3334

3435
# Build services help text dynamically from constants
3536
_SERVICES_HELP = (
@@ -114,39 +115,39 @@ def init_command(
114115
# Validate Python version
115116
if python_version not in SUPPORTED_PYTHON_VERSIONS:
116117
typer.secho(
117-
f"Invalid Python version '{python_version}'. Must be one of: {', '.join(SUPPORTED_PYTHON_VERSIONS)}",
118+
t(
119+
"validation.invalid_python",
120+
version=python_version,
121+
supported=", ".join(SUPPORTED_PYTHON_VERSIONS),
122+
),
118123
fg="red",
119124
err=True,
120125
)
121126
raise typer.Exit(1)
122127

123-
typer.secho("Aegis Stack Project Initialization", fg=typer.colors.BLUE, bold=True)
128+
typer.secho(t("init.title"), fg=typer.colors.BLUE, bold=True)
124129

125130
# Determine output directory
126131
base_output_dir = Path(output_dir) if output_dir else Path.cwd()
127132
project_path = base_output_dir / project_name
128133

129134
typer.echo(
130-
f"{typer.style('Location:', fg=typer.colors.CYAN)} {project_path.resolve()}"
135+
f"{typer.style(t('init.location'), fg=typer.colors.CYAN)} {project_path.resolve()}"
131136
)
132137

133138
if to_version:
134139
typer.echo(
135-
f"{typer.style('Template Version:', fg=typer.colors.CYAN)} {to_version}"
140+
f"{typer.style(t('init.template_version'), fg=typer.colors.CYAN)} {to_version}"
136141
)
137142

138143
# Check if directory already exists
139144
if project_path.exists():
140145
if not force:
141-
typer.secho(
142-
f"Directory '{project_path}' already exists", fg="red", err=True
143-
)
144-
typer.echo(
145-
" Use --force to overwrite or choose a different name", err=True
146-
)
146+
typer.secho(t("init.dir_exists", path=project_path), fg="red", err=True)
147+
typer.echo(f" {t('init.dir_exists_hint')}", err=True)
147148
raise typer.Exit(1)
148149
else:
149-
typer.secho(f"Overwriting existing directory: {project_path}", fg="yellow")
150+
typer.secho(t("init.overwriting", path=project_path), fg="yellow")
150151

151152
# Interactive component selection
152153
# Note: components is list[str] after callback, despite str annotation
@@ -168,9 +169,7 @@ def init_command(
168169
selected_services, components_for_validation
169170
)
170171
if errors:
171-
typer.secho(
172-
"Service-component compatibility errors:", fg="red", err=True
173-
)
172+
typer.secho(t("init.compat_errors"), fg="red", err=True)
174173
for error in errors:
175174
typer.echo(f" • {error}", err=True)
176175

@@ -182,15 +181,20 @@ def init_command(
182181
)
183182
if missing_components:
184183
typer.echo(
185-
f"Suggestion: Add missing components --components {','.join(sorted(set(selected_components + missing_components)))}",
184+
t(
185+
"init.suggestion_add",
186+
components=",".join(
187+
sorted(set(selected_components + missing_components))
188+
),
189+
),
186190
err=True,
187191
)
188192
typer.echo(
189-
" Or remove --components to let services auto-add dependencies.",
193+
f" {t('init.suggestion_remove')}",
190194
err=True,
191195
)
192196
typer.echo(
193-
" Alternatively, use interactive mode to auto-add service dependencies.",
197+
f" {t('init.suggestion_interactive')}",
194198
err=True,
195199
)
196200
raise typer.Exit(1)
@@ -201,7 +205,10 @@ def init_command(
201205
)
202206
if service_components:
203207
typer.secho(
204-
f"Services require components: {', '.join(sorted(service_components))}",
208+
t(
209+
"init.services_require",
210+
components=", ".join(sorted(service_components)),
211+
),
205212
fg=typer.colors.YELLOW,
206213
)
207214
selected_components = service_components
@@ -219,7 +226,7 @@ def init_command(
219226
scheduler_backend = detect_scheduler_backend(selected_components)
220227
if scheduler_backend != StorageBackends.MEMORY:
221228
typer.secho(
222-
f"Auto-detected: Scheduler with {scheduler_backend} persistence",
229+
t("init.auto_detected_scheduler", backend=scheduler_backend),
223230
fg=typer.colors.YELLOW,
224231
)
225232

@@ -255,7 +262,7 @@ def init_command(
255262
)
256263
if auto_added:
257264
typer.secho(
258-
f"\nAuto-added dependencies: {', '.join(auto_added)}",
265+
"\n" + t("init.auto_added_deps", deps=", ".join(auto_added)),
259266
fg=typer.colors.YELLOW,
260267
)
261268

@@ -294,11 +301,14 @@ def init_command(
294301
service_component_map[comp] = []
295302
service_component_map[comp].append(service_name)
296303

297-
typer.secho("\nAuto-added by services:", fg=typer.colors.YELLOW)
304+
typer.secho(
305+
"\n" + t("init.auto_added_by_services"),
306+
fg=typer.colors.YELLOW,
307+
)
298308
for comp, requiring_services in service_component_map.items():
299309
services_str = ", ".join(requiring_services)
300310
typer.echo(
301-
f" • {comp} {typer.style(f'(required by {services_str})', dim=True)}"
311+
f" • {comp} {typer.style(t('init.required_by', services=services_str), dim=True)}"
302312
)
303313

304314
# Create template generator with scheduler backend context
@@ -312,10 +322,12 @@ def init_command(
312322

313323
# Show selected configuration
314324
typer.echo()
315-
typer.secho("Project Configuration", fg=typer.colors.CYAN, bold=True)
316-
typer.echo(f" {typer.style('Name:', fg=typer.colors.CYAN)} {project_name}")
325+
typer.secho(t("init.config_title"), fg=typer.colors.CYAN, bold=True)
326+
typer.echo(
327+
f" {typer.style(t('init.config_name'), fg=typer.colors.CYAN)} {project_name}"
328+
)
317329
typer.echo(
318-
f" {typer.style('Core:', fg=typer.colors.CYAN)} {', '.join(CORE_COMPONENTS)}"
330+
f" {typer.style(t('init.config_core'), fg=typer.colors.CYAN)} {', '.join(CORE_COMPONENTS)}"
319331
)
320332

321333
# Show infrastructure components
@@ -331,60 +343,60 @@ def init_command(
331343

332344
if infra_components:
333345
typer.echo(
334-
f" {typer.style('Infrastructure:', fg=typer.colors.CYAN)} {', '.join(infra_components)}"
346+
f" {typer.style(t('init.config_infra'), fg=typer.colors.CYAN)} {', '.join(infra_components)}"
335347
)
336348

337349
# Show selected services
338350
if selected_services:
339351
typer.echo(
340-
f" {typer.style('Services:', fg=typer.colors.CYAN)} {', '.join(selected_services)}"
352+
f" {typer.style(t('init.config_services'), fg=typer.colors.CYAN)} {', '.join(selected_services)}"
341353
)
342354

343355
# Show template files that will be generated
344356
template_files = template_gen.get_template_files()
345357
if template_files:
346-
typer.secho("\nComponent Files:", fg=typer.colors.CYAN, bold=True)
358+
typer.secho("\n" + t("init.component_files"), fg=typer.colors.CYAN, bold=True)
347359
for file_path in template_files:
348360
typer.echo(f" • {file_path}")
349361

350362
# Show entrypoints that will be created
351363
entrypoints = template_gen.get_entrypoints()
352364
if entrypoints:
353-
typer.secho("\nEntrypoints:", fg=typer.colors.CYAN, bold=True)
365+
typer.secho("\n" + t("init.entrypoints"), fg=typer.colors.CYAN, bold=True)
354366
for entrypoint in entrypoints:
355367
typer.echo(f" • {entrypoint}")
356368

357369
# Show worker queues that will be created
358370
worker_queues = template_gen.get_worker_queues()
359371
if worker_queues:
360-
typer.secho("\nWorker Queues:", fg=typer.colors.CYAN, bold=True)
372+
typer.secho("\n" + t("init.worker_queues"), fg=typer.colors.CYAN, bold=True)
361373
for queue in worker_queues:
362374
typer.echo(f" • {queue}")
363375

364376
# Show dependency information using template generator
365377
deps = template_gen._get_pyproject_deps()
366378
if deps:
367-
typer.secho("\nDependencies to be installed:", fg=typer.colors.CYAN, bold=True)
379+
typer.secho("\n" + t("init.dependencies"), fg=typer.colors.CYAN, bold=True)
368380
for dep in deps:
369381
typer.echo(f" • {dep}")
370382

371383
# Confirm before proceeding
372384
typer.echo()
373-
if not yes and not typer.confirm("Create this project?", default=True):
374-
typer.secho("Project creation cancelled", fg="red")
385+
if not yes and not typer.confirm(t("init.confirm_create"), default=True):
386+
typer.secho(t("init.cancelled"), fg="red")
375387
raise typer.Exit(0)
376388

377389
# Handle force overwrite by completely removing existing directory
378390
project_path = base_output_dir / project_name
379391
if force and project_path.exists():
380-
typer.echo(f"Removing existing directory: {project_path}")
392+
typer.echo(t("init.removing_dir", path=project_path))
381393
import shutil
382394

383395
shutil.rmtree(project_path)
384396

385397
# Create project using Copier template engine
386398
typer.echo()
387-
typer.secho(f"Creating project: {project_name}", fg=typer.colors.BLUE, bold=True)
399+
typer.secho(t("init.creating", name=project_name), fg=typer.colors.BLUE, bold=True)
388400

389401
try:
390402
from ..core.copier_manager import generate_with_copier
@@ -401,5 +413,5 @@ def init_command(
401413
# which provides better status reporting and automated setup
402414

403415
except Exception as e:
404-
typer.secho(f"Error creating project: {e}", fg="red", err=True)
416+
typer.secho(t("init.error", error=e), fg="red", err=True)
405417
raise typer.Exit(1)

aegis/core/copier_manager.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from packaging.version import Version
1616

1717
from aegis import __version__
18+
from aegis.i18n import t
1819

1920
from ..config.defaults import (
2021
DEFAULT_PYTHON_VERSION,
@@ -326,12 +327,11 @@ def generate_with_copier(
326327

327328
# Show docs/star links
328329
typer.echo()
329-
typer.secho("Docs: https://lbedner.github.io/aegis-stack", dim=True)
330+
typer.secho(t("postgen.docs_link"), dim=True)
330331
typer.echo()
331332
star = typer.style("\u2605", fg=typer.colors.BRIGHT_YELLOW, bold=True)
332333
typer.echo(
333-
f"{star} If Aegis Stack made your life easier, consider leaving a star:\n"
334-
" https://github.com/lbedner/aegis-stack"
334+
f"{star} {t('postgen.star_prompt')}\n https://github.com/lbedner/aegis-stack"
335335
)
336336

337337
# CRITICAL: Fix _src_path in .copier-answers.yml for future updates to work

0 commit comments

Comments
 (0)