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/config/__init__.py CHANGED
@@ -18,6 +18,13 @@ if TYPE_CHECKING:
18
18
  from ..utils.xdg_basedir import XDGPathEntry
19
19
  from .news import NewsReadStatusStore
20
20
 
21
+ import babel
22
+ # not sure why Pyright insists on individual imports
23
+ # otherwise, at the use site (`except babel.core.UnknownLocaleError`):
24
+ # error: "core" is not a known attribute of module "babel" (reportAttributeAccessIssue)
25
+ from babel.core import UnknownLocaleError
26
+
27
+ from ..i18n import _
21
28
  from . import errors
22
29
  from . import schema
23
30
 
@@ -115,7 +122,11 @@ class GlobalConfig:
115
122
  if iem is not None and not is_global_scope:
116
123
  iem_cfg_key = f"{schema.SECTION_INSTALLATION}.{schema.KEY_INSTALLATION_EXTERNALLY_MANAGED}"
117
124
  self.logger.W(
118
- f"the config key [yellow]{iem_cfg_key}[/] cannot be set from user config; ignoring",
125
+ _(
126
+ "the config key [yellow]{key}[/] cannot be set from user config; ignoring"
127
+ ).format(
128
+ key=iem_cfg_key,
129
+ ),
119
130
  )
120
131
  else:
121
132
  self.is_installation_externally_managed = bool(iem)
@@ -133,7 +144,11 @@ class GlobalConfig:
133
144
  if self.override_repo_dir:
134
145
  if not pathlib.Path(self.override_repo_dir).is_absolute():
135
146
  self.logger.W(
136
- f"the local repo path '{self.override_repo_dir}' is not absolute; ignoring"
147
+ _(
148
+ "the local repo path '{path}' is not absolute; ignoring"
149
+ ).format(
150
+ path=self.override_repo_dir,
151
+ )
137
152
  )
138
153
  self.override_repo_dir = None
139
154
 
@@ -274,6 +289,15 @@ class GlobalConfig:
274
289
  def lang_code(self) -> str:
275
290
  return self._lang_code
276
291
 
292
+ @cached_property
293
+ def babel_locale(self) -> babel.Locale:
294
+ try:
295
+ return babel.Locale.parse(self.lang_code)
296
+ except UnknownLocaleError:
297
+ # this can happen in case of unrecognized locale names, which
298
+ # apparently falls back to "C"
299
+ return babel.Locale.parse("en_US")
300
+
277
301
  @property
278
302
  def cache_root(self) -> os.PathLike[Any]:
279
303
  return self._dirs.app_cache
ruyi/config/errors.py CHANGED
@@ -1,6 +1,8 @@
1
1
  from os import PathLike
2
2
  from typing import Any, Sequence
3
3
 
4
+ from ..i18n import _
5
+
4
6
 
5
7
  class InvalidConfigSectionError(Exception):
6
8
  def __init__(self, section: str) -> None:
@@ -8,7 +10,7 @@ class InvalidConfigSectionError(Exception):
8
10
  self._section = section
9
11
 
10
12
  def __str__(self) -> str:
11
- return f"invalid config section: {self._section}"
13
+ return _("invalid config section: {section}").format(section=self._section)
12
14
 
13
15
  def __repr__(self) -> str:
14
16
  return f"InvalidConfigSectionError({self._section!r})"
@@ -20,7 +22,7 @@ class InvalidConfigKeyError(Exception):
20
22
  self._key = key
21
23
 
22
24
  def __str__(self) -> str:
23
- return f"invalid config key: {self._key}"
25
+ return _("invalid config key: {key}").format(key=self._key)
24
26
 
25
27
  def __repr__(self) -> str:
26
28
  return f"InvalidConfigKeyError({self._key:!r})"
@@ -39,7 +41,13 @@ class InvalidConfigValueTypeError(TypeError):
39
41
  self._expected = expected
40
42
 
41
43
  def __str__(self) -> str:
42
- return f"invalid value type for config key {self._key}: {type(self._val)}, expected {self._expected}"
44
+ return _(
45
+ "invalid value type for config key {key}: {actual_type}, expected {expected_type}"
46
+ ).format(
47
+ key=self._key,
48
+ actual_type=type(self._val),
49
+ expected_type=self._expected,
50
+ )
43
51
 
44
52
  def __repr__(self) -> str:
45
53
  return f"InvalidConfigValueTypeError({self._key!r}, {self._val!r}, {self._expected:!r})"
@@ -58,8 +66,10 @@ class InvalidConfigValueError(ValueError):
58
66
  self._typ = typ
59
67
 
60
68
  def __str__(self) -> str:
61
- return (
62
- f"invalid config value for key {self._key} (type {self._typ}): {self._val}"
69
+ return _("invalid config value for key {key} (type {typ}): {val}").format(
70
+ key=self._key,
71
+ typ=self._typ,
72
+ val=self._val,
63
73
  )
64
74
 
65
75
  def __repr__(self) -> str:
@@ -74,7 +84,7 @@ class MalformedConfigFileError(Exception):
74
84
  self._path = path
75
85
 
76
86
  def __str__(self) -> str:
77
- return f"malformed config file: {self._path}"
87
+ return _("malformed config file: {path}").format(path=self._path)
78
88
 
79
89
  def __repr__(self) -> str:
80
90
  return f"MalformedConfigFileError({self._path:!r})"
@@ -86,7 +96,9 @@ class ProtectedGlobalConfigError(Exception):
86
96
  self._key = key
87
97
 
88
98
  def __str__(self) -> str:
89
- return f"attempt to modify protected global config key: {self._key}"
99
+ return _("attempt to modify protected global config key: {key}").format(
100
+ key=self._key,
101
+ )
90
102
 
91
103
  def __repr__(self) -> str:
92
104
  return f"ProtectedGlobalConfigError({self._key!r})"
ruyi/device/provision.py CHANGED
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, TypedDict, TypeGuard, cast
4
4
 
5
5
  from ..cli import user_input
6
6
  from ..config import GlobalConfig
7
+ from ..i18n import _
7
8
  from ..log import RuyiLogger
8
9
  from ..ruyipkg.atom import Atom, ExprAtom, SlugAtom
9
10
  from ..ruyipkg.entity_provider import BaseEntity
@@ -36,7 +37,8 @@ def do_provision_interactive(config: GlobalConfig) -> int:
36
37
  mr.ensure_git_repo()
37
38
 
38
39
  log.stdout(
39
- """
40
+ _(
41
+ """
40
42
  [bold green]RuyiSDK Device Provisioning Wizard[/]
41
43
 
42
44
  This is a wizard intended to help you install a system on your device for your
@@ -52,11 +54,12 @@ required to flash images, you should arrange to allow your user account [yellow]
52
54
  access to necessary commands such as [yellow]dd[/]. Flashing will fail if the [yellow]sudo[/]
53
55
  configuration does not allow so.
54
56
  """
57
+ )
55
58
  )
56
59
 
57
- if not user_input.ask_for_yesno_confirmation(log, "Continue?"):
60
+ if not user_input.ask_for_yesno_confirmation(log, _("Continue?")):
58
61
  log.stdout(
59
- "\nExiting. You can restart the wizard whenever prepared.",
62
+ _("\nExiting. You can restart the wizard whenever prepared."),
60
63
  end="\n\n",
61
64
  )
62
65
  return 1
@@ -68,7 +71,9 @@ configuration does not allow so.
68
71
  dev_choices = {k: v.display_name or "" for k, v in devices_by_id.items()}
69
72
  dev_id = user_input.ask_for_kv_choice(
70
73
  log,
71
- "\nThe following devices are currently supported by the wizard. Please pick your device:",
74
+ _(
75
+ "\nThe following devices are currently supported by the wizard. Please pick your device:"
76
+ ),
72
77
  dev_choices,
73
78
  )
74
79
  dev = devices_by_id[dev_id]
@@ -84,7 +89,9 @@ configuration does not allow so.
84
89
  variant_choices = [get_variant_display_name(dev, i) for i in variants]
85
90
  variant_idx = user_input.ask_for_choice(
86
91
  log,
87
- "\nThe device has the following variants. Please choose the one corresponding to your hardware at hand:",
92
+ _(
93
+ "\nThe device has the following variants. Please choose the one corresponding to your hardware at hand:"
94
+ ),
88
95
  variant_choices,
89
96
  )
90
97
  variant = variants[variant_idx]
@@ -101,7 +108,9 @@ configuration does not allow so.
101
108
  combo_choices = [combo.display_name or "" for combo in supported_combos]
102
109
  combo_idx = user_input.ask_for_choice(
103
110
  log,
104
- "\nThe following system configurations are supported by the device variant you have chosen. Please pick the one you want to put on the device:",
111
+ _(
112
+ "\nThe following system configurations are supported by the device variant you have chosen. Please pick the one you want to put on the device:"
113
+ ),
105
114
  combo_choices,
106
115
  )
107
116
  combo = supported_combos[combo_idx]
@@ -132,7 +141,8 @@ def do_provision_combo_interactive(
132
141
  combo: BaseEntity,
133
142
  ) -> int:
134
143
  logger = config.logger
135
- logger.D(f"provisioning device variant '{dev_decl.id}@{variant_decl.id}'")
144
+ devid = f"{dev_decl.id}@{variant_decl.id}"
145
+ logger.D(f"provisioning device variant '{devid}'")
136
146
 
137
147
  # download packages
138
148
  pkg_atoms = combo.data.get("package_atoms", [])
@@ -141,28 +151,36 @@ def do_provision_combo_interactive(
141
151
  return 0
142
152
 
143
153
  logger.F(
144
- f"malformed config: device variant '{dev_decl.id}@{variant_decl.id}' asks for no packages but provides no messages either"
154
+ _(
155
+ "malformed config: device variant '{devid}' asks for no packages but provides no messages either"
156
+ ).format(
157
+ devid=devid,
158
+ )
145
159
  )
146
160
  return 1
147
161
 
148
162
  new_pkg_atoms = customize_package_versions(config, mr, pkg_atoms)
149
163
  if new_pkg_atoms is None:
150
- logger.stdout("\nExiting. You may restart the wizard at any time.", end="\n\n")
164
+ logger.stdout(
165
+ _("\nExiting. You may restart the wizard at any time."), end="\n\n"
166
+ )
151
167
  return 1
152
168
  else:
153
169
  pkg_atoms = new_pkg_atoms
154
170
 
155
171
  pkg_names_for_display = "\n".join(f" * [green]{i}[/]" for i in pkg_atoms)
156
172
  logger.stdout(
157
- f"""
158
- We are about to download and install the following packages for your device:
159
-
160
- {pkg_names_for_display}
161
- """
173
+ _(
174
+ "\nWe are about to download and install the following packages for your device:"
175
+ ),
176
+ end="\n\n",
162
177
  )
178
+ logger.stdout(pkg_names_for_display, end="\n\n")
163
179
 
164
- if not user_input.ask_for_yesno_confirmation(logger, "Proceed?"):
165
- logger.stdout("\nExiting. You may restart the wizard at any time.", end="\n\n")
180
+ if not user_input.ask_for_yesno_confirmation(logger, _("Proceed?")):
181
+ logger.stdout(
182
+ _("\nExiting. You may restart the wizard at any time."), end="\n\n"
183
+ )
166
184
  return 1
167
185
 
168
186
  ret = do_install_atoms(
@@ -174,8 +192,8 @@ We are about to download and install the following packages for your device:
174
192
  reinstall=False,
175
193
  )
176
194
  if ret != 0:
177
- logger.F("failed to download and install packages")
178
- logger.I("your device was not touched")
195
+ logger.F(_("failed to download and install packages"))
196
+ logger.I(_("your device was not touched"))
179
197
  return 2
180
198
 
181
199
  strat_provider = ProvisionStrategyProvider(mr)
@@ -198,7 +216,8 @@ We are about to download and install the following packages for your device:
198
216
  host_blkdev_map: PartitionMapDecl = {}
199
217
  if requested_host_blkdevs:
200
218
  logger.stdout(
201
- """
219
+ _(
220
+ """
202
221
  For initializing this target device, you should plug into this host system the
203
222
  device's storage (e.g. SD card or NVMe SSD), or a removable disk to be
204
223
  reformatted as a live medium, and note down the corresponding device file
@@ -206,6 +225,7 @@ path(s), e.g. /dev/sdX, /dev/nvmeXnY for whole disks; /dev/sdXY, /dev/nvmeXnYpZ
206
225
  for partitions. You may consult e.g. [yellow]sudo blkid[/] output for the
207
226
  information you will need later.
208
227
  """
228
+ )
209
229
  )
210
230
  for part in requested_host_blkdevs:
211
231
  part_desc = get_part_desc(part)
@@ -213,7 +233,9 @@ information you will need later.
213
233
  while True:
214
234
  path = user_input.ask_for_file(
215
235
  logger,
216
- f"Please give the path for the {part_desc}:",
236
+ _("Please give the path for the {part_desc}:").format(
237
+ part_desc=part_desc,
238
+ ),
217
239
  )
218
240
 
219
241
  # Retrieve the latest mount info in case the user un-mounts
@@ -224,10 +246,17 @@ information you will need later.
224
246
  for m in blkdev_mounts:
225
247
  if m.source_path.samefile(path):
226
248
  logger.W(
227
- f"path [cyan]'{path}'[/] is currently mounted at [yellow]'{m.target}'[/]"
249
+ _(
250
+ "path [cyan]'{path}'[/] is currently mounted at [yellow]'{target}'[/]"
251
+ ).format(
252
+ path=path,
253
+ target=m.target,
254
+ )
228
255
  )
229
256
  logger.I(
230
- "rejecting the path for safety; please double-check and retry"
257
+ _(
258
+ "rejecting the path for safety; please double-check and retry"
259
+ )
231
260
  )
232
261
  path_valid = False
233
262
  break
@@ -238,12 +267,14 @@ information you will need later.
238
267
 
239
268
  # final confirmation
240
269
  logger.stdout(
241
- """
270
+ _(
271
+ """
242
272
  We have collected enough information for the actual flashing. Now is the last
243
273
  chance to re-check and confirm everything is fine.
244
274
 
245
275
  We are about to:
246
276
  """
277
+ )
247
278
  )
248
279
 
249
280
  pretend_steps = "\n".join(
@@ -257,9 +288,11 @@ We are about to:
257
288
  )
258
289
  logger.stdout(pretend_steps, end="\n\n")
259
290
 
260
- if not user_input.ask_for_yesno_confirmation(logger, "Proceed with flashing?"):
291
+ if not user_input.ask_for_yesno_confirmation(logger, _("Proceed with flashing?")):
261
292
  logger.stdout(
262
- "\nExiting. The device is not touched and you may re-start the wizard at will.",
293
+ _(
294
+ "\nExiting. The device is not touched and you may re-start the wizard at will."
295
+ ),
263
296
  end="\n\n",
264
297
  )
265
298
  return 1
@@ -273,18 +306,20 @@ We are about to:
273
306
  # ask the user to ensure the device shows up
274
307
  # TODO: automate doing so
275
308
  logger.stdout(
276
- """
309
+ _("""
277
310
  Some flashing steps require the use of fastboot, in which case you should
278
311
  ensure the target device is showing up in [yellow]fastboot devices[/] output.
279
312
  Please [bold red]confirm it yourself before continuing[/].
280
- """
313
+ """)
281
314
  )
282
315
  if not user_input.ask_for_yesno_confirmation(
283
316
  logger,
284
- "Is the device identified by fastboot now?",
317
+ _("Is the device identified by fastboot now?"),
285
318
  ):
286
319
  logger.stdout(
287
- "\nAborting. The device is not touched. You may re-start the wizard after [yellow]fastboot[/] is fixed for the device.",
320
+ _(
321
+ "\nAborting. The device is not touched. You may re-start the wizard after [yellow]fastboot[/] is fixed for the device."
322
+ ),
288
323
  end="\n\n",
289
324
  )
290
325
  return 1
@@ -294,16 +329,16 @@ Please [bold red]confirm it yourself before continuing[/].
294
329
  logger.D(f"flashing {pkg} with strategy {strat}")
295
330
  ret = strat.flash(pkg_part_maps[pkg], host_blkdev_map)
296
331
  if ret != 0:
297
- logger.F("flashing failed, check your device right now")
332
+ logger.F(_("flashing failed, check your device right now"))
298
333
  return ret
299
334
 
300
335
  # parting words
301
336
  logger.stdout(
302
- """
337
+ _("""
303
338
  It seems the flashing has finished without errors.
304
339
 
305
340
  [bold green]Happy hacking![/]
306
- """
341
+ """)
307
342
  )
308
343
 
309
344
  maybe_render_postinst_msg(logger, mr, combo, config.lang_code)
@@ -314,11 +349,11 @@ It seems the flashing has finished without errors.
314
349
  def get_part_desc(part: PartitionKind) -> str:
315
350
  match part:
316
351
  case "disk":
317
- return "target's whole disk"
352
+ return _("target's whole disk")
318
353
  case "live":
319
- return "removable disk to use as live medium"
354
+ return _("removable disk to use as live medium")
320
355
  case _:
321
- return f"target's '{part}' partition"
356
+ return _("target's '{part}' partition").format(part=part)
322
357
 
323
358
 
324
359
  class PackageProvisionStrategyDecl(TypedDict):
@@ -489,17 +524,19 @@ def customize_package_versions(
489
524
 
490
525
  # Ask if the user wants to customize package versions
491
526
  logger.stdout(
492
- "By default, we'll install the latest version of each package, but in this case, other choices are possible."
527
+ _(
528
+ "By default, we'll install the latest version of each package, but in this case, other choices are possible."
529
+ )
493
530
  )
494
531
  if not user_input.ask_for_yesno_confirmation(
495
532
  logger,
496
- "Would you like to customize package versions?",
533
+ _("Would you like to customize package versions?"),
497
534
  ):
498
535
  return pkg_atoms
499
536
 
500
537
  while True: # Loop to allow restarting the selection process
501
538
  result: list[str] = []
502
- logger.stdout("\n[bold]Package Version Selection[/]")
539
+ logger.stdout(_("\n[bold]Package Version Selection[/]"))
503
540
 
504
541
  for atom_str in pkg_atoms:
505
542
  # Parse the atom to get package name
@@ -507,18 +544,26 @@ def customize_package_versions(
507
544
  if isinstance(a, ExprAtom):
508
545
  # If it's already an expression with version constraints, show the constraints
509
546
  logger.stdout(
510
- f"\nPackage [green]{atom_str}[/] already has version constraints."
547
+ _(
548
+ "\nPackage [green]{atom}[/] already has version constraints."
549
+ ).format(
550
+ atom=atom_str,
551
+ )
511
552
  )
512
553
  if not user_input.ask_for_yesno_confirmation(
513
554
  logger,
514
- "Would you like to change them?",
555
+ _("Would you like to change them?"),
515
556
  ):
516
557
  result.append(atom_str)
517
558
  continue
518
559
  elif isinstance(a, SlugAtom):
519
560
  # Slugs already fix the version, so we can't change them
520
561
  logger.W(
521
- f"version cannot be overridden for slug atom [green]{atom_str}[/]"
562
+ _(
563
+ "version cannot be overridden for slug atom [green]{atom}[/]"
564
+ ).format(
565
+ atom=atom_str,
566
+ )
522
567
  )
523
568
  result.append(atom_str)
524
569
  continue
@@ -526,19 +571,24 @@ def customize_package_versions(
526
571
  # Get all available versions for this package
527
572
  package_name = a.name
528
573
  category = a.category
574
+ pkg_fullname = f"{category}/{package_name}" if category else package_name
529
575
 
530
576
  available_versions: "list[BoundPackageManifest]" = []
531
577
  try:
532
578
  available_versions = list(mr.iter_pkg_vers(package_name, category))
533
579
  except KeyError:
534
580
  logger.W(
535
- f"could not find package [yellow]{category}/{package_name}[/] in repository"
581
+ _("could not find package [yellow]{pkg}[/] in repository").format(
582
+ pkg=pkg_fullname
583
+ )
536
584
  )
537
585
  result.append(atom_str)
538
586
 
539
587
  if not available_versions:
540
588
  logger.W(
541
- f"no versions found for package [yellow]{category}/{package_name}[/]"
589
+ _("no versions found for package [yellow]{pkg}[/]").format(
590
+ pkg=pkg_fullname
591
+ )
542
592
  )
543
593
  result.append(atom_str)
544
594
  continue
@@ -547,7 +597,12 @@ def customize_package_versions(
547
597
  # If there's only one version available, use it
548
598
  selected_version = available_versions[0]
549
599
  logger.stdout(
550
- f"Only one version available for [green]{category}/{package_name}[/]: [blue]{selected_version.ver}[/], using it."
600
+ _(
601
+ "Only one version available for [green]{pkg}[/]: [blue]{ver}[/], using it."
602
+ ).format(
603
+ pkg=pkg_fullname,
604
+ ver=selected_version.ver,
605
+ )
551
606
  )
552
607
  result.append(atom_str)
553
608
  continue
@@ -562,11 +617,15 @@ def customize_package_versions(
562
617
  remarks = []
563
618
 
564
619
  if pm.is_prerelease:
565
- remarks.append("prerelease")
620
+ remarks.append(_("prerelease"))
566
621
  if pm.service_level.has_known_issues:
567
- remarks.append("has known issues")
622
+ remarks.append(_("has known issues"))
568
623
  if pm.upstream_version:
569
- remarks.append(f"upstream: {pm.upstream_version}")
624
+ remarks.append(
625
+ _("upstream: {upstream_ver}").format(
626
+ upstream_ver=pm.upstream_version
627
+ )
628
+ )
570
629
 
571
630
  remark_str = f" ({', '.join(remarks)})" if remarks else ""
572
631
  version_choices.append(f"{version_str}{remark_str}")
@@ -574,7 +633,9 @@ def customize_package_versions(
574
633
  # Ask the user to select a version
575
634
  version_idx = user_input.ask_for_choice(
576
635
  logger,
577
- f"\nSelect a version for package [green]{category or ''}{('/' + package_name) if category else package_name}[/]:",
636
+ _("\nSelect a version for package [green]{pkg}[/]:").format(
637
+ pkg=pkg_fullname,
638
+ ),
578
639
  version_choices,
579
640
  )
580
641
 
@@ -586,27 +647,27 @@ def customize_package_versions(
586
647
  else:
587
648
  new_atom = f"{package_name}(=={selected_version.ver})"
588
649
 
589
- logger.stdout(f"Selected: [blue]{new_atom}[/]")
650
+ logger.stdout(_("Selected: [blue]{new_atom}[/]").format(new_atom=new_atom))
590
651
  result.append(new_atom)
591
652
 
592
- logger.stdout("\nPackage versions to be installed:")
653
+ logger.stdout(_("\nPackage versions to be installed:"))
593
654
  for atom in result:
594
655
  logger.stdout(f" * [green]{atom}[/]")
595
656
 
596
657
  confirmation = user_input.ask_for_choice(
597
658
  logger,
598
- "\nHow would you like to proceed?",
659
+ _("\nHow would you like to proceed?"),
599
660
  [
600
- "Continue with these versions",
601
- "Restart version selection",
602
- "Abort device provisioning",
661
+ _("Continue with these versions"),
662
+ _("Restart version selection"),
663
+ _("Abort device provisioning"),
603
664
  ],
604
665
  )
605
666
 
606
667
  if confirmation == 0: # Continue with these versions
607
668
  return result
608
669
  elif confirmation == 1: # Restart version selection
609
- logger.stdout("\nRestarting package version selection...")
670
+ logger.stdout(_("\nRestarting package version selection..."))
610
671
  continue
611
672
  else: # Abort installation
612
673
  return None
@@ -2,6 +2,7 @@ import argparse
2
2
  from typing import TYPE_CHECKING
3
3
 
4
4
  from ..cli.cmd import RootCommand
5
+ from ..i18n import _
5
6
 
6
7
  if TYPE_CHECKING:
7
8
  from ..cli.completion import ArgumentParser
@@ -12,7 +13,7 @@ class DeviceCommand(
12
13
  RootCommand,
13
14
  cmd="device",
14
15
  has_subcommands=True,
15
- help="Manage devices",
16
+ help=_("Manage devices"),
16
17
  ):
17
18
  @classmethod
18
19
  def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
@@ -23,7 +24,7 @@ class DeviceProvisionCommand(
23
24
  DeviceCommand,
24
25
  cmd="provision",
25
26
  aliases=["flash"],
26
- help="Interactively initialize a device for development",
27
+ help=_("Interactively initialize a device for development"),
27
28
  ):
28
29
  @classmethod
29
30
  def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
@@ -36,5 +37,7 @@ class DeviceProvisionCommand(
36
37
  try:
37
38
  return do_provision_interactive(cfg)
38
39
  except KeyboardInterrupt:
39
- cfg.logger.stdout("\n\nKeyboard interrupt received, exiting.", end="\n\n")
40
+ cfg.logger.stdout(
41
+ _("\n\nKeyboard interrupt received, exiting."), end="\n\n"
42
+ )
40
43
  return 1