ruyi 0.44.0b20251219__py3-none-any.whl → 0.45.0__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 (53) hide show
  1. ruyi/__main__.py +16 -4
  2. ruyi/cli/cmd.py +6 -5
  3. ruyi/cli/config_cli.py +14 -11
  4. ruyi/cli/main.py +14 -4
  5. ruyi/cli/oobe.py +7 -3
  6. ruyi/cli/self_cli.py +48 -34
  7. ruyi/cli/user_input.py +42 -12
  8. ruyi/cli/version_cli.py +11 -5
  9. ruyi/config/__init__.py +26 -2
  10. ruyi/config/errors.py +19 -7
  11. ruyi/device/provision.py +116 -55
  12. ruyi/device/provision_cli.py +6 -3
  13. ruyi/i18n/__init__.py +129 -0
  14. ruyi/log/__init__.py +6 -5
  15. ruyi/mux/runtime.py +19 -6
  16. ruyi/mux/venv/maker.py +93 -35
  17. ruyi/mux/venv/venv_cli.py +13 -10
  18. ruyi/pluginhost/plugin_cli.py +4 -3
  19. ruyi/resource_bundle/__init__.py +22 -8
  20. ruyi/resource_bundle/__main__.py +6 -5
  21. ruyi/resource_bundle/data.py +13 -9
  22. ruyi/ruyipkg/admin_checksum.py +4 -1
  23. ruyi/ruyipkg/admin_cli.py +9 -6
  24. ruyi/ruyipkg/augmented_pkg.py +15 -14
  25. ruyi/ruyipkg/checksum.py +8 -2
  26. ruyi/ruyipkg/distfile.py +33 -9
  27. ruyi/ruyipkg/entity.py +12 -2
  28. ruyi/ruyipkg/entity_cli.py +20 -12
  29. ruyi/ruyipkg/entity_provider.py +11 -2
  30. ruyi/ruyipkg/fetcher.py +38 -9
  31. ruyi/ruyipkg/install.py +143 -42
  32. ruyi/ruyipkg/install_cli.py +18 -15
  33. ruyi/ruyipkg/list.py +27 -20
  34. ruyi/ruyipkg/list_cli.py +12 -7
  35. ruyi/ruyipkg/news.py +23 -11
  36. ruyi/ruyipkg/news_cli.py +10 -7
  37. ruyi/ruyipkg/profile_cli.py +8 -2
  38. ruyi/ruyipkg/repo.py +22 -8
  39. ruyi/ruyipkg/unpack.py +42 -8
  40. ruyi/ruyipkg/unpack_method.py +5 -1
  41. ruyi/ruyipkg/update_cli.py +8 -3
  42. ruyi/telemetry/provider.py +74 -29
  43. ruyi/telemetry/telemetry_cli.py +9 -8
  44. ruyi/utils/git.py +18 -11
  45. ruyi/utils/prereqs.py +10 -5
  46. ruyi/utils/ssl_patch.py +2 -1
  47. ruyi/version.py +9 -3
  48. {ruyi-0.44.0b20251219.dist-info → ruyi-0.45.0.dist-info}/METADATA +2 -1
  49. ruyi-0.45.0.dist-info/RECORD +103 -0
  50. {ruyi-0.44.0b20251219.dist-info → ruyi-0.45.0.dist-info}/WHEEL +1 -1
  51. ruyi-0.44.0b20251219.dist-info/RECORD +0 -102
  52. {ruyi-0.44.0b20251219.dist-info → ruyi-0.45.0.dist-info}/entry_points.txt +0 -0
  53. {ruyi-0.44.0b20251219.dist-info → ruyi-0.45.0.dist-info}/licenses/LICENSE-Apache.txt +0 -0
ruyi/i18n/__init__.py ADDED
@@ -0,0 +1,129 @@
1
+ from io import BytesIO
2
+ import gettext
3
+ import os
4
+ import sys
5
+ from typing import Final, Mapping, NewType
6
+
7
+ if sys.version_info >= (3, 11):
8
+ from typing import LiteralString
9
+ else:
10
+ # It may happen that Python and typing_extensions are both too old, which
11
+ # is unfortunately the case with Ubuntu 22.04 LTS system packages, meaning
12
+ # typing_extensions cannot guarantee us LiteralString either.
13
+ #
14
+ # We don't expect development work within such an environment, so just
15
+ # alias to str to avoid importing typing_extensions altogether. This also
16
+ # helps CLI startup performance.
17
+ #
18
+ # Unfortunately, simply assigning str to LiteralString would not work either,
19
+ # due to mypy/pyright not wanting us to re-assign types; we have to
20
+ # resort to providing different function signatures for Python 3.10, which
21
+ # is done below.
22
+ #
23
+ # LiteralString = str # type: ignore[misc]
24
+ pass
25
+
26
+ from ..resource_bundle import get_resource_blob
27
+
28
+
29
+ def _probe_lang(environ: Mapping[str, str]) -> list[str]:
30
+ """Probe the environment variables the gettext way, to determine the list
31
+ of preferred languages."""
32
+ languages: list[str] = []
33
+ # check the variables in this order
34
+ for envar in ("LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"):
35
+ if val := environ.get(envar):
36
+ languages = val.split(":")
37
+ break
38
+ if "C" not in languages:
39
+ languages.append("C")
40
+
41
+ for i, lang in enumerate(languages):
42
+ # normalize things like en_US.UTF-8 to en_US
43
+ if "." in lang:
44
+ languages[i] = lang.split(".", 1)[0]
45
+
46
+ return languages
47
+
48
+
49
+ _DOMAINS = (
50
+ "argparse",
51
+ "ruyi",
52
+ )
53
+ """gettext domains we supply and use ourselves"""
54
+
55
+
56
+ class I18nAdapter:
57
+ """Adapter for gettext translation functions."""
58
+
59
+ def __init__(self) -> None:
60
+ self._t = gettext.NullTranslations()
61
+
62
+ def hook(self) -> None:
63
+ # monkey-patch the global gettext functions
64
+ # the type ignore comments are necessary because mypy doesn't see
65
+ # the bounded methods as compatible with the unbound functions
66
+ # (it doesn't remove self from the unbound method signature)
67
+ gettext.gettext = self.gettext # type: ignore[assignment]
68
+ gettext.ngettext = self.ngettext # type: ignore[assignment]
69
+
70
+ def init_from_env(self, environ: Mapping[str, str] | None = None) -> None:
71
+ if environ is None:
72
+ environ = os.environ
73
+
74
+ langs = _probe_lang(environ)
75
+ for domain in _DOMAINS:
76
+ for lang in langs:
77
+ if self.set_locale(domain, lang):
78
+ break
79
+
80
+ def _get_mo(self, domain: str, locale: str) -> BytesIO | None:
81
+ # this is always forward-slash-separated, because this is not a concrete
82
+ # filesystem path, rather a resource bundle key
83
+ path = f"locale/{locale}/LC_MESSAGES/{domain}.mo"
84
+ blob = get_resource_blob(path)
85
+ if blob:
86
+ return BytesIO(blob)
87
+ return None
88
+
89
+ def set_locale(self, domain: str, locale: str | None = None) -> bool:
90
+ if locale is not None:
91
+ if mo_file := self._get_mo(domain, locale):
92
+ self._t.add_fallback(gettext.GNUTranslations(mo_file))
93
+ return True
94
+ return False
95
+
96
+ def gettext(self, x: str) -> str:
97
+ return self._t.gettext(x)
98
+
99
+ def ngettext(self, singular: str, plural: str, n: int) -> str:
100
+ return self._t.ngettext(singular, plural, n)
101
+
102
+
103
+ ADAPTER: Final = I18nAdapter()
104
+
105
+
106
+ DeferredI18nString = NewType("DeferredI18nString", str)
107
+
108
+
109
+ if sys.version_info >= (3, 11):
110
+
111
+ def _(x: LiteralString | DeferredI18nString) -> str:
112
+ """``gettext`` alias that ensures its input is string literal via type
113
+ signature."""
114
+ return ADAPTER.gettext(x)
115
+
116
+ def d_(x: LiteralString) -> DeferredI18nString:
117
+ """Mark a string literal for deferred translation: call ``_`` at use sites."""
118
+ return DeferredI18nString(x)
119
+
120
+ else:
121
+
122
+ def _(x: str | DeferredI18nString) -> str:
123
+ """``gettext`` alias that ensures its input is string literal via type
124
+ signature."""
125
+ return ADAPTER.gettext(x)
126
+
127
+ def d_(x: str) -> DeferredI18nString:
128
+ """Mark a string literal for deferred translation: call ``_`` at use sites."""
129
+ return DeferredI18nString(x)
ruyi/log/__init__.py CHANGED
@@ -11,6 +11,7 @@ if TYPE_CHECKING:
11
11
  from rich.console import Console, RenderableType
12
12
  from rich.text import Text
13
13
 
14
+ from ..i18n import _
14
15
  from ..utils.global_mode import ProvidesGlobalMode
15
16
  from ..utils.porcelain import PorcelainEntity, PorcelainEntityType, PorcelainOutput
16
17
 
@@ -217,7 +218,7 @@ class RuyiConsoleLogger(RuyiLogger):
217
218
  return self._emit_porcelain_log("F", message, sep, *objects)
218
219
 
219
220
  return self.log_console.print(
220
- f"[bold red]fatal error:[/] {message}",
221
+ _("[bold red]fatal error:[/] {message}").format(message=message),
221
222
  *objects,
222
223
  sep=sep,
223
224
  end=end,
@@ -234,7 +235,7 @@ class RuyiConsoleLogger(RuyiLogger):
234
235
  return self._emit_porcelain_log("I", message, sep, *objects)
235
236
 
236
237
  return self.log_console.print(
237
- f"[bold green]info:[/] {message}",
238
+ _("[bold green]info:[/] {message}").format(message=message),
238
239
  *objects,
239
240
  sep=sep,
240
241
  end=end,
@@ -251,7 +252,7 @@ class RuyiConsoleLogger(RuyiLogger):
251
252
  return self._emit_porcelain_log("W", message, sep, *objects)
252
253
 
253
254
  return self.log_console.print(
254
- f"[bold yellow]warn:[/] {message}",
255
+ _("[bold yellow]warn:[/] {message}").format(message=message),
255
256
  *objects,
256
257
  sep=sep,
257
258
  end=end,
@@ -263,10 +264,10 @@ def humanize_list(
263
264
  *,
264
265
  sep: str = ", ",
265
266
  item_color: str | None = None,
266
- empty_prompt: str = "(none)",
267
+ empty_prompt: str | None = None,
267
268
  ) -> str:
268
269
  if not obj:
269
- return empty_prompt
270
+ return empty_prompt if empty_prompt is not None else _("(none)")
270
271
  if item_color is None:
271
272
  return sep.join(obj)
272
273
  return sep.join(f"[{item_color}]{x}[/]" for x in obj)
ruyi/mux/runtime.py CHANGED
@@ -5,6 +5,7 @@ import shlex
5
5
  from typing import Final, List, NoReturn
6
6
 
7
7
  from ..config import GlobalConfig
8
+ from ..i18n import _
8
9
  from ..utils.global_mode import ProvidesGlobalMode
9
10
  from .venv_cfg import RuyiVenvConfig
10
11
 
@@ -31,8 +32,8 @@ def mux_main(
31
32
 
32
33
  vcfg = RuyiVenvConfig.load_from_venv(gm, logger)
33
34
  if vcfg is None:
34
- logger.F("the Ruyi toolchain mux is not configured")
35
- logger.I("check out `ruyi venv` for making a virtual environment")
35
+ logger.F(_("the Ruyi toolchain mux is not configured"))
36
+ logger.I(_("check out `ruyi venv` for making a virtual environment"))
36
37
  return 1
37
38
 
38
39
  direct_symlink_target = resolve_direct_symlink_target(gm.argv0, vcfg)
@@ -60,7 +61,11 @@ def mux_main(
60
61
  tgt_data = vcfg.targets.get(target_tuple)
61
62
  if tgt_data is None:
62
63
  logger.F(
63
- f"internal error: no target data for tuple [yellow]{target_tuple}[/]"
64
+ _(
65
+ "internal error: no target data for tuple [yellow]{target_tuple}[/]"
66
+ ).format(
67
+ target_tuple=target_tuple,
68
+ )
64
69
  )
65
70
  return 1
66
71
  toolchain_sysroot = tgt_data.get("toolchain_sysroot")
@@ -83,14 +88,22 @@ def mux_main(
83
88
  if toolchain_bindir is None:
84
89
  # should not happen
85
90
  logger.F(
86
- f"internal error: no bindir configured for target [yellow]{target_tuple}[/]"
91
+ _(
92
+ "internal error: no bindir configured for target [yellow]{target_tuple}[/]"
93
+ ).format(
94
+ target_tuple=target_tuple,
95
+ )
87
96
  )
88
97
  return 1
89
98
 
90
99
  binpath = os.path.join(toolchain_bindir, basename)
91
100
 
92
101
  if target_tuple is None:
93
- logger.F(f"no configured target found for command [yellow]{basename}[/]")
102
+ logger.F(
103
+ _("no configured target found for command [yellow]{basename}[/]").format(
104
+ basename=basename,
105
+ )
106
+ )
94
107
  return 1
95
108
 
96
109
  logger.D(f"binary to exec: {binpath}")
@@ -176,7 +189,7 @@ def mux_qemu_main(
176
189
  logger = gc.logger
177
190
  binpath = vcfg.qemu_bin
178
191
  if binpath is None:
179
- logger.F("this virtual environment has no QEMU-like emulator configured")
192
+ logger.F(_("this virtual environment has no QEMU-like emulator configured"))
180
193
  return 1
181
194
 
182
195
  if vcfg.profile_emu_env is not None:
ruyi/mux/venv/maker.py CHANGED
@@ -7,11 +7,13 @@ import shutil
7
7
  from typing import Any, Final, Iterator, TypedDict
8
8
 
9
9
  from ...config import GlobalConfig
10
+ from ...i18n import _
10
11
  from ...log import RuyiLogger, humanize_list
11
12
  from ...ruyipkg.atom import Atom
12
13
  from ...ruyipkg.pkg_manifest import BoundPackageManifest, EmulatorProgDecl
13
14
  from ...ruyipkg.profile import ProfileProxy
14
15
  from ...utils.global_mode import ProvidesGlobalMode
16
+ from ...utils.l10n import match_lang_code
15
17
  from ...utils.templating import render_template_str
16
18
  from .emulator_cfg import ResolvedEmulatorProg
17
19
 
@@ -67,7 +69,7 @@ def do_make_venv(
67
69
  # this should come after implementation of local state cache
68
70
  if tc_atoms_str is None:
69
71
  logger.F(
70
- "You have to specify at least one toolchain atom for now, e.g. [yellow]`-t gnu-plct`[/]"
72
+ _("You have to specify at least one toolchain atom for now, e.g. [yellow]`-t gnu-plct`[/]")
71
73
  )
72
74
  return 1
73
75
 
@@ -75,7 +77,7 @@ def do_make_venv(
75
77
 
76
78
  profile = mr.get_profile(profile_name)
77
79
  if profile is None:
78
- logger.F(f"profile '{profile_name}' not found")
80
+ logger.F(_("profile '{profile}' not found").format(profile=profile_name))
79
81
  return 1
80
82
 
81
83
  target_arch = ""
@@ -93,32 +95,46 @@ def do_make_venv(
93
95
  tc_atom = Atom.parse(tc_atom_str)
94
96
  tc_pm = tc_atom.match_in_repo(mr, config.include_prereleases)
95
97
  if tc_pm is None:
96
- logger.F(f"cannot match a toolchain package with [yellow]{tc_atom_str}[/]")
98
+ logger.F(_("cannot match a toolchain package with [yellow]{atom}[/]").format(
99
+ atom=tc_atom_str,
100
+ ))
97
101
  return 1
98
102
 
99
103
  if tc_pm.toolchain_metadata is None:
100
- logger.F(f"the package [yellow]{tc_atom_str}[/] is not a toolchain")
104
+ logger.F(_("the package [yellow]{atom}[/] is not a toolchain").format(
105
+ atom=tc_atom_str,
106
+ ))
101
107
  return 1
102
108
 
103
109
  if not tc_pm.toolchain_metadata.satisfies_quirk_set(profile.need_quirks):
104
- logger.F(
105
- f"the package [yellow]{tc_atom_str}[/] does not support all necessary features for the profile [yellow]{profile_name}[/]"
110
+ logger.F(_(
111
+ "the package [yellow]{atom}[/] does not support all necessary features for the profile [yellow]{profile}[/]"
112
+ ).format(
113
+ atom=tc_atom_str,
114
+ profile=profile_name,
115
+ )
106
116
  )
107
117
  logger.I(
108
- f"quirks needed by profile: {humanize_list(profile.need_quirks, item_color='cyan')}"
118
+ _("quirks needed by profile: {humanized_list}").format(
119
+ humanized_list=humanize_list(profile.need_quirks, item_color='cyan'),
120
+ )
109
121
  )
110
122
  logger.I(
111
- f"quirks provided by package: {humanize_list(tc_pm.toolchain_metadata.quirks, item_color='yellow')}"
123
+ _("quirks provided by package: {humanized_list}").format(
124
+ humanized_list=humanize_list(tc_pm.toolchain_metadata.quirks, item_color='yellow'),
125
+ )
112
126
  )
113
127
  return 1
114
128
 
115
129
  target_tuple = tc_pm.toolchain_metadata.target
116
130
  if target_tuple in seen_target_tuples:
117
131
  logger.F(
118
- f"the target tuple [yellow]{target_tuple}[/] is already covered by one of the requested toolchains"
132
+ _("the target tuple [yellow]{target_tuple}[/] is already covered by one of the requested toolchains").format(
133
+ target_tuple=target_tuple,
134
+ )
119
135
  )
120
136
  logger.I(
121
- "for now, only toolchains with differing target tuples can co-exist in one virtual environment"
137
+ _("for now, only toolchains with differing target tuples can co-exist in one virtual environment")
122
138
  )
123
139
  return 1
124
140
 
@@ -127,7 +143,7 @@ def do_make_venv(
127
143
  tc_pm.name_for_installation,
128
144
  )
129
145
  if toolchain_root is None:
130
- logger.F("cannot find the installed directory for the toolchain")
146
+ logger.F(_("cannot find the installed directory for the toolchain"))
131
147
  return 1
132
148
 
133
149
  tc_sysroot_dir: PathLike[Any] | None = None
@@ -139,7 +155,7 @@ def do_make_venv(
139
155
  else:
140
156
  if sysroot_atom_str is None:
141
157
  logger.F(
142
- "sysroot is requested but the toolchain package does not include one, and [yellow]--sysroot-from[/] is not given"
158
+ _("sysroot is requested but the toolchain package does not include one, and [yellow]--sysroot-from[/] is not given")
143
159
  )
144
160
  return 1
145
161
 
@@ -150,13 +166,17 @@ def do_make_venv(
150
166
  gcc_pkg_pm = gcc_pkg_atom.match_in_repo(mr, config.include_prereleases)
151
167
  if gcc_pkg_pm is None:
152
168
  logger.F(
153
- f"cannot match a toolchain package with [yellow]{sysroot_atom_str}[/]"
169
+ _("cannot match a toolchain package with [yellow]{atom}[/]").format(
170
+ atom=sysroot_atom_str,
171
+ )
154
172
  )
155
173
  return 1
156
174
 
157
175
  if gcc_pkg_pm.toolchain_metadata is None:
158
176
  logger.F(
159
- f"the package [yellow]{sysroot_atom_str}[/] is not a toolchain"
177
+ _("the package [yellow]{atom}[/] is not a toolchain").format(
178
+ atom=sysroot_atom_str,
179
+ )
160
180
  )
161
181
  return 1
162
182
 
@@ -166,14 +186,16 @@ def do_make_venv(
166
186
  )
167
187
  if gcc_pkg_root is None:
168
188
  logger.F(
169
- "cannot find the installed directory for the sysroot package"
189
+ _("cannot find the installed directory for the sysroot package")
170
190
  )
171
191
  return 1
172
192
 
173
193
  tc_sysroot_relpath = gcc_pkg_pm.toolchain_metadata.included_sysroot
174
194
  if tc_sysroot_relpath is None:
175
195
  logger.F(
176
- f"sysroot is requested but the package [yellow]{sysroot_atom_str}[/] does not contain one"
196
+ _("sysroot is requested but the package [yellow]{atom}[/] does not contain one").format(
197
+ atom=sysroot_atom_str,
198
+ )
177
199
  )
178
200
  return 1
179
201
 
@@ -191,7 +213,7 @@ def do_make_venv(
191
213
  # for now, require this directory to be present (or clang would barely work)
192
214
  if gcc_install_dir is None:
193
215
  logger.F(
194
- "cannot find a GCC include & lib directory in the sysroot package"
216
+ _("cannot find a GCC include & lib directory in the sysroot package")
195
217
  )
196
218
  return 1
197
219
 
@@ -228,9 +250,11 @@ def do_make_venv(
228
250
  warn_differing_target_arch = True
229
251
 
230
252
  if warn_differing_target_arch:
231
- logger.W("multiple toolchains specified with differing target architecture")
253
+ logger.W(_("multiple toolchains specified with differing target architecture"))
232
254
  logger.I(
233
- f"using the target architecture of the first toolchain: [yellow]{target_arch}[/]"
255
+ _("using the target architecture of the first toolchain: [yellow]{arch}[/]").format(
256
+ arch=target_arch,
257
+ )
234
258
  )
235
259
 
236
260
  # Now handle the emulator.
@@ -240,17 +264,24 @@ def do_make_venv(
240
264
  emu_atom = Atom.parse(emu_atom_str)
241
265
  emu_pm = emu_atom.match_in_repo(mr, config.include_prereleases)
242
266
  if emu_pm is None:
243
- logger.F(f"cannot match an emulator package with [yellow]{emu_atom_str}[/]")
267
+ logger.F(_("cannot match an emulator package with [yellow]{atom}[/]").format(
268
+ atom=emu_atom_str,
269
+ ))
244
270
  return 1
245
271
 
246
272
  if emu_pm.emulator_metadata is None:
247
- logger.F(f"the package [yellow]{emu_atom_str}[/] is not an emulator")
273
+ logger.F(_("the package [yellow]{atom}[/] is not an emulator").format(
274
+ atom=emu_atom_str,
275
+ ))
248
276
  return 1
249
277
 
250
278
  emu_progs = list(emu_pm.emulator_metadata.list_for_arch(target_arch))
251
279
  if not emu_progs:
252
280
  logger.F(
253
- f"the emulator package [yellow]{emu_atom_str}[/] does not support the target architecture [yellow]{target_arch}[/]"
281
+ _("the emulator package [yellow]{atom}[/] does not support the target architecture [yellow]{arch}[/]").format(
282
+ atom=emu_atom_str,
283
+ arch=target_arch,
284
+ )
254
285
  )
255
286
  return 1
256
287
 
@@ -260,13 +291,20 @@ def do_make_venv(
260
291
  emu_pm.emulator_metadata.quirks,
261
292
  ):
262
293
  logger.F(
263
- f"the package [yellow]{emu_atom_str}[/] does not support all necessary features for the profile [yellow]{profile_name}[/]"
294
+ _("the package [yellow]{atom}[/] does not support all necessary features for the profile [yellow]{profile}[/]").format(
295
+ atom=emu_atom_str,
296
+ profile=profile_name,
297
+ )
264
298
  )
265
299
  logger.I(
266
- f"quirks needed by profile: {humanize_list(profile.get_needed_emulator_pkg_flavors(prog.flavor), item_color='cyan')}"
300
+ _("quirks needed by profile: {humanized_list}").format(
301
+ humanized_list=humanize_list(profile.get_needed_emulator_pkg_flavors(prog.flavor), item_color='cyan'),
302
+ )
267
303
  )
268
304
  logger.I(
269
- f"quirks provided by package: {humanize_list(emu_pm.emulator_metadata.quirks or [], item_color='yellow')}"
305
+ _("quirks provided by package: {humanized_list}").format(
306
+ humanized_list=humanize_list(emu_pm.emulator_metadata.quirks or [], item_color='yellow'),
307
+ )
270
308
  )
271
309
  return 1
272
310
 
@@ -275,7 +313,7 @@ def do_make_venv(
275
313
  emu_pm.name_for_installation,
276
314
  )
277
315
  if emu_root is None:
278
- logger.F("cannot find the installed directory for the emulator")
316
+ logger.F(_("cannot find the installed directory for the emulator"))
279
317
  return 1
280
318
 
281
319
  venv_metadata["emulator_pkgs"][target_arch] = _venv_pkg_info_from_pkg(emu_pm)
@@ -291,21 +329,28 @@ def do_make_venv(
291
329
  )
292
330
  if extra_cmd_pm is None:
293
331
  logger.F(
294
- f"cannot match an extra command package with [yellow]{extra_cmd_atom_str}[/]"
332
+ _("cannot match an extra command package with [yellow]{atom}[/]").format(
333
+ atom=extra_cmd_atom_str,
334
+ )
295
335
  )
296
336
  return 1
297
337
 
298
338
  extra_cmd_bm = extra_cmd_pm.binary_metadata
299
339
  if not extra_cmd_bm:
300
340
  logger.F(
301
- f"the package [yellow]{extra_cmd_atom_str}[/] is not a binary-providing package"
341
+ _("the package [yellow]{atom}[/] is not a binary-providing package").format(
342
+ atom=extra_cmd_atom_str,
343
+ )
302
344
  )
303
345
  return 1
304
346
 
305
347
  extra_cmds_decl = extra_cmd_bm.get_commands_for_host(host)
306
348
  if not extra_cmds_decl:
307
349
  logger.W(
308
- f"the package [yellow]{extra_cmd_atom_str}[/] does not provide any command for host [yellow]{host}[/], ignoring"
350
+ _("the package [yellow]{atom}[/] does not provide any command for host [yellow]{host}[/], ignoring").format(
351
+ atom=extra_cmd_atom_str,
352
+ host=host,
353
+ )
309
354
  )
310
355
  continue
311
356
 
@@ -315,7 +360,9 @@ def do_make_venv(
315
360
  )
316
361
  if cmd_root is None:
317
362
  logger.F(
318
- f"cannot find the installed directory for the package [yellow]{extra_cmd_pm.name_for_installation}[/]"
363
+ _("cannot find the installed directory for the package [yellow]{pkg}[/]").format(
364
+ pkg=extra_cmd_pm.name_for_installation,
365
+ )
319
366
  )
320
367
  return 1
321
368
  cmd_root = pathlib.Path(cmd_root)
@@ -329,7 +376,7 @@ def do_make_venv(
329
376
  # we don't allow commands to resolve outside of the
330
377
  # providing package's install root
331
378
  logger.F(
332
- "internal error: resolved command path is outside of the providing package"
379
+ _("internal error: resolved command path is outside of the providing package")
333
380
  )
334
381
  return 1
335
382
 
@@ -338,10 +385,17 @@ def do_make_venv(
338
385
 
339
386
  if override_name is not None:
340
387
  logger.I(
341
- f"Creating a Ruyi virtual environment [cyan]'{override_name}'[/] at [green]{dest}[/]..."
388
+ _("Creating a Ruyi virtual environment [cyan]'{name}'[/] at [green]{dest}[/]...").format(
389
+ name=override_name,
390
+ dest=dest,
391
+ )
342
392
  )
343
393
  else:
344
- logger.I(f"Creating a Ruyi virtual environment at [green]{dest}[/]...")
394
+ logger.I(
395
+ _("Creating a Ruyi virtual environment at [green]{dest}[/]...").format(
396
+ dest=dest,
397
+ )
398
+ )
345
399
 
346
400
  maker = VenvMaker(
347
401
  config,
@@ -356,9 +410,11 @@ def do_make_venv(
356
410
  )
357
411
  maker.provision()
358
412
 
413
+ # TODO: move the template to PO
414
+ locale = match_lang_code(config.lang_code, avail=("en", "zh_CN"))
359
415
  logger.I(
360
416
  render_template_str(
361
- "prompt.venv-created.txt",
417
+ f"prompt.venv-created.{locale}.txt",
362
418
  {
363
419
  "sysroot": maker.sysroot_destdir(None),
364
420
  },
@@ -559,7 +615,9 @@ class VenvMaker:
559
615
  for cmd, dest in extra_cmds.items():
560
616
  if cmd in cmd_metadata_map:
561
617
  self.logger.W(
562
- f"extra command {cmd} is already provided by another package, overriding it"
618
+ _("extra command {cmd} is already provided by another package, overriding it").format(
619
+ cmd=cmd,
620
+ )
563
621
  )
564
622
  cmd_metadata_map[cmd] = {
565
623
  "dest": dest,
ruyi/mux/venv/venv_cli.py CHANGED
@@ -3,6 +3,7 @@ import pathlib
3
3
  from typing import TYPE_CHECKING
4
4
 
5
5
  from ...cli.cmd import RootCommand
6
+ from ...i18n import _
6
7
 
7
8
  if TYPE_CHECKING:
8
9
  from ...cli.completion import ArgumentParser
@@ -12,55 +13,57 @@ if TYPE_CHECKING:
12
13
  class VenvCommand(
13
14
  RootCommand,
14
15
  cmd="venv",
15
- help="Generate a virtual environment adapted to the chosen toolchain and profile",
16
+ help=_("Generate a virtual environment adapted to the chosen toolchain and profile"),
16
17
  ):
17
18
  @classmethod
18
19
  def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
19
- p.add_argument("profile", type=str, help="Profile to use for the environment")
20
- p.add_argument("dest", type=str, help="Path to the new virtual environment")
20
+ p.add_argument("profile", type=str, help=_("Profile to use for the environment"),
21
+ )
22
+ p.add_argument("dest", type=str, help=_("Path to the new virtual environment"),
23
+ )
21
24
  p.add_argument(
22
25
  "--name",
23
26
  "-n",
24
27
  type=str,
25
28
  default=None,
26
- help="Override the venv's name",
29
+ help=_("Override the venv's name"),
27
30
  )
28
31
  p.add_argument(
29
32
  "--toolchain",
30
33
  "-t",
31
34
  type=str,
32
35
  action="append",
33
- help="Specifier(s) (atoms) of the toolchain package(s) to use",
36
+ help=_("Specifier(s) (atoms) of the toolchain package(s) to use"),
34
37
  )
35
38
  p.add_argument(
36
39
  "--emulator",
37
40
  "-e",
38
41
  type=str,
39
- help="Specifier (atom) of the emulator package to use",
42
+ help=_("Specifier (atom) of the emulator package to use"),
40
43
  )
41
44
  p.add_argument(
42
45
  "--with-sysroot",
43
46
  action="store_true",
44
47
  dest="with_sysroot",
45
48
  default=True,
46
- help="Provision a fresh sysroot inside the new virtual environment (default)",
49
+ help=_("Provision a fresh sysroot inside the new virtual environment (default)"),
47
50
  )
48
51
  p.add_argument(
49
52
  "--without-sysroot",
50
53
  action="store_false",
51
54
  dest="with_sysroot",
52
- help="Do not include a sysroot inside the new virtual environment",
55
+ help=_("Do not include a sysroot inside the new virtual environment"),
53
56
  )
54
57
  p.add_argument(
55
58
  "--sysroot-from",
56
59
  type=str,
57
- help="Specifier (atom) of the sysroot package to use, in favor of the toolchain-included one if applicable",
60
+ help=_("Specifier (atom) of the sysroot package to use, in favor of the toolchain-included one if applicable"),
58
61
  )
59
62
  p.add_argument(
60
63
  "--extra-commands-from",
61
64
  type=str,
62
65
  action="append",
63
- help="Specifier(s) (atoms) of extra package(s) to add commands to the new virtual environment",
66
+ help=_("Specifier(s) (atoms) of extra package(s) to add commands to the new virtual environment"),
64
67
  )
65
68
 
66
69
  @classmethod
@@ -2,6 +2,7 @@ import argparse
2
2
  from typing import TYPE_CHECKING
3
3
 
4
4
  from ..cli.cmd import AdminCommand
5
+ from ..i18n import _
5
6
 
6
7
  if TYPE_CHECKING:
7
8
  from ..cli.completion import ArgumentParser
@@ -11,7 +12,7 @@ if TYPE_CHECKING:
11
12
  class AdminRunPluginCommand(
12
13
  AdminCommand,
13
14
  cmd="run-plugin-cmd",
14
- help="Run a plugin-defined command",
15
+ help=_("Run a plugin-defined command"),
15
16
  ):
16
17
  @classmethod
17
18
  def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
@@ -19,14 +20,14 @@ class AdminRunPluginCommand(
19
20
  "cmd_name",
20
21
  type=str,
21
22
  metavar="COMMAND-NAME",
22
- help="Command name",
23
+ help=_("Command name"),
23
24
  )
24
25
  p.add_argument(
25
26
  "cmd_args",
26
27
  type=str,
27
28
  nargs="*",
28
29
  metavar="COMMAND-ARG",
29
- help="Arguments to pass to the plugin command",
30
+ help=_("Arguments to pass to the plugin command"),
30
31
  )
31
32
 
32
33
  @classmethod