project-auto-wizard 0.13.2 → 0.14.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 (112) hide show
  1. package/README.md +1 -1
  2. package/bin/project-auto-wizard.js +18 -4
  3. package/package.json +2 -2
  4. package/payload/config/breaking-changes.json +28 -14
  5. package/payload/config/wizard-prompts.yml +102 -46
  6. package/payload/flutter-app/android/fastlane/Fastfile.playstore +44 -32
  7. package/payload/flutter-app/ios/ExportOptions.plist +8 -8
  8. package/payload/flutter-app/ios/fastlane/Fastfile +57 -43
  9. package/payload/scripts/changelog_manager.py +296 -257
  10. package/payload/scripts/issue_helper.py +54 -50
  11. package/payload/scripts/messages.py +2279 -0
  12. package/payload/scripts/truncate_release_notes.py +7 -6
  13. package/payload/scripts/version_manager.py +64 -61
  14. package/payload/version.yml.template +2 -1
  15. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +17 -12
  16. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +59 -34
  17. package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
  18. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +17 -10
  19. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +54 -44
  20. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +24 -17
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +194 -184
  22. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +226 -213
  23. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +46 -42
  24. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +334 -241
  25. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +152 -128
  26. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +246 -188
  27. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +353 -238
  28. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +96 -85
  29. package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -36
  30. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +945 -508
  31. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +155 -133
  32. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +76 -60
  33. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +97 -77
  34. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +62 -47
  35. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +942 -505
  36. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +148 -126
  37. package/payload/workflows/react/PROJECT-REACT-CI.yaml +86 -70
  38. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +112 -92
  39. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +112 -82
  40. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +180 -150
  41. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +161 -131
  42. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +970 -549
  43. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +193 -162
  44. package/src/cli/args.js +98 -52
  45. package/src/cli/help.js +8 -41
  46. package/src/commands/doctor.js +85 -89
  47. package/src/commands/dry-run.js +63 -43
  48. package/src/commands/full.js +79 -77
  49. package/src/commands/install-settings.js +16 -15
  50. package/src/commands/interactive-flutter.js +20 -19
  51. package/src/commands/interactive.js +135 -132
  52. package/src/commands/purge.js +47 -42
  53. package/src/commands/status.js +36 -36
  54. package/src/commands/uninstall.js +43 -41
  55. package/src/context.js +12 -11
  56. package/src/core/assets.js +12 -11
  57. package/src/core/baseline.js +27 -27
  58. package/src/core/branches.js +29 -27
  59. package/src/core/branding.js +7 -6
  60. package/src/core/breaking-check.js +22 -21
  61. package/src/core/breaking.js +15 -8
  62. package/src/core/copy/app-files.js +5 -4
  63. package/src/core/copy/flutter-app.js +8 -8
  64. package/src/core/copy/gitignore.js +58 -50
  65. package/src/core/copy/readme.js +45 -35
  66. package/src/core/copy/simple.js +16 -15
  67. package/src/core/copy/workflows.js +131 -124
  68. package/src/core/deploy-style.js +51 -48
  69. package/src/core/detect-fs.js +57 -57
  70. package/src/core/detect.js +67 -65
  71. package/src/core/errors.js +2 -2
  72. package/src/core/flutter-doctor.js +17 -16
  73. package/src/core/flutter-hooks.js +33 -32
  74. package/src/core/flutter-options.js +34 -33
  75. package/src/core/fsutil.js +12 -12
  76. package/src/core/installed-stores.js +4 -4
  77. package/src/core/logger.js +42 -40
  78. package/src/core/paths-resolve.js +88 -86
  79. package/src/core/paths.js +10 -10
  80. package/src/core/release-options.js +8 -8
  81. package/src/core/removal-exec.js +14 -13
  82. package/src/core/removal-plan.js +37 -37
  83. package/src/core/types.js +47 -47
  84. package/src/core/verify.js +44 -38
  85. package/src/core/version-yml.js +76 -68
  86. package/src/core/wizard-env.js +46 -46
  87. package/src/core/wizard-labels.js +63 -41
  88. package/src/i18n/catalog/en/cli.js +50 -0
  89. package/src/i18n/catalog/en/commands.js +187 -0
  90. package/src/i18n/catalog/en/copy.js +19 -0
  91. package/src/i18n/catalog/en/core.js +74 -0
  92. package/src/i18n/catalog/en/core2.js +15 -0
  93. package/src/i18n/catalog/en/ui.js +221 -0
  94. package/src/i18n/catalog/en.js +12 -0
  95. package/src/i18n/catalog/index.js +5 -0
  96. package/src/i18n/catalog/ko/cli.js +50 -0
  97. package/src/i18n/catalog/ko/commands.js +187 -0
  98. package/src/i18n/catalog/ko/copy.js +19 -0
  99. package/src/i18n/catalog/ko/core.js +74 -0
  100. package/src/i18n/catalog/ko/core2.js +15 -0
  101. package/src/i18n/catalog/ko/ui.js +221 -0
  102. package/src/i18n/catalog/ko.js +10 -0
  103. package/src/i18n/index.js +44 -0
  104. package/src/i18n/languages.js +9 -0
  105. package/src/index.js +128 -94
  106. package/src/ui/ansi.js +7 -7
  107. package/src/ui/banner.js +7 -6
  108. package/src/ui/env-plan.js +82 -81
  109. package/src/ui/prompts.js +97 -122
  110. package/src/ui/readline-engine.js +59 -58
  111. package/src/ui/status-cards.js +37 -31
  112. package/src/ui/summary.js +89 -88
@@ -42,6 +42,8 @@ import urllib.error
42
42
  import urllib.request
43
43
  from datetime import datetime, timedelta, timezone
44
44
 
45
+ from messages import t, use_utf8_output
46
+
45
47
  KST = timezone(timedelta(hours=9))
46
48
  COMMENT_MARKER_DEFAULT = "<!-- project-auto-wizard issue helper -->"
47
49
  API_BASE = "https://api.github.com"
@@ -52,7 +54,7 @@ def log(message):
52
54
 
53
55
 
54
56
  # ===================================================================
55
- # 정규화 (순수 함수 — GitHub API 호출 없음)
57
+ # Normalization (pure functions — no GitHub API calls)
56
58
  # ===================================================================
57
59
 
58
60
  def extract_issue_number(issue_url):
@@ -79,10 +81,10 @@ _ZWJ = "‍"
79
81
 
80
82
 
81
83
  def _is_removable_char(ch):
82
- # 원본(TS) 정규식 \p{So}|\p{C}|️|‍ 이식.
83
- # Python re는 \p{...}를 지원하지 않으므로 unicodedata.category()로 대체:
84
- # "So"(Symbol, other) 또는 "C"로 시작(Cc/Cf/Co/Cs/Cn)하면 제거 대상.
85
- # 변형 선택자(U+FE0F, 카테고리 Mn)는 위 두 조건에 안 걸리므로 명시적으로 추가.
84
+ # Port of the original (TS) regex \p{So}|\p{C}|️|‍.
85
+ # Python's re has no \p{...}, so unicodedata.category() is used instead:
86
+ # "So" (Symbol, other) or any category starting with "C" (Cc/Cf/Co/Cs/Cn) is removed.
87
+ # The variation selector (U+FE0F, category Mn) matches neither, so it is listed explicitly.
86
88
  if ch in (_VARIATION_SELECTOR, _ZWJ):
87
89
  return True
88
90
  category = unicodedata.category(ch)
@@ -99,11 +101,12 @@ def format_date_yyyymmdd(dt):
99
101
  return dt.strftime("%Y%m%d")
100
102
 
101
103
 
102
- # 유니코드 문자·숫자는 모두 살린다. 한글·영문만 남기면 일본어 등 다른 언어 제목이
103
- # 통째로 사라져 브랜치명이 `_`로 끝나고 커밋 제목이 비었다.
104
+ # Keep every Unicode letter and digit. Keeping only Hangul and Latin made titles in
105
+ # other languages (e.g. Japanese) vanish entirely, leaving a branch name ending in `_`
106
+ # and an empty commit title.
104
107
  _NON_WORD_RE = re.compile(r"[\W_]+")
105
108
  _MULTI_UNDERSCORE_RE = re.compile(r"_+")
106
- # 이모지·기호만 있는 제목처럼 정규화 결과가 비었을 때 쓰는 이름
109
+ # Name used when normalization leaves nothing (e.g. a title made only of emoji or symbols)
107
110
  FALLBACK_TITLE = "issue"
108
111
 
109
112
 
@@ -116,7 +119,7 @@ def normalize_title(title):
116
119
  def create_branch_name(issue_title, issue_number, date_yyyymmdd, branch_prefix, max_branch_length):
117
120
  normalized_title = normalize_title(issue_title) or FALLBACK_TITLE
118
121
  base = f"{date_yyyymmdd}_#{issue_number}_{normalized_title}"
119
- # 절단 지점이 구분자면 `_`로 끝나지 않게 정리한다.
122
+ # If the cut lands on a separator, do not leave the name ending in `_`.
120
123
  limited_base = base[:max_branch_length].rstrip("_") if max_branch_length > 0 else base
121
124
  return f"{branch_prefix}{limited_base}"
122
125
 
@@ -141,7 +144,7 @@ def normalize_all(title, issue_url, issue_number, date_yyyymmdd, branch_prefix,
141
144
 
142
145
 
143
146
  # ===================================================================
144
- # PR 본문 이슈 연결 (Closes #N) — 마커 블록 삽입/치환
147
+ # Linking issues in a PR body (Closes #N) — insert/replace a marker block
145
148
  # ===================================================================
146
149
 
147
150
  LINK_MARKER_START = "<!-- auto-issue-link:start -->"
@@ -158,11 +161,10 @@ def upsert_issue_links_in_body(body, issue_numbers, replace_existing):
158
161
  if not issue_numbers:
159
162
  return body, False
160
163
 
161
- # 완전한 START...END 블록이 있을 때만 "마커 있음"으로 취급한다 — START만
162
- # 있고 END가 없는 손상된 상태를 마커로 오인하면 _LINK_BLOCK_RE.sub()가
163
- # 아무 것도 치환하지 못한 채 changed=True만 반환해, replace_existing=True
164
- # 경로(release PR)에서 새 이슈 목록이 영구히 반영되지 않는 채로 조용히
165
- # "성공"처럼 보이는 버그가 생긴다.
164
+ # Treat the marker as present only for a complete START...END block. Mistaking a
165
+ # damaged state (START without END) for a marker makes _LINK_BLOCK_RE.sub() replace
166
+ # nothing yet return changed=True, so on the replace_existing=True path (release PR)
167
+ # the new issue list is never applied while the run silently looks successful.
166
168
  has_full_block = _LINK_BLOCK_RE.search(body) is not None
167
169
  if has_full_block and not replace_existing:
168
170
  return body, False
@@ -177,7 +179,7 @@ def upsert_issue_links_in_body(body, issue_numbers, replace_existing):
177
179
 
178
180
 
179
181
  # ===================================================================
180
- # GitHub REST API (urllib.request + GITHUB_TOKEN, 서드파티 의존 없음)
182
+ # GitHub REST API (urllib.request + GITHUB_TOKEN, no third-party dependencies)
181
183
  # ===================================================================
182
184
 
183
185
  def _headers(token, has_body):
@@ -224,7 +226,7 @@ def list_comments(owner, repo, issue_number, token):
224
226
  while url:
225
227
  status, page, link = _api_request("GET", url, token)
226
228
  if status >= 400:
227
- raise RuntimeError(f"이슈 코멘트 조회 실패({status}): {issue_number}")
229
+ raise RuntimeError(t("issue_helper.err_list_comments", status=status, issue_number=issue_number))
228
230
  comments.extend(page or [])
229
231
  url = _find_next_link(link)
230
232
  return comments
@@ -238,15 +240,15 @@ def upsert_comment(owner, repo, issue_number, token, marker, body):
238
240
  "PATCH", f"{API_BASE}/repos/{owner}/{repo}/issues/comments/{existing['id']}", token, {"body": body},
239
241
  )
240
242
  if status >= 400:
241
- raise RuntimeError(f"코멘트 갱신 실패({status})")
242
- log("기존 코멘트를 갱신했습니다.")
243
+ raise RuntimeError(t("issue_helper.err_update_comment", status=status))
244
+ log(t("issue_helper.comment_updated"))
243
245
  else:
244
246
  status, _, _ = _api_request(
245
247
  "POST", f"{API_BASE}/repos/{owner}/{repo}/issues/{issue_number}/comments", token, {"body": body},
246
248
  )
247
249
  if status >= 400:
248
- raise RuntimeError(f"코멘트 작성 실패({status})")
249
- log("새 코멘트를 작성했습니다.")
250
+ raise RuntimeError(t("issue_helper.err_create_comment", status=status))
251
+ log(t("issue_helper.comment_created"))
250
252
 
251
253
 
252
254
  def create_branch_if_needed(owner, repo, branch_name, base_branch, create_branch, token):
@@ -257,12 +259,12 @@ def create_branch_if_needed(owner, repo, branch_name, base_branch, create_branch
257
259
  if not base:
258
260
  status, repo_data, _ = _api_request("GET", f"{API_BASE}/repos/{owner}/{repo}", token)
259
261
  if status >= 400:
260
- raise RuntimeError(f"레포 정보 조회 실패({status})")
262
+ raise RuntimeError(t("issue_helper.err_repo_info", status=status))
261
263
  base = repo_data["default_branch"]
262
264
 
263
265
  status, ref_data, _ = _api_request("GET", f"{API_BASE}/repos/{owner}/{repo}/git/ref/heads/{base}", token)
264
266
  if status >= 400:
265
- raise RuntimeError(f"base 브랜치 ref 조회 실패({base}, {status})")
267
+ raise RuntimeError(t("issue_helper.err_base_ref", base=base, status=status))
266
268
  sha = ref_data["object"]["sha"]
267
269
 
268
270
  status, _, _ = _api_request(
@@ -270,28 +272,29 @@ def create_branch_if_needed(owner, repo, branch_name, base_branch, create_branch
270
272
  {"ref": f"refs/heads/{branch_name}", "sha": sha},
271
273
  )
272
274
  if status == 422:
273
- log(f"브랜치가 이미 존재함, 건너뜀: {branch_name}")
275
+ log(t("issue_helper.branch_exists", branch_name=branch_name))
274
276
  return
275
277
  if status >= 400:
276
- raise RuntimeError(f"브랜치 생성 실패({branch_name}, {status})")
277
- log(f"브랜치 생성됨: {branch_name}")
278
+ raise RuntimeError(t("issue_helper.err_create_branch", branch_name=branch_name, status=status))
279
+ log(t("issue_helper.branch_created", branch_name=branch_name))
278
280
 
279
281
 
280
282
  def filter_existing_issues(owner, repo, issue_numbers, token):
281
- """레포에 실제로 있는 이슈 번호만 남긴다. 브랜치명의 번호가 오타이거나 다른 레포
282
- 기준이면 없는 이슈를 Closes로 걸고 "연결 완료"로 기록하게 된다.
283
- 조회 자체가 실패(권한·일시 오류)하면 확인할 수 없으므로 그대로 둔다."""
283
+ """Keep only issue numbers that really exist in the repo. If the number in a branch
284
+ name is a typo or refers to another repo, we would attach Closes to a missing issue
285
+ and record it as "linked". If the lookup itself fails (permissions, transient
286
+ error) we cannot verify, so the number is kept as is."""
284
287
  kept = []
285
288
  for n in issue_numbers:
286
289
  status, data, _ = _api_request("GET", f"{API_BASE}/repos/{owner}/{repo}/issues/{n}", token)
287
290
  if status in (404, 410):
288
- print(f"::warning::이슈 #{n}이 레포에 없어 PR 본문 Closes 연결에서 제외합니다", file=sys.stderr)
291
+ print(t("issue_helper.warn_issue_missing", n=n), file=sys.stderr)
289
292
  continue
290
293
  if status < 400 and isinstance(data, dict) and data.get("pull_request"):
291
- print(f"::warning::#{n}은 이슈가 아니라 PR이라 Closes 연결에서 제외합니다", file=sys.stderr)
294
+ print(t("issue_helper.warn_is_pr", n=n), file=sys.stderr)
292
295
  continue
293
296
  if status >= 400:
294
- log(f"이슈 #{n} 확인 실패({status}) — 확인하지 못해 그대로 연결합니다")
297
+ log(t("issue_helper.issue_check_failed", n=n, status=status))
295
298
  kept.append(n)
296
299
  return kept
297
300
 
@@ -299,29 +302,29 @@ def filter_existing_issues(owner, repo, issue_numbers, token):
299
302
  def link_pr_issues(owner, repo, pr_number, issue_numbers, token, replace_existing):
300
303
  issue_numbers = filter_existing_issues(owner, repo, issue_numbers, token)
301
304
  if not issue_numbers:
302
- log(f"연결할 이슈가 없음 — 건너뜀 (PR #{pr_number})")
305
+ log(t("issue_helper.no_issue_to_link", pr_number=pr_number))
303
306
  return
304
307
 
305
308
  status, pr_data, _ = _api_request("GET", f"{API_BASE}/repos/{owner}/{repo}/pulls/{pr_number}", token)
306
309
  if status >= 400:
307
- raise RuntimeError(f"PR 조회 실패({status}): {pr_number}")
310
+ raise RuntimeError(t("issue_helper.err_get_pr", status=status, pr_number=pr_number))
308
311
  body = pr_data.get("body") or ""
309
312
 
310
313
  new_body, changed = upsert_issue_links_in_body(body, issue_numbers, replace_existing)
311
314
  if not changed:
312
- log(f"이슈 연결 변경 없음 — 건너뜀 (PR #{pr_number})")
315
+ log(t("issue_helper.link_unchanged", pr_number=pr_number))
313
316
  return
314
317
 
315
318
  status, _, _ = _api_request(
316
319
  "PATCH", f"{API_BASE}/repos/{owner}/{repo}/pulls/{pr_number}", token, {"body": new_body},
317
320
  )
318
321
  if status >= 400:
319
- raise RuntimeError(f"PR 본문 갱신 실패({status}): {pr_number}")
320
- log(f"이슈 연결 완료: {', '.join(f'#{n}' for n in issue_numbers)} (PR #{pr_number})")
322
+ raise RuntimeError(t("issue_helper.err_update_pr", status=status, pr_number=pr_number))
323
+ log(t("issue_helper.link_done", numbers=', '.join(f'#{n}' for n in issue_numbers), pr_number=pr_number))
321
324
 
322
325
 
323
326
  # ===================================================================
324
- # 이벤트 처리 / CLI
327
+ # Event handling / CLI
325
328
  # ===================================================================
326
329
 
327
330
  def _bool_env(name, default):
@@ -334,13 +337,13 @@ def _bool_env(name, default):
334
337
  def cmd_run():
335
338
  event_path = os.environ.get("GITHUB_EVENT_PATH")
336
339
  if not event_path:
337
- log("ERROR: GITHUB_EVENT_PATH가 없습니다.")
340
+ log(t("issue_helper.err_no_event_path"))
338
341
  return 1
339
342
  try:
340
343
  with open(event_path, "r", encoding="utf-8") as f:
341
344
  payload = json.load(f)
342
345
  except (OSError, json.JSONDecodeError) as e:
343
- log(f"ERROR: 이벤트 페이로드를 읽을 수 없습니다: {e}")
346
+ log(t("issue_helper.err_event_payload", error=e))
344
347
  return 1
345
348
 
346
349
  action = payload.get("action")
@@ -350,18 +353,18 @@ def cmd_run():
350
353
  is_edited_with_title = action == "edited" and "title" in changes
351
354
 
352
355
  if not is_opened and not is_edited_with_title:
353
- log("열림 또는 제목 변경 이벤트가 아님 → 종료")
356
+ log(t("issue_helper.not_relevant_event"))
354
357
  return 0
355
358
 
356
359
  repo_full = os.environ.get("GITHUB_REPOSITORY", "")
357
360
  if "/" not in repo_full or repo_full.count("/") != 1 or not all(repo_full.split("/")):
358
- log(f"ERROR: GITHUB_REPOSITORY 형식이 올바르지 않습니다: {repo_full!r}")
361
+ log(t("issue_helper.err_repo_format", repo=repr(repo_full)))
359
362
  return 1
360
363
  owner, repo = repo_full.split("/", 1)
361
364
 
362
365
  token = os.environ.get("GITHUB_TOKEN", "").strip()
363
366
  if not token:
364
- log("ERROR: GITHUB_TOKEN이 없습니다.")
367
+ log(t("issue_helper.err_no_token"))
365
368
  return 1
366
369
 
367
370
  raw_title = issue.get("title", "")
@@ -375,7 +378,7 @@ def cmd_run():
375
378
  max_branch_length = int(os.environ.get("ISSUE_HELPER_MAX_BRANCH_LENGTH") or "120")
376
379
  commit_template = (
377
380
  os.environ.get("ISSUE_HELPER_COMMIT_TEMPLATE")
378
- or "${issueTitle} : feat : {변경 사항에 대한 설명} ${issueUrl}"
381
+ or t("issue_helper.commit_template_default")
379
382
  )
380
383
  create_branch = _bool_env("ISSUE_HELPER_CREATE_BRANCH", False)
381
384
  base_branch = os.environ.get("ISSUE_HELPER_BASE_BRANCH", "").strip()
@@ -388,11 +391,11 @@ def cmd_run():
388
391
  body = (
389
392
  f"{comment_marker}\n"
390
393
  "## Issue Helper\n"
391
- "### 브랜치명\n"
394
+ f"### {t('issue_helper.branch_title')}\n"
392
395
  "```\n"
393
396
  f"{branch_name}\n"
394
397
  "```\n\n"
395
- "### 커밋 메시지\n"
398
+ f"### {t('issue_helper.commit_title')}\n"
396
399
  "```\n"
397
400
  f"{commit_message}\n"
398
401
  "```"
@@ -413,18 +416,18 @@ def cmd_run():
413
416
  def cmd_link_pr_issues(pr_number, issue_numbers_csv, replace_existing):
414
417
  repo_full = os.environ.get("GITHUB_REPOSITORY", "")
415
418
  if "/" not in repo_full or repo_full.count("/") != 1 or not all(repo_full.split("/")):
416
- log(f"ERROR: GITHUB_REPOSITORY 형식이 올바르지 않습니다: {repo_full!r}")
419
+ log(t("issue_helper.err_repo_format", repo=repr(repo_full)))
417
420
  return 1
418
421
  owner, repo = repo_full.split("/", 1)
419
422
 
420
423
  token = os.environ.get("GITHUB_TOKEN", "").strip()
421
424
  if not token:
422
- log("ERROR: GITHUB_TOKEN이 없습니다.")
425
+ log(t("issue_helper.err_no_token"))
423
426
  return 1
424
427
 
425
428
  issue_numbers = [n.strip() for n in issue_numbers_csv.split(",") if n.strip()]
426
429
  if not issue_numbers:
427
- log("이슈 번호가 없음 — 건너뜀")
430
+ log(t("issue_helper.no_issue_numbers"))
428
431
  return 0
429
432
 
430
433
  link_pr_issues(owner, repo, pr_number, issue_numbers, token, replace_existing)
@@ -449,6 +452,7 @@ def build_parser():
449
452
 
450
453
 
451
454
  def main(argv=None):
455
+ use_utf8_output()
452
456
  parser = build_parser()
453
457
  args = parser.parse_args(argv)
454
458
  try:
@@ -462,7 +466,7 @@ def main(argv=None):
462
466
  if args.command == "link-pr-issues":
463
467
  return cmd_link_pr_issues(args.pr, args.issue_numbers, args.replace)
464
468
  except Exception as e:
465
- log(f"실행 실패: {e}")
469
+ log(t("issue_helper.run_failed", error=e))
466
470
  return 1
467
471
  return 2
468
472