project-auto-wizard 0.13.3 → 0.15.0

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 (115) hide show
  1. package/README.ja.md +2 -0
  2. package/README.ko.md +2 -0
  3. package/README.md +3 -1
  4. package/README.zh-CN.md +2 -0
  5. package/bin/project-auto-wizard.js +18 -4
  6. package/package.json +2 -2
  7. package/payload/config/breaking-changes.json +28 -14
  8. package/payload/config/wizard-prompts.yml +102 -46
  9. package/payload/flutter-app/android/fastlane/Fastfile.playstore +44 -32
  10. package/payload/flutter-app/ios/ExportOptions.plist +8 -8
  11. package/payload/flutter-app/ios/fastlane/Fastfile +57 -43
  12. package/payload/scripts/changelog_manager.py +304 -265
  13. package/payload/scripts/issue_helper.py +54 -50
  14. package/payload/scripts/messages.py +2585 -0
  15. package/payload/scripts/truncate_release_notes.py +7 -6
  16. package/payload/scripts/version_manager.py +110 -107
  17. package/payload/version.yml.template +2 -1
  18. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +23 -16
  19. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +106 -63
  20. package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
  21. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +55 -45
  22. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +90 -72
  23. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +59 -45
  24. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +194 -184
  25. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +226 -213
  26. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +46 -42
  27. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +334 -241
  28. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +152 -128
  29. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +246 -188
  30. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +353 -238
  31. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +96 -85
  32. package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -36
  33. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +945 -508
  34. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +155 -133
  35. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +76 -60
  36. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +97 -77
  37. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +62 -47
  38. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +942 -505
  39. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +148 -126
  40. package/payload/workflows/react/PROJECT-REACT-CI.yaml +86 -70
  41. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +112 -92
  42. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +112 -82
  43. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +180 -150
  44. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +161 -131
  45. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +970 -549
  46. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +193 -162
  47. package/src/cli/args.js +98 -52
  48. package/src/cli/help.js +8 -41
  49. package/src/commands/doctor.js +90 -89
  50. package/src/commands/dry-run.js +64 -43
  51. package/src/commands/full.js +79 -77
  52. package/src/commands/install-settings.js +16 -15
  53. package/src/commands/interactive-flutter.js +20 -19
  54. package/src/commands/interactive.js +135 -132
  55. package/src/commands/purge.js +47 -42
  56. package/src/commands/status.js +36 -36
  57. package/src/commands/uninstall.js +43 -41
  58. package/src/context.js +12 -11
  59. package/src/core/assets.js +12 -11
  60. package/src/core/baseline.js +27 -27
  61. package/src/core/branches.js +29 -27
  62. package/src/core/branding.js +7 -6
  63. package/src/core/breaking-check.js +22 -21
  64. package/src/core/breaking.js +15 -8
  65. package/src/core/copy/app-files.js +5 -4
  66. package/src/core/copy/flutter-app.js +8 -8
  67. package/src/core/copy/gitignore.js +58 -50
  68. package/src/core/copy/readme.js +102 -36
  69. package/src/core/copy/simple.js +16 -15
  70. package/src/core/copy/workflows.js +131 -124
  71. package/src/core/deploy-style.js +51 -48
  72. package/src/core/detect-fs.js +57 -57
  73. package/src/core/detect.js +67 -65
  74. package/src/core/errors.js +2 -2
  75. package/src/core/flutter-doctor.js +17 -16
  76. package/src/core/flutter-hooks.js +33 -32
  77. package/src/core/flutter-options.js +34 -33
  78. package/src/core/fsutil.js +12 -12
  79. package/src/core/installed-stores.js +4 -4
  80. package/src/core/logger.js +42 -40
  81. package/src/core/paths-resolve.js +88 -86
  82. package/src/core/paths.js +10 -10
  83. package/src/core/release-options.js +8 -8
  84. package/src/core/removal-exec.js +14 -13
  85. package/src/core/removal-plan.js +37 -37
  86. package/src/core/types.js +47 -47
  87. package/src/core/verify.js +44 -38
  88. package/src/core/version-yml.js +76 -68
  89. package/src/core/wizard-env.js +46 -46
  90. package/src/core/wizard-labels.js +63 -41
  91. package/src/i18n/catalog/en/cli.js +50 -0
  92. package/src/i18n/catalog/en/commands.js +188 -0
  93. package/src/i18n/catalog/en/copy.js +20 -0
  94. package/src/i18n/catalog/en/core.js +74 -0
  95. package/src/i18n/catalog/en/core2.js +15 -0
  96. package/src/i18n/catalog/en/ui.js +221 -0
  97. package/src/i18n/catalog/en.js +12 -0
  98. package/src/i18n/catalog/index.js +5 -0
  99. package/src/i18n/catalog/ko/cli.js +50 -0
  100. package/src/i18n/catalog/ko/commands.js +188 -0
  101. package/src/i18n/catalog/ko/copy.js +20 -0
  102. package/src/i18n/catalog/ko/core.js +74 -0
  103. package/src/i18n/catalog/ko/core2.js +15 -0
  104. package/src/i18n/catalog/ko/ui.js +221 -0
  105. package/src/i18n/catalog/ko.js +10 -0
  106. package/src/i18n/index.js +44 -0
  107. package/src/i18n/languages.js +9 -0
  108. package/src/index.js +132 -97
  109. package/src/ui/ansi.js +7 -7
  110. package/src/ui/banner.js +7 -6
  111. package/src/ui/env-plan.js +82 -81
  112. package/src/ui/prompts.js +97 -122
  113. package/src/ui/readline-engine.js +59 -58
  114. package/src/ui/status-cards.js +37 -31
  115. package/src/ui/summary.js +90 -89
@@ -38,26 +38,28 @@ import sys
38
38
  from pathlib import Path
39
39
  from typing import Callable, NamedTuple
40
40
 
41
+ from messages import t, use_utf8_output
42
+
41
43
  VERSION_YML = "version.yml"
42
44
 
43
45
 
44
46
  class VersionSyncError(Exception):
45
- """version.yml·프로젝트 파일에 버전을 쓰지 못했다 — exit 1로 알린다."""
47
+ """The version could not be written to version.yml or a project file — reported via exit 1."""
46
48
  SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
47
49
 
48
- # 프로젝트 파일 버전 해석 규칙 — 설치 시 감지(src/core/detect.js coreVersion)와 같아야 한다.
49
- # version.yml은 x.y.z만 저장·증가시키므로 파일의 1.2.3-rc.1·1.2.3+4·1.2.0-SNAPSHOT은 코어 x.y.z로 읽는다.
50
- # 다르게 읽으면 설치 직후 version.yml 값과 첫 릴리스 때 읽은 값이 어긋난다.
50
+ # How project-file versions are interpreted — must match the install-time detection (src/core/detect.js coreVersion).
51
+ # version.yml only stores and bumps x.y.z, so 1.2.3-rc.1, 1.2.3+4 and 1.2.0-SNAPSHOT in a file are read as the core x.y.z.
52
+ # Reading them differently would make the value right after install disagree with the one read at the first release.
51
53
  _CORE_VERSION_RE = re.compile(r"^v?(\d+\.\d+\.\d+)(?:[-+][0-9A-Za-z.+-]*)?$")
52
54
 
53
- # Gradle·Maven의 -SNAPSHOT은 특정 버전의 프리릴리스가 아니라 "개발 중 빌드" 표식이다(배포 저장소 선택,
54
- # 산출물 이름에 쓰인다). 그래서 버전을 올려도 떼지 않고 새 버전 뒤에 그대로 붙인다.
55
- # rc.1·beta.1 같은 프리릴리스는 그 버전 전용 표식이라 새 x.y.z로 릴리스할 때 버린다.
55
+ # Gradle/Maven -SNAPSHOT is not a pre-release of a specific version but an "in-development build" marker (it selects the
56
+ # publishing repository and appears in artifact names). So it is kept and re-appended after a version bump.
57
+ # Pre-releases such as rc.1 or beta.1 belong to that version only, so they are dropped when releasing a new x.y.z.
56
58
  SNAPSHOT_SUFFIX = "-SNAPSHOT"
57
59
 
58
60
 
59
61
  def core_version(value):
60
- """x.y.z[-pre][+build] → x.y.z. 버전 형식이 아니면 None."""
62
+ """x.y.z[-pre][+build] -> x.y.z. None if the value is not a version."""
61
63
  m = _CORE_VERSION_RE.match(str(value or "").strip())
62
64
  return m.group(1) if m else None
63
65
 
@@ -115,7 +117,7 @@ def _version_yml_path():
115
117
 
116
118
  def require_version_yml():
117
119
  if not _version_yml_path().is_file():
118
- log("ERROR: version.yml not found")
120
+ log(t("version_manager.err_yml_missing"))
119
121
  sys.exit(1)
120
122
 
121
123
 
@@ -236,7 +238,7 @@ def get_version_code():
236
238
  require_version_yml()
237
239
  code = read_scalar_key("version_code", None)
238
240
  if code is None or code == "" or code == "null":
239
- log("WARNING: version_code field missing, adding default value 1")
241
+ log(t("version_manager.warn_code_missing"))
240
242
  text = read_text()
241
243
  if re.search(r'^version:', text, re.MULTILINE):
242
244
  new_text = re.sub(
@@ -258,7 +260,7 @@ def set_version_code(new_code):
258
260
  if current not in (None, "", "null"):
259
261
  try:
260
262
  if int(new_code) < int(current):
261
- log(f"WARNING: version_code {new_code} is lower than current {current} — writing anyway (regression?)")
263
+ log(t("version_manager.warn_code_regress", new_code=new_code, current=current))
262
264
  except ValueError:
263
265
  pass
264
266
  text = read_text()
@@ -287,7 +289,7 @@ def increment_patch(version):
287
289
 
288
290
 
289
291
  def increment_version(version, bump="patch"):
290
- """bump: 'major'|'minor'|'patch'. 생략하면 기존과 동일하게 patch(increment_patch)로 동작."""
292
+ """bump: 'major'|'minor'|'patch'. Defaults to patch when omitted."""
291
293
  if bump == "major":
292
294
  major, _minor, _patch = version.split(".")
293
295
  return f"{int(major) + 1}.0.0"
@@ -315,8 +317,8 @@ def get_higher_version(v1, v2):
315
317
 
316
318
  def update_version_yml(new_version):
317
319
  if not write_scalar_key("version", new_version):
318
- # 키가 없으면 아무것도 쓰지 않은 채 성공처럼 끝나 매번 같은 버전이 나왔다.
319
- log("WARNING: version field missing in version.yml, adding it")
320
+ # Without the key nothing was written yet the call looked successful, so the same version came out every time.
321
+ log(t("version_manager.warn_version_missing"))
320
322
  text = read_text()
321
323
  line = f'version: "{new_version}"\n'
322
324
  if re.search(r'^version_code:', text, re.MULTILINE):
@@ -325,7 +327,7 @@ def update_version_yml(new_version):
325
327
  new_text = text.rstrip("\n") + "\n" + line
326
328
  write_text(new_text)
327
329
  if read_scalar_key("version") != new_version:
328
- raise VersionSyncError(f"failed to write version {new_version} to version.yml")
330
+ raise VersionSyncError(t("version_manager.err_write_failed", version=new_version))
329
331
  today = datetime.date.today().isoformat()
330
332
  user = os.environ.get("GITHUB_ACTOR", "")
331
333
  if not user:
@@ -365,16 +367,16 @@ _XML_TOKEN_RE = re.compile(
365
367
 
366
368
 
367
369
  def _pom_text_span(text, path):
368
- """루트(<project>) 기준 경로의 요소 텍스트 구간 (start, end). 없으면 None.
369
- <parent>·<dependencies> 안의 <version>은 다른 아티팩트 버전이라 깊이로 구분한다."""
370
+ """Text span (start, end) of the element at `path` under the root (<project>). None if absent.
371
+ A <version> inside <parent>/<dependencies> is another artifact's version, so depth tells them apart."""
370
372
  stack = []
371
373
  start = None
372
374
  target = list(path)
373
375
  for m in _XML_TOKEN_RE.finditer(text):
374
376
  name = m.group(2)
375
377
  if not name:
376
- continue # 주석·CDATA·선언
377
- if m.group(1): # 닫는 태그
378
+ continue # comment / CDATA / declaration
379
+ if m.group(1): # closing tag
378
380
  if start is not None and stack[1:] == target:
379
381
  return start, m.start()
380
382
  if stack:
@@ -401,24 +403,24 @@ def _pom_replace(text, path, value):
401
403
 
402
404
 
403
405
  def sync_maven(path_dir, new_version):
404
- """pom.xml의 프로젝트 자신의 <version>만 바꾼다. 하위 모듈은 루트를 가리키는
405
- <parent><version>(과 루트와 같던 자기 <version>)만 따라 올린다."""
406
+ """Only change the project's own <version> in pom.xml. A child module follows only the
407
+ <parent><version> that points at the root (and its own <version> if it equaled the root's)."""
406
408
  root_pom = Path(path_dir) / "pom.xml"
407
409
  if not root_pom.is_file():
408
410
  return False
409
411
  text = read_file(root_pom)
410
412
  old_version = _pom_text(text, ["version"])
411
413
  if old_version is None:
412
- log(f"WARNING: spring: {root_pom} has no project <version> (inherited from parent?) — skipping")
414
+ log(t("version_manager.warn_spring_no_version", pom=root_pom))
413
415
  return True
414
416
  if "${" in old_version:
415
- # ${revision} 같은 CI-friendly 버전은 프로퍼티 쪽에서 관리하므로 건드리지 않는다.
416
- log(f"WARNING: spring: {root_pom} <version> is a property ({old_version}) — skipping")
417
+ # CI-friendly versions such as ${revision} are managed in a property, so leave them alone.
418
+ log(t("version_manager.warn_spring_property", pom=root_pom, version=old_version))
417
419
  return True
418
420
  new_full = _keep_snapshot(old_version, new_version)
419
421
  new_text, _ = _pom_replace(text, ["version"], new_full)
420
422
  write_file(root_pom, new_text)
421
- log(f"updated: {root_pom}")
423
+ log(t("version_manager.updated", path=root_pom))
422
424
 
423
425
  root_artifact = _pom_text(text, ["artifactId"])
424
426
  for child in sorted(Path(path_dir).glob("*/pom.xml")):
@@ -431,7 +433,7 @@ def sync_maven(path_dir, new_version):
431
433
  if _pom_text(ctext, ["version"]) == old_version:
432
434
  ctext, _ = _pom_replace(ctext, ["version"], new_full)
433
435
  write_file(child, ctext)
434
- log(f"updated: {child}")
436
+ log(t("version_manager.updated", path=child))
435
437
  return True
436
438
 
437
439
 
@@ -440,12 +442,12 @@ _GRADLE_SHARED_BLOCKS = ("allprojects", "subprojects")
440
442
 
441
443
 
442
444
  def _gradle_code(line, quote=""):
443
- """한 줄에서 주석을 떼고 따옴표 안 글자는 공백으로 바꾼다. url 'https://…'의 `//`나
444
- 문자열 속 중괄호가 블록 깊이 계산을 흐트러뜨리지 않게 한다. detect.js와 같은 규칙.
445
+ """Strip the comment from a line and blank out quoted text, so the `//` in a url 'https://...' or
446
+ a brace inside a string cannot disturb block-depth counting. Same rule as detect.js.
445
447
 
446
- quote는 이 줄이 시작될 때 열려 있던 따옴표다. 삼중따옴표(\"\"\" ''')는 여러 줄에 걸치므로
447
- (코드, 줄 끝에서 열려 있는 따옴표)를 돌려줘 다음 줄이 이어받게 한다. 한 줄짜리 따옴표는
448
- 줄 끝에서 닫힌 것으로 본다."""
448
+ `quote` is the quote that was open when this line started. Triple quotes (\"\"\" ''') span lines, so
449
+ return (code, quote still open at end of line) for the next line to continue. A single-line quote
450
+ is treated as closed at end of line."""
449
451
  out, i = [], 0
450
452
  while i < len(line):
451
453
  ch = line[i]
@@ -468,14 +470,14 @@ def _gradle_code(line, quote=""):
468
470
 
469
471
 
470
472
  def _gradle_version_matches(text):
471
- """프로젝트 버전 줄의 match 목록. 읽기와 동기화가 이 한 곳을 공유해 같은 줄만 다룬다.
473
+ """Matches of the project version lines. Reading and syncing share this one place so they touch the same lines.
472
474
 
473
- 들여쓰지 않은 `version =`이 있으면 그것만 쓴다. 없을 때만 allprojects/subprojects 블록 안의
474
- 들여쓴 줄을 인정한다 — `node { version = '20.11.0' }` 같은 플러그인 설정 블록을
475
- 프로젝트 버전으로 오인하면 버전이 뛰고 빌드 설정이 깨진다."""
475
+ If an unindented `version =` exists, only that is used. Only when there is none are indented lines
476
+ inside allprojects/subprojects blocks accepted — mistaking a plugin config block such as `node { version = '20.11.0' }`
477
+ for the project version would make the version jump and break the build config."""
476
478
  top, shared = [], []
477
- stack = [] # 열린 블록 이름. 줄 끝이 `{`로 끝나는 줄의 마지막 단어
478
- quote = "" # 여러 줄 문자열 안이면 그 따옴표. 문자열 속 줄은 코드가 아니다
479
+ stack = [] # open block names: the last word of each line ending in `{`
480
+ quote = "" # the open quote while inside a multi-line string; lines inside a string are not code
479
481
  pos = 0
480
482
  for line in text.splitlines(keepends=True):
481
483
  m = None if quote else _GRADLE_VERSION_RE.match(line)
@@ -507,27 +509,27 @@ def sync_spring(path_dir, new_version):
507
509
  has_pom = sync_maven(path_dir, new_version)
508
510
  if not candidates:
509
511
  if not has_pom:
510
- log(f"WARNING: spring: no build.gradle(.kts) or pom.xml found under {path_dir} — skipping")
512
+ log(t("version_manager.warn_spring_no_files", dir=path_dir))
511
513
  return
512
514
  for gradle_file in candidates:
513
515
  text = read_file(gradle_file)
514
- # 줄 시작의 `version =`만 프로젝트 버전이다. 앵커가 없으면 kotlin_version 같은
515
- # 의존성 버전 변수까지 함께 바뀌어 빌드가 깨진다.
516
+ # Only a `version =` at the start of a line is the project version. Without the anchor,
517
+ # dependency version variables such as kotlin_version would change too and break the build.
516
518
  matches = _gradle_version_matches(text)
517
519
  new_text = text
518
- # 뒤에서부터 바꿔 앞쪽 span이 밀리지 않게 한다.
520
+ # Replace from the end so earlier spans do not shift.
519
521
  for m in reversed(matches):
520
522
  replaced = f"{m.group(1)}{m.group(2)}{_keep_snapshot(m.group(3), new_version)}{m.group(2)}"
521
523
  new_text = new_text[:m.start()] + replaced + new_text[m.end():]
522
524
  if not matches:
523
- log(f"WARNING: spring: no `version = '...'` line in {gradle_file} — skipping")
525
+ log(t("version_manager.warn_spring_no_gradle_line", file=gradle_file))
524
526
  continue
525
527
  write_file(gradle_file, new_text)
526
- log(f"updated: {gradle_file}")
528
+ log(t("version_manager.updated", path=gradle_file))
527
529
 
528
530
 
529
531
  def _pubspec_build_number(path_dir):
530
- """pubspec.yaml `version: x.y.z+N`의 N. 없으면 None."""
532
+ """N in pubspec.yaml `version: x.y.z+N`. None if absent."""
531
533
  target = Path(path_dir) / "pubspec.yaml"
532
534
  if not target.is_file():
533
535
  return None
@@ -536,9 +538,9 @@ def _pubspec_build_number(path_dir):
536
538
 
537
539
 
538
540
  def get_reconciled_version_code():
539
- """version.yml의 version_code와 pubspec.yaml의 +N 중 큰 값.
540
- 로컬 수동 배포 등으로 pubspec 쪽이 앞서 있으면 스토어는 더 작은 build number를
541
- 거부하므로, 그 값을 version.yml에 반영해 역행을 막는다."""
541
+ """The larger of version.yml version_code and the +N in pubspec.yaml.
542
+ If pubspec is ahead (e.g. after a manual local release), the store rejects a lower build number,
543
+ so that value is adopted into version.yml to prevent a regression."""
542
544
  code = int(get_version_code())
543
545
  types = get_project_types_csv()
544
546
  pubspec_codes = [
@@ -546,7 +548,7 @@ def get_reconciled_version_code():
546
548
  if n is not None
547
549
  ]
548
550
  if pubspec_codes and max(pubspec_codes) > code:
549
- log(f"pubspec.yaml build number {max(pubspec_codes)} is ahead of version_code {code} — adopting it")
551
+ log(t("version_manager.pubspec_ahead", build=max(pubspec_codes), code=code))
550
552
  code = max(pubspec_codes)
551
553
  set_version_code(code)
552
554
  return code
@@ -555,7 +557,7 @@ def get_reconciled_version_code():
555
557
  def sync_flutter(path_dir, new_version, version_code):
556
558
  target = Path(path_dir) / "pubspec.yaml"
557
559
  if not target.is_file():
558
- log(f"WARNING: flutter: {target} not found — skipping")
560
+ log(t("version_manager.warn_flutter_missing", target=target))
559
561
  return
560
562
  text = read_file(target)
561
563
  full_version = f"{new_version}+{version_code}"
@@ -565,7 +567,7 @@ def sync_flutter(path_dir, new_version, version_code):
565
567
  else:
566
568
  new_text = text.rstrip("\n") + f"\nversion: {full_version}\n"
567
569
  write_file(target, new_text)
568
- log(f"updated: {target}")
570
+ log(t("version_manager.updated", path=target))
569
571
 
570
572
 
571
573
  def _json_indent(text):
@@ -580,20 +582,20 @@ def _json_indent(text):
580
582
 
581
583
  def sync_json_version(target, new_version, key_path):
582
584
  if not target.is_file():
583
- log(f"WARNING: {target} not found — skipping")
585
+ log(t("version_manager.warn_target_missing", target=target))
584
586
  return
585
587
  raw = read_file(target)
586
588
  try:
587
589
  data = json.loads(raw)
588
590
  except json.JSONDecodeError as e:
589
- # 건너뛰고 성공으로 끝내면 태그·version.yml과 패키지 버전이 조용히 어긋난다.
590
- raise VersionSyncError(f"{target} is not valid JSON ({e}) — cannot write version")
591
+ # Skipping and reporting success would silently leave the tag/version.yml and the package version out of step.
592
+ raise VersionSyncError(t("version_manager.err_not_json", target=target, error=e))
591
593
  node = data
592
594
  for k in key_path[:-1]:
593
595
  node = node.setdefault(k, {})
594
596
  node[key_path[-1]] = new_version
595
597
  write_file(target, json.dumps(data, indent=_json_indent(raw), ensure_ascii=False) + "\n")
596
- log(f"updated: {target}")
598
+ log(t("version_manager.updated", path=target))
597
599
 
598
600
 
599
601
  _TOML_HEADER_RE = re.compile(r'^[ \t]*\[+[ \t]*([^\]\n]+?)[ \t]*\]+[ \t]*(?:#.*)?$', re.MULTILINE)
@@ -601,8 +603,8 @@ _TOML_VERSION_RE = re.compile(r'^[ \t]*version[ \t]*=[ \t]*([\'"])([^\'"\n]*)\1'
601
603
 
602
604
 
603
605
  def _pyproject_version_span(text):
604
- """[project] 또는 [tool.poetry] 섹션의 version 값 구간. [tool.*] 등 다른 섹션의
605
- `version =`은 도구 설정이라 패키지 버전으로 쓰지 않는다."""
606
+ """Value span of `version` in the [project] or [tool.poetry] section. A `version =` in other sections
607
+ such as [tool.*] is tool config, not the package version."""
606
608
  headers = list(_TOML_HEADER_RE.finditer(text))
607
609
  for i, h in enumerate(headers):
608
610
  if h.group(1) not in ("project", "tool.poetry"):
@@ -614,13 +616,13 @@ def _pyproject_version_span(text):
614
616
  return None
615
617
 
616
618
 
617
- # setup(version="x.y.z"). python_requires·python_version 같은 다른 키는 앞 글자로 거른다.
619
+ # setup(version="x.y.z"). Other keys such as python_requires / python_version are filtered out by the preceding character.
618
620
  _SETUP_PY_VERSION_RE = re.compile(r"""(?<![\w.])version\s*=\s*(['"])([^'"]+)\1""")
619
621
 
620
622
 
621
623
  def _python_version_spans(path_dir):
622
- """버전을 읽고 쓰는 파일과 값 구간 목록. 읽을 때는 앞쪽(pyproject.toml → setup.py)이 우선이다.
623
- setup.py만 쓰는 프로젝트도 설치 때 버전을 읽으므로, 릴리스 때도 같은 파일을 읽고 써야 한다."""
624
+ """Files and value spans for reading/writing the version. When reading, earlier entries win (pyproject.toml -> setup.py).
625
+ Projects using only setup.py also have their version read at install time, so releases must read and write the same file."""
624
626
  spans = []
625
627
  pyproject = Path(path_dir) / "pyproject.toml"
626
628
  if pyproject.is_file():
@@ -638,21 +640,21 @@ def _python_version_spans(path_dir):
638
640
  def sync_python(path_dir, new_version):
639
641
  spans = _python_version_spans(path_dir)
640
642
  if not spans:
641
- # dynamic = ["version"] 등 버전을 파일에 두지 않는 구성은 쓸 곳이 없다.
642
- log(f"WARNING: python: no version in pyproject.toml [project]/[tool.poetry] or setup.py under {path_dir} — skipping")
643
+ # Setups that keep the version out of the file (e.g. dynamic = ["version"]) have nowhere to write it.
644
+ log(t("version_manager.warn_python_no_version", dir=path_dir))
643
645
  return
644
646
  for target, (start, end) in spans:
645
647
  text = read_file(target)
646
648
  write_file(target, text[:start] + new_version + text[end:])
647
- log(f"updated: {target}")
649
+ log(t("version_manager.updated", path=target))
648
650
 
649
651
 
650
652
  _PLIST_VERSION_RE = re.compile(r'(<key>CFBundleShortVersionString</key>\s*<string>)([^<]*)(</string>)')
651
653
 
652
654
 
653
655
  def _rn_app_plists(ios_dir):
654
- """앱 타깃의 Info.plist만 이름순으로. Pods·빌드 산출물·테스트 타깃은 우리 버전이 아니다.
655
- 읽기와 동기화가 같은 파일 집합을 보도록 한 곳에서 고른다."""
656
+ """Only the app target Info.plist files, sorted by name. Pods, build output and test targets do not carry our version.
657
+ Chosen in one place so reading and syncing see the same set of files."""
656
658
  skip = {"Pods", "build"}
657
659
  return [
658
660
  p for p in sorted(ios_dir.glob("*/Info.plist"))
@@ -669,35 +671,35 @@ def sync_react_native(path_dir, new_version):
669
671
  m = _PLIST_VERSION_RE.search(text)
670
672
  if not m:
671
673
  continue
672
- # $(MARKETING_VERSION) 같은 빌드 변수 참조는 Xcode 설정이 원본이라 덮어쓰지 않는다.
674
+ # A build-variable reference such as $(MARKETING_VERSION) is owned by the Xcode settings, so do not overwrite it.
673
675
  if m.group(2).strip().startswith("$("):
674
- log(f"skipped: {plist_file} — CFBundleShortVersionString references a build variable")
676
+ log(t("version_manager.skipped_plist", file=plist_file))
675
677
  continue
676
678
  new_text = _PLIST_VERSION_RE.sub(lambda mm: mm.group(1) + new_version + mm.group(3), text)
677
679
  write_file(plist_file, new_text)
678
- log(f"updated: {plist_file}")
680
+ log(t("version_manager.updated", path=plist_file))
679
681
  found_plist = True
680
682
  else:
681
- log(f"WARNING: react-native: {ios_dir} not found — skipping")
683
+ log(t("version_manager.warn_rn_ios_missing", dir=ios_dir))
682
684
 
683
685
  gradle_file = Path(path_dir) / "android" / "app" / "build.gradle"
684
686
  if gradle_file.is_file():
685
687
  text = read_file(gradle_file)
686
688
  new_text = re.sub(r'versionName\s+"[^"]*"', f'versionName "{new_version}"', text)
687
689
  write_file(gradle_file, new_text)
688
- log(f"updated: {gradle_file}")
690
+ log(t("version_manager.updated", path=gradle_file))
689
691
  else:
690
- log(f"WARNING: react-native: {gradle_file} not found — skipping")
692
+ log(t("version_manager.warn_rn_gradle_missing", file=gradle_file))
691
693
 
692
694
  if not found_plist and not gradle_file.is_file():
693
- log(f"WARNING: react-native: no target files found under {path_dir}")
695
+ log(t("version_manager.warn_rn_no_targets", dir=path_dir))
694
696
 
695
697
 
696
698
  def sync_for_type(project_type, new_version, version_code_getter):
697
699
  path_dir = get_type_path(project_type)
698
700
  handler = TYPE_HANDLERS.get(project_type)
699
701
  if handler is None:
700
- log(f"WARNING: unknown project type: {project_type} — skipping")
702
+ log(t("version_manager.warn_unknown_type", type=project_type))
701
703
  return
702
704
  handler.sync(path_dir, new_version, version_code_getter)
703
705
 
@@ -707,17 +709,17 @@ def sync_all_project_files(new_version):
707
709
  if not types:
708
710
  # No silent fallback: an unreadable project_types used to degrade to
709
711
  # "basic" and skip every sync without a word.
710
- raise SystemExit("ERROR: version.yml has no readable project_types — cannot sync project files")
712
+ raise SystemExit(t("version_manager.err_no_types_sync"))
711
713
  errors = []
712
- for t in types:
713
- # 한 타입이 실패해도 나머지는 맞춰 두고, 실패는 끝에서 모아 종료 코드로 알린다.
714
+ for ptype in types:
715
+ # Keep syncing the other types when one fails; collect failures and report them via the exit code at the end.
714
716
  try:
715
- sync_for_type(t, new_version, get_reconciled_version_code)
717
+ sync_for_type(ptype, new_version, get_reconciled_version_code)
716
718
  except VersionSyncError as e:
717
- log(f"ERROR: {t}: {e}")
718
- errors.append(t)
719
+ log(t("version_manager.err_type", type=ptype, error=e))
720
+ errors.append(ptype)
719
721
  if errors:
720
- raise VersionSyncError(f"project file sync failed for: {', '.join(errors)}")
722
+ raise VersionSyncError(t("version_manager.err_sync_failed", types=", ".join(errors)))
721
723
 
722
724
 
723
725
  README_VERSION_LINE_RE = re.compile(
@@ -725,11 +727,11 @@ README_VERSION_LINE_RE = re.compile(
725
727
 
726
728
 
727
729
  def update_readme_version(new_version, path="README.md"):
728
- """AUTO-VERSION-SECTION 마커 바로 아래 버전 줄만 새 버전으로 바꾼다.
730
+ """Rewrite only the version line right below the AUTO-VERSION-SECTION marker.
729
731
 
730
- README 갱신 워크플로우는 태그를 만든 뒤에 돌기 때문에, 버전 확정 커밋에 README가 함께
731
- 들어가지 않으면 태그 시점 README가 한 버전 전을 가리킨다. 마커·줄 삽입 같은 나머지 일은
732
- 그 워크플로우에 맡기고, 여기서는 이미 있는 줄만 고친다(날짜 표기 여부도 그대로 둔다)."""
732
+ The README update workflow runs after the tag is created, so unless the README is part of the
733
+ version-confirm commit, the README at the tag points one version behind. Everything else, such as
734
+ inserting the marker or line, is left to that workflow; here only an existing line is fixed (date presence is kept)."""
733
735
  p = Path(path)
734
736
  if not p.is_file():
735
737
  return False
@@ -747,7 +749,7 @@ def update_readme_version(new_version, path="README.md"):
747
749
  return False
748
750
  lines[i + 1] = new_line
749
751
  write_file(p, "\n".join(lines))
750
- log(f"README.md version line -> v{new_version}")
752
+ log(t("version_manager.readme_updated", version=new_version))
751
753
  return True
752
754
  return False
753
755
 
@@ -763,7 +765,7 @@ def update_all_versions(new_version):
763
765
  # ===================================================================
764
766
 
765
767
  def _read_spring(path_dir):
766
- # build.gradle에 버전이 없으면(allprojects 설정만 있는 경우 등) build.gradle.kts → pom.xml 순으로 이어 본다.
768
+ # If build.gradle has no version (e.g. only an allprojects config), try build.gradle.kts, then pom.xml.
767
769
  for name in ("build.gradle", "build.gradle.kts"):
768
770
  p = Path(path_dir) / name
769
771
  if p.is_file():
@@ -796,9 +798,9 @@ def _read_package_json(path_dir):
796
798
 
797
799
 
798
800
  def _read_react_native(path_dir):
799
- """릴리스 때 쓰는 파일(ios/<앱>/Info.plist, android/app/build.gradle)에서 읽는다.
800
- package.json version은 동기화하지 않으므로 기준으로 삼으면 매 릴리스마다 어긋난다.
801
- $(MARKETING_VERSION) 참조나 템플릿 기본값 "1.0"처럼 x.y.z가 아닌 값은 건너뛴다."""
801
+ """Read from the files used at release time (ios/<app>/Info.plist, android/app/build.gradle).
802
+ package.json version is not synced, so using it as the reference would drift on every release.
803
+ Values that are not x.y.z, such as a $(MARKETING_VERSION) reference or the template default "1.0", are skipped."""
802
804
  ios_dir = Path(path_dir) / "ios"
803
805
  if ios_dir.is_dir():
804
806
  for plist in _rn_app_plists(ios_dir):
@@ -839,8 +841,8 @@ def _sync_none(path_dir, new_version, version_code_getter):
839
841
 
840
842
 
841
843
  class TypeHandler(NamedTuple):
842
- """타입별 버전 파일 처리. read(path_dir)는 파일의 버전(없으면 None),
843
- sync(path_dir, new_version, version_code_getter)는 새 버전을 파일에 쓴다."""
844
+ """Per-type version file handling. read(path_dir) returns the version in the file (None if absent),
845
+ sync(path_dir, new_version, version_code_getter) writes the new version to the file."""
844
846
  read: Callable
845
847
  sync: Callable
846
848
 
@@ -850,11 +852,11 @@ _PACKAGE_JSON = TypeHandler(
850
852
  sync=lambda d, v, _code: sync_json_version(Path(d) / "package.json", v, ["version"]),
851
853
  )
852
854
 
853
- # 새 타입은 여기 한 줄만 추가하면 읽기(sync 비교)와 쓰기가 함께 따라온다.
854
- # 버전 파일이 없는 타입(basic·go)은 version.yml만 쓰도록 빈 핸들러를 둔다.
855
+ # Adding one line here is enough for a new type: both reading (sync comparison) and writing follow.
856
+ # Types without a version file (basic, go) get an empty handler so only version.yml is written.
855
857
  TYPE_HANDLERS = {
856
858
  "spring": TypeHandler(read=_read_spring, sync=lambda d, v, _code: sync_spring(d, v)),
857
- # build number가 필요한 타입만 version_code를 계산한다 (pubspec 조정 부수효과가 있다).
859
+ # Only types that need a build number compute version_code (this has a pubspec-adjusting side effect).
858
860
  "flutter": TypeHandler(read=_read_flutter, sync=lambda d, v, code: sync_flutter(d, v, code())),
859
861
  "react": _PACKAGE_JSON,
860
862
  "next": _PACKAGE_JSON,
@@ -878,7 +880,7 @@ def get_project_file_version(project_type):
878
880
  if handler is not None:
879
881
  version = handler.read(path_dir)
880
882
  except Exception as e:
881
- log(f"WARNING: failed reading project file for {project_type}: {e}")
883
+ log(t("version_manager.warn_read_failed", type=project_type, error=e))
882
884
  version = None
883
885
 
884
886
  if not version:
@@ -890,32 +892,32 @@ def sync_versions():
890
892
  yml_version = get_current_version()
891
893
  types = get_project_types_csv()
892
894
  if not types:
893
- raise SystemExit("ERROR: version.yml has no readable project_types — cannot sync versions")
895
+ raise SystemExit(t("version_manager.err_no_types_check"))
894
896
  primary_type = types[0]
895
897
  project_version = get_project_file_version(primary_type)
896
898
 
897
- log("Version sync check")
898
- log(f" version.yml: {yml_version}")
899
- log(f" project file: {project_version}")
899
+ log(t("version_manager.sync_check"))
900
+ log(t("version_manager.sync_yml_version", version=yml_version))
901
+ log(t("version_manager.sync_project_version", version=project_version))
900
902
 
901
903
  if yml_version != project_version:
902
904
  if validate_version(yml_version) and validate_version(project_version):
903
905
  higher = get_higher_version(yml_version, project_version)
904
- log(f"Version mismatch detected, syncing to higher version: {higher}")
906
+ log(t("version_manager.sync_mismatch", version=higher))
905
907
  if higher != yml_version:
906
908
  update_version_yml(higher)
907
909
  if higher != project_version:
908
910
  sync_all_project_files(higher)
909
911
  return higher
910
912
  else:
911
- log("WARNING: version format invalid, cannot sync")
913
+ log(t("version_manager.warn_format_invalid"))
912
914
  return yml_version
913
915
  else:
914
916
  types = get_project_types_csv()
915
917
  if types:
916
- log(f"Multi-type — reconciling all type files to version.yml version: {yml_version}")
918
+ log(t("version_manager.sync_multi", version=yml_version))
917
919
  sync_all_project_files(yml_version)
918
- log(f"Version already in sync: {yml_version}")
920
+ log(t("version_manager.sync_ok", version=yml_version))
919
921
  return yml_version
920
922
 
921
923
 
@@ -950,11 +952,11 @@ def cmd_increment(args):
950
952
  require_version_yml()
951
953
  current_version = sync_versions()
952
954
  if not validate_version(current_version):
953
- log(f"ERROR: invalid version format: {current_version}")
955
+ log(t("version_manager.err_invalid_version", version=current_version))
954
956
  return 1
955
957
  bump = getattr(args, "bump", None) or "patch"
956
958
  new_version = increment_version(current_version, bump)
957
- # build number를 먼저 올려야 이어지는 pubspec 동기화에 새 값이 함께 기록된다.
959
+ # Bump the build number first so the following pubspec sync writes the new value too.
958
960
  set_version_code(get_reconciled_version_code() + 1)
959
961
  update_all_versions(new_version)
960
962
 
@@ -966,7 +968,7 @@ def cmd_set(args):
966
968
  require_version_yml()
967
969
  new_version = args.version
968
970
  if not validate_version(new_version):
969
- log(f"ERROR: invalid version format: {new_version} (must be x.y.z)")
971
+ log(t("version_manager.err_invalid_version_xyz", version=new_version))
970
972
  return 1
971
973
  update_all_versions(new_version)
972
974
  print(new_version)
@@ -988,7 +990,7 @@ def build_parser():
988
990
  sub.add_parser("get-code")
989
991
  p_increment = sub.add_parser("increment")
990
992
  p_increment.add_argument("--bump", choices=["major", "minor", "patch"], default="patch",
991
- help="승격 폭 (기본 patch — 지정 안 하면 기존 동작과 동일)")
993
+ help=t("version.bump_help"))
992
994
  sub.add_parser("increment-code")
993
995
  sub.add_parser("sync")
994
996
 
@@ -999,6 +1001,7 @@ def build_parser():
999
1001
 
1000
1002
 
1001
1003
  def main(argv=None):
1004
+ use_utf8_output()
1002
1005
  parser = build_parser()
1003
1006
  args = parser.parse_args(argv)
1004
1007
 
@@ -1014,7 +1017,7 @@ def main(argv=None):
1014
1017
  try:
1015
1018
  return handler(args)
1016
1019
  except VersionSyncError as e:
1017
- log(f"ERROR: {e}")
1020
+ log(t("version_manager.err_generic", error=e))
1018
1021
  return 1
1019
1022
 
1020
1023
 
@@ -34,6 +34,7 @@
34
34
  version: "{{VERSION}}"
35
35
  version_code: {{VERSION_CODE}} # app build number
36
36
  project_types: {{PROJECT_TYPES}} # first entry is primary
37
+ language: "{{LANGUAGE}}" # message language: en | ko
37
38
  {{PROJECT_PATHS}}
38
39
  metadata:
39
40
  last_updated: "{{NOW}}"
@@ -52,6 +53,6 @@ metadata:
52
53
  options:
53
54
  semver_auto: {{OPT_SEMVER_AUTO}}
54
55
  copilot_ai: {{OPT_COPILOT_AI}}
55
- deploy_style: "{{OPT_DEPLOY_STYLE}}" # simple | nginx | traefik | none (서버 배포 워크플로우)
56
+ deploy_style: "{{OPT_DEPLOY_STYLE}}" # simple | nginx | traefik | none (server deploy workflows)
56
57
  {{TYPE_OPTIONS}}
57
58
  {{DEPLOY}}