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
@@ -1,41 +1,41 @@
1
1
  # project-auto-wizard:managed-workflow
2
- # Flutter iOS 테스트용 TestFlight 배포 워크플로우
2
+ # Flutter iOS test TestFlight deploy workflow
3
3
  #
4
- # 이 워크플로우는 기능 브랜치에서 수동으로 실행하여
5
- # 테스트용 iOS 빌드를 TestFlight에 배포합니다.
4
+ # Run this workflow manually from a feature branch
5
+ # to deploy a test iOS build to TestFlight.
6
6
  #
7
- # 설정 파일(ios/fastlane/Fastfile, ios/ExportOptions.plist)은 project-auto-wizard가
8
- # 없을 때만 설치하며, 이미 있으면 덮어쓰지 않습니다.
7
+ # The config files (ios/fastlane/Fastfile, ios/ExportOptions.plist) are installed by
8
+ # project-auto-wizard only when missing; existing files are never overwritten.
9
9
  #
10
- # 주요 특징:
11
- # - workflow_dispatch로 수동 실행 또는 repository_dispatch로 트리거
12
- # - 버전은 version.yml에서 동적으로 읽어 사용 (운영 버전과 동일, 빌드 번호로 구분)
13
- # - 빌드 번호는 PR번호 + 카운트 또는 GITHUB_RUN_NUMBER 사용
14
- # - 브랜치명에서 이슈 번호 자동 추출 (YYYYMMDD_#이슈번호_내용 형식)
15
- # - GitHub API로 이슈 정보 가져와서 릴리즈 노트에 포함
10
+ # Highlights:
11
+ # - Runs manually via workflow_dispatch or is triggered by repository_dispatch
12
+ # - The version is read dynamically from version.yml (same as production; builds are told apart by build number)
13
+ # - The build number is PR number + count, or GITHUB_RUN_NUMBER
14
+ # - Extracts the issue number from the branch name automatically (YYYYMMDD_#issue-number_title format)
15
+ # - Fetches the issue through the GitHub API and includes it in the release notes
16
16
  #
17
- # 빌드 파이프라인:
18
- # 1. flutter build ios --no-codesign (Flutter 빌드)
19
- # 2. xcodebuild archive (Xcode 아카이브 생성)
20
- # 3. xcodebuild -exportArchive (IPA 생성, ExportOptions.plist 사용)
21
- # 4. fastlane upload_testflight (ios/fastlane/Fastfile 사용)
17
+ # Build pipeline:
18
+ # 1. flutter build ios --no-codesign (Flutter build)
19
+ # 2. xcodebuild archive (create the Xcode archive)
20
+ # 3. xcodebuild -exportArchive (create the IPA using ExportOptions.plist)
21
+ # 4. fastlane upload_testflight (uses ios/fastlane/Fastfile)
22
22
  #
23
- # 필수 GitHub Secrets
23
+ # Required GitHub Secrets
24
24
  #
25
- # Apple 인증서 & 프로비저닝 프로파일:
26
- # APPLE_CERTIFICATE_BASE64: .p12 인증서 (base64 인코딩)
27
- # APPLE_CERTIFICATE_PASSWORD: .p12 인증서 비밀번호
28
- # APPLE_PROVISIONING_PROFILE_BASE64: .mobileprovision 파일 (base64 인코딩)
29
- # IOS_PROVISIONING_PROFILE_NAME: 프로비저닝 프로파일 이름
25
+ # Apple certificate & provisioning profile:
26
+ # APPLE_CERTIFICATE_BASE64: .p12 certificate (base64 encoded)
27
+ # APPLE_CERTIFICATE_PASSWORD: .p12 certificate password
28
+ # APPLE_PROVISIONING_PROFILE_BASE64: .mobileprovision file (base64 encoded)
29
+ # IOS_PROVISIONING_PROFILE_NAME: provisioning profile name
30
30
  #
31
31
  # App Store Connect API:
32
- # APP_STORE_CONNECT_API_KEY_ID: API Key ID (10자리)
33
- # APP_STORE_CONNECT_ISSUER_ID: Issuer ID (UUID 형식)
34
- # APP_STORE_CONNECT_API_KEY_BASE64: AuthKey_XXXXXX.p8 파일 (base64 인코딩)
32
+ # APP_STORE_CONNECT_API_KEY_ID: API Key ID (10 characters)
33
+ # APP_STORE_CONNECT_ISSUER_ID: Issuer ID (UUID format)
34
+ # APP_STORE_CONNECT_API_KEY_BASE64: AuthKey_XXXXXX.p8 file (base64 encoded)
35
35
  #
36
- # 환경 설정 (선택):
37
- # ENV_FILE (선택): .env 파일 내용
38
- # SECRETS_XCCONFIG (선택): ios/Flutter/Secrets.xcconfig 내용
36
+ # Environment settings (optional):
37
+ # ENV_FILE (optional): contents of the .env file
38
+ # SECRETS_XCCONFIG (optional): contents of ios/Flutter/Secrets.xcconfig
39
39
 
40
40
  name: PROJECT-Flutter-iOS-Test-TestFlight
41
41
 
@@ -48,30 +48,30 @@ on:
48
48
  repository_dispatch:
49
49
  types: [build-ios-app]
50
50
 
51
- # 레포 기본 권한이 read여도 빌드 진행 댓글을 달 수 있게 필요한 권한을 직접 선언한다
51
+ # Declare the needed permissions explicitly so progress comments work even when the repository default is read-only
52
52
  permissions:
53
53
  contents: read
54
54
  issues: write
55
55
  pull-requests: write
56
56
 
57
- # 프로젝트별 설정 (아래 값들을 수정하세요)
57
+ # Project settings (edit the values below)
58
58
  env:
59
- # Flutter SDK 버전 — 비워 두면 stable 채널 최신을 쓴다(최신 flutter create 프로젝트의 SDK 제약과 맞음).
60
- # 특정 버전에 고정하려면 "3.38.9"처럼 적는다. pubspec.yaml의 environment.sdk 범위를 만족해야 한다.
59
+ # Flutter SDK version — leave empty to use the latest stable channel (matches the SDK constraint of a fresh flutter create project).
60
+ # To pin a version, write it like "3.38.9". It must satisfy environment.sdk in pubspec.yaml.
61
61
  FLUTTER_VERSION: ""
62
62
  XCODE_VERSION: "26.0"
63
63
  PROJECT_TYPE: "flutter"
64
- # Flutter 루트 경로(레포 루트 기준). 단일레포면 ".", 모노레포면 "app" 등 — 설치 시 자동 설정
64
+ # Flutter root path (relative to the repo root). "." for a single repo, "app" etc. for a monorepo — set automatically at install
65
65
  FLUTTER_PROJECT_DIR: "." # @wizard auto:flutter-root
66
- # 환경변수 전달 방식 (dart-define | dotenv) — 설치 시 선택값으로 자동 설정
66
+ # How env vars are passed (dart-define | dotenv) — set automatically from the value chosen at install
67
67
  ENV_MODE: "dart-define" # @wizard auto:flutter-env-mode
68
- # dotenv 모드에서 .env 파일을 만들 경로 (Flutter 루트 기준)
68
+ # Where to create the .env file in dotenv mode (relative to the Flutter root)
69
69
  ENV_FILE_PATH: ".env"
70
70
 
71
71
  jobs:
72
- # 즉시 댓글 생성 (ubuntu-latest로 빠르게 실행)
72
+ # Create the comment right away (runs quickly on ubuntu-latest)
73
73
  notify-start:
74
- name: 진행 상황 알림
74
+ name: Progress notification
75
75
  runs-on: ubuntu-latest
76
76
  if: github.event_name == 'repository_dispatch'
77
77
 
@@ -81,18 +81,40 @@ jobs:
81
81
  branch_valid: ${{ steps.verify_branch.outputs.valid }}
82
82
 
83
83
  steps:
84
- - name: 브랜치 존재 여부 사전 확인
84
+ # Fetch the message catalog from the triggering ref first — the feature branch being built may predate it
85
+ - name: Checkout messages
86
+ uses: actions/checkout@v7
87
+ with:
88
+ path: .paw-msg
89
+ sparse-checkout: |
90
+ .github/scripts/messages.py
91
+ version.yml
92
+ sparse-checkout-cone-mode: false
93
+
94
+ - name: Load messages
95
+ working-directory: ${{ github.workspace }}
96
+ run: |
97
+ cp .paw-msg/.github/scripts/messages.py "$RUNNER_TEMP/paw_messages.py"
98
+ LANG_CODE=$(cd .paw-msg && python3 "$RUNNER_TEMP/paw_messages.py" lang)
99
+ rm -rf .paw-msg
100
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE" >> "$GITHUB_ENV"
101
+ echo "PAW_MESSAGES_PY=$RUNNER_TEMP/paw_messages.py" >> "$GITHUB_ENV"
102
+ echo "PAW_MSG=$(PROJECT_AUTO_WIZARD_LANG=$LANG_CODE python3 "$RUNNER_TEMP/paw_messages.py" dump ios_test_tf.)" >> "$GITHUB_ENV"
103
+
104
+ - name: Verify the branch exists up front
85
105
  id: verify_branch
86
106
  uses: actions/github-script@v9
87
107
  with:
88
108
  github-token: ${{ secrets.GITHUB_TOKEN }}
89
109
  script: |
110
+ const M = JSON.parse(process.env.PAW_MSG);
111
+ const msg = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
90
112
  const branchName = '${{ github.event.client_payload.branch_name }}';
91
113
  const prNumber = '${{ github.event.client_payload.pr_number }}';
92
114
  const runId = '${{ github.run_id }}';
93
115
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
94
116
 
95
- console.log(`🔍 브랜치 존재 여부 확인: ${branchName}`);
117
+ console.log(msg('ios_test_tf.branch_check_log', { branch: branchName }));
96
118
 
97
119
  try {
98
120
  await github.rest.repos.getBranch({
@@ -100,26 +122,26 @@ jobs:
100
122
  repo: context.repo.repo,
101
123
  branch: branchName
102
124
  });
103
- console.log(`✅ 브랜치 존재 확인됨: ${branchName}`);
125
+ console.log(msg('ios_test_tf.branch_exists_log', { branch: branchName }));
104
126
  core.setOutput('valid', 'true');
105
127
  } catch (error) {
106
128
  if (error.status === 404) {
107
- console.log(`❌ 브랜치를 찾을 수 없음: ${branchName}`);
129
+ console.log(msg('ios_test_tf.branch_not_found_log', { branch: branchName }));
108
130
 
109
131
  const body = [
110
- '🍎 ❌ **iOS TestFlight 빌드 실패 - 브랜치를 찾을 수 없습니다**',
132
+ msg('ios_test_tf.branch_missing_title'),
111
133
  '',
112
- '| 항목 | 값 |',
134
+ msg('ios_test_tf.kv_head'),
113
135
  '|------|-----|',
114
- `| **요청된 브랜치** | \`${branchName}\` |`,
115
- `| **이슈/PR** | #${prNumber} |`,
136
+ msg('ios_test_tf.row_requested_branch', { branch: branchName }),
137
+ msg('ios_test_tf.row_issue_pr', { number: prNumber }),
116
138
  '',
117
- '### 💡 확인 사항',
118
- '1. 브랜치가 원격 저장소에 push되었는지 확인하세요',
119
- '2. "Issue Helper" 댓글의 브랜치명이 올바른지 확인하세요',
120
- '3. 브랜치명에 오타가 없는지 확인하세요',
139
+ msg('ios_test_tf.hint_title'),
140
+ msg('ios_test_tf.hint_pushed'),
141
+ msg('ios_test_tf.hint_helper_branch'),
142
+ msg('ios_test_tf.hint_typo'),
121
143
  '',
122
- `🔗 [워크플로우 로그](${runUrl})`
144
+ msg('ios_test_tf.workflow_log_link', { url: runUrl })
123
145
  ].join('\n');
124
146
 
125
147
  await github.rest.issues.createComment({
@@ -129,13 +151,13 @@ jobs:
129
151
  body: body
130
152
  });
131
153
 
132
- core.setFailed(`브랜치를 찾을 수 없습니다: ${branchName}`);
154
+ core.setFailed(msg('ios_test_tf.branch_not_found_fail', { branch: branchName }));
133
155
  } else {
134
- core.setFailed(`브랜치 확인 중 오류: ${error.message}`);
156
+ core.setFailed(msg('ios_test_tf.branch_check_error_fail', { error: error.message }));
135
157
  }
136
158
  }
137
159
 
138
- - name: 진행 상황 댓글 생성
160
+ - name: Create the progress comment
139
161
  id: progress
140
162
  if: github.event.client_payload.pr_number != ''
141
163
  continue-on-error: true
@@ -143,6 +165,9 @@ jobs:
143
165
  with:
144
166
  github-token: ${{ secrets.GITHUB_TOKEN }}
145
167
  script: |
168
+ const M = JSON.parse(process.env.PAW_MSG);
169
+ const msg = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
170
+ const row = (name, status, elapsed = '-') => `| ${msg('ios_test_tf.step_' + name)} | ${msg('ios_test_tf.st_' + status)} | ${elapsed} |`;
146
171
  const prNumber = parseInt('${{ github.event.client_payload.pr_number }}');
147
172
  const branchName = '${{ github.event.client_payload.branch_name }}';
148
173
  const buildNumber = '${{ github.event.client_payload.build_number }}';
@@ -151,23 +176,23 @@ jobs:
151
176
  const startTime = Date.now();
152
177
 
153
178
  const body = [
154
- '## 🍎 iOS TestFlight 빌드 진행 중...',
179
+ msg('ios_test_tf.title_running'),
155
180
  '',
156
- '| 단계 | 상태 | 소요 시간 |',
181
+ msg('ios_test_tf.table_header'),
157
182
  '|------|------|----------|',
158
- '| 🔧 준비 | ⏳ 진행 중... | - |',
159
- '| 🔨 IPA 빌드 | ⏸️ 대기 | - |',
160
- '| 📤 TestFlight 배포 | ⏸️ 대기 | - |',
183
+ row('prepare', 'running'),
184
+ row('build', 'waiting'),
185
+ row('deploy', 'waiting'),
161
186
  '',
162
- '| 항목 | 값 |',
187
+ msg('ios_test_tf.kv_head'),
163
188
  '|------|-----|',
164
- `| **빌드 번호** | \`${buildNumber}\` |`,
165
- `| **브랜치** | \`${branchName}\` |`,
189
+ msg('ios_test_tf.row_build_number', { build: buildNumber }),
190
+ msg('ios_test_tf.row_branch', { branch: branchName }),
166
191
  '',
167
- `**[📋 실시간 로그 보기](${runUrl})**`,
192
+ msg('ios_test_tf.live_log_link', { url: runUrl }),
168
193
  '',
169
194
  '---',
170
- '*🤖 이 댓글은 자동으로 업데이트됩니다.*'
195
+ msg('ios_test_tf.auto_update_note')
171
196
  ].join('\n');
172
197
 
173
198
  const { data: comment } = await github.rest.issues.createComment({
@@ -177,13 +202,13 @@ jobs:
177
202
  body: body
178
203
  });
179
204
 
180
- console.log(`✅ 진행 상황 댓글 생성 완료: #${comment.id}`);
205
+ console.log(msg('ios_test_tf.log_progress_created', { id: comment.id }));
181
206
  core.setOutput('comment_id', comment.id);
182
207
  core.setOutput('start_time', startTime);
183
208
 
184
- # 테스트 빌드 준비 (macOS 환경 설정)
209
+ # Prepare the test build (macOS environment setup)
185
210
  prepare-test-build:
186
- name: 테스트 빌드 준비
211
+ name: Prepare test build
187
212
  runs-on: macos-26
188
213
  defaults:
189
214
  run:
@@ -204,6 +229,26 @@ jobs:
204
229
  commit_hash: ${{ steps.release_notes.outputs.commit_hash }}
205
230
 
206
231
  steps:
232
+ # Fetch the message catalog from the triggering ref first — the feature branch being built may predate it
233
+ - name: Checkout messages
234
+ uses: actions/checkout@v7
235
+ with:
236
+ path: .paw-msg
237
+ sparse-checkout: |
238
+ .github/scripts/messages.py
239
+ version.yml
240
+ sparse-checkout-cone-mode: false
241
+
242
+ - name: Load messages
243
+ working-directory: ${{ github.workspace }}
244
+ run: |
245
+ cp .paw-msg/.github/scripts/messages.py "$RUNNER_TEMP/paw_messages.py"
246
+ LANG_CODE=$(cd .paw-msg && python3 "$RUNNER_TEMP/paw_messages.py" lang)
247
+ rm -rf .paw-msg
248
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE" >> "$GITHUB_ENV"
249
+ echo "PAW_MESSAGES_PY=$RUNNER_TEMP/paw_messages.py" >> "$GITHUB_ENV"
250
+ echo "PAW_MSG=$(PROJECT_AUTO_WIZARD_LANG=$LANG_CODE python3 "$RUNNER_TEMP/paw_messages.py" dump ios_test_tf.)" >> "$GITHUB_ENV"
251
+
207
252
  - name: Checkout repository
208
253
  uses: actions/checkout@v7
209
254
  with:
@@ -213,8 +258,8 @@ jobs:
213
258
  - name: Pull latest changes
214
259
  run: git pull origin ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}
215
260
 
216
- # 인증서 import·빌드 전에 설정 누락을 한 번에 알려 준다 — 없으면 macOS 러너에서 한참 빌드한 뒤에야 엉뚱한 단계에서 실패한다
217
- - name: 배포 설정 사전 검증
261
+ # Report every missing setting up front, before importing certificates or building — otherwise the macOS runner builds for a long time and fails at an unrelated step
262
+ - name: Validate deploy settings
218
263
  env:
219
264
  APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
220
265
  APPLE_PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_PROVISIONING_PROFILE_BASE64 }}
@@ -223,6 +268,7 @@ jobs:
223
268
  APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
224
269
  APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}
225
270
  run: |
271
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
226
272
  FAILED=0
227
273
  MISSING=""
228
274
  for name in APPLE_CERTIFICATE_BASE64 APPLE_PROVISIONING_PROFILE_BASE64 IOS_PROVISIONING_PROFILE_NAME \
@@ -230,23 +276,23 @@ jobs:
230
276
  [ -n "${!name}" ] || MISSING="$MISSING $name"
231
277
  done
232
278
  if [ -n "$MISSING" ]; then
233
- echo "::error::필수 GitHub Secret이 비어 있습니다:$MISSING (워크플로우 상단 주석 참고)"
279
+ echo "::error::$(m ios_test_tf.secrets_missing missing="$MISSING")"
234
280
  FAILED=1
235
281
  fi
236
282
  if [ ! -f ios/ExportOptions.plist ]; then
237
- echo "::error::ios/ExportOptions.plist가 없습니다 — 저장소에 추가(커밋)하세요."
283
+ echo "::error::$(m ios_test_tf.export_options_missing)"
238
284
  FAILED=1
239
285
  elif grep -Eq '__[A-Z][A-Z0-9_]*__' ios/ExportOptions.plist; then
240
- echo "::error::ios/ExportOptions.plist에 채워지지 않은 플레이스홀더가 남아 있습니다 — 팀 ID·번들 ID·프로비저닝 프로파일 이름으로 바꿔 커밋하세요."
286
+ echo "::error::$(m ios_test_tf.export_options_placeholders)"
241
287
  grep -En '__[A-Z][A-Z0-9_]*__' ios/ExportOptions.plist
242
288
  FAILED=1
243
289
  fi
244
290
  if [ ! -f ios/fastlane/Fastfile ]; then
245
- echo "::error::ios/fastlane/Fastfile이 없습니다 — 저장소에 추가(커밋)하세요."
291
+ echo "::error::$(m ios_test_tf.fastfile_missing_error)"
246
292
  FAILED=1
247
293
  fi
248
294
  [ "$FAILED" -eq 0 ] || exit 1
249
- echo "✅ 배포 설정 확인 완료"
295
+ m ios_test_tf.preflight_ok
250
296
 
251
297
  - name: Select Xcode version
252
298
  run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer
@@ -264,81 +310,85 @@ jobs:
264
310
  echo "ℹ️ Empty Secrets.xcconfig created (no secrets provided)"
265
311
  fi
266
312
 
267
- - name: 테스트 빌드 버전 설정
313
+ - name: Set test build version
268
314
  id: test_version
269
- working-directory: ${{ github.workspace }} # version_manager.py가 레포 루트 version.yml을 읽어야 함
315
+ working-directory: ${{ github.workspace }} # version_manager.py must read version.yml at the repo root
270
316
  run: |
271
- # version_manager.py로 버전 가져오기 (프로덕션 워크플로우와 동일한 방식)
317
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
318
+ # Get the version through version_manager.py (same approach as the production workflow)
272
319
  VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
273
320
  echo "version=$VERSION" >> $GITHUB_OUTPUT
274
321
 
275
- # repository_dispatch 이벤트인 경우 PR 번호를 빌드 번호로 사용
322
+ # For repository_dispatch events, use the PR number as the build number
276
323
  if [ "${{ github.event_name }}" == "repository_dispatch" ]; then
277
324
  BUILD_NUMBER="${{ github.event.client_payload.build_number }}"
278
325
  PR_NUMBER="${{ github.event.client_payload.pr_number }}"
279
- echo "📋 repository_dispatch 이벤트 감지"
280
- echo " PR 번호: #$PR_NUMBER"
281
- echo " 빌드 번호: $BUILD_NUMBER (PR 번호 사용)"
326
+ m ios_test_tf.event_dispatch
327
+ m ios_test_tf.event_pr_number pr="$PR_NUMBER"
328
+ m ios_test_tf.event_build_number_pr build="$BUILD_NUMBER"
282
329
  else
283
330
  BUILD_NUMBER="${{ github.run_number }}"
284
331
  PR_NUMBER=""
285
- echo "📋 workflow_dispatch 이벤트 감지"
286
- echo " 빌드 번호: $BUILD_NUMBER (기본값)"
332
+ m ios_test_tf.event_workflow_dispatch
333
+ m ios_test_tf.event_build_number_default build="$BUILD_NUMBER"
287
334
  fi
288
335
 
289
336
  echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT
290
337
  echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
291
- echo "📋 테스트 빌드: 버전=$VERSION, 빌드번호=$BUILD_NUMBER"
338
+ m ios_test_tf.test_build_log version="$VERSION" build="$BUILD_NUMBER"
292
339
 
293
- - name: 브랜치명에서 이슈 정보 추출
340
+ - name: Extract issue info from the branch name
294
341
  id: issue_info
295
342
  run: |
296
- # repository_dispatch인 경우 client_payload에서 브랜치명 가져오기
343
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
344
+ # For repository_dispatch, take the branch name from client_payload
297
345
  if [ "${{ github.event_name }}" == "repository_dispatch" ]; then
298
346
  BRANCH_NAME="${{ github.event.client_payload.branch_name }}"
299
347
  ISSUE_NUMBER="${{ github.event.client_payload.issue_number }}"
300
348
  else
301
349
  BRANCH_NAME="${{ github.ref_name }}"
302
- # 브랜치명에서 이슈 번호 추출 (#387 형식)
350
+ # Extract the issue number from the branch name (`<date>_#<number>_...` format)
303
351
  ISSUE_NUMBER=$(echo "$BRANCH_NAME" | sed -n 's/.*#\([0-9]*\).*/\1/p')
304
352
  fi
305
353
 
306
- echo "🌿 브랜치명: $BRANCH_NAME"
354
+ m ios_test_tf.branch_name_log branch="$BRANCH_NAME"
307
355
  echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
308
356
 
309
357
  if [ -z "$ISSUE_NUMBER" ]; then
310
- echo "⚠️ 브랜치명에서 이슈 번호를 찾을 수 없습니다."
358
+ m ios_test_tf.issue_number_missing
311
359
  echo "issue_url=" >> $GITHUB_OUTPUT
312
360
  echo "issue_title=" >> $GITHUB_OUTPUT
313
361
  echo "issue_number=" >> $GITHUB_OUTPUT
314
362
  else
315
- echo "✅ 추출된 이슈 번호: #$ISSUE_NUMBER"
363
+ m ios_test_tf.issue_number_extracted number="$ISSUE_NUMBER"
316
364
  ISSUE_URL="https://github.com/${{ github.repository }}/issues/$ISSUE_NUMBER"
317
365
  echo "issue_url=$ISSUE_URL" >> $GITHUB_OUTPUT
318
366
  echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
319
367
 
320
- # GitHub API로 이슈 정보 가져오기
321
- echo "🔍 GitHub API로 이슈 정보 조회 중..."
368
+ # Fetch the issue info through the GitHub API
369
+ m ios_test_tf.issue_fetching
322
370
  ISSUE_RESPONSE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
323
371
  "https://api.github.com/repos/${{ github.repository }}/issues/$ISSUE_NUMBER")
324
372
 
325
- ISSUE_TITLE=$(echo "$ISSUE_RESPONSE" | jq -r '.title // "이슈 정보 없음"')
373
+ NO_ISSUE_INFO="$(m ios_test_tf.issue_title_default)"
374
+ ISSUE_TITLE=$(echo "$ISSUE_RESPONSE" | jq -r --arg d "$NO_ISSUE_INFO" '.title // $d')
326
375
  ISSUE_STATE=$(echo "$ISSUE_RESPONSE" | jq -r '.state // "unknown"')
327
376
 
328
- if [ "$ISSUE_TITLE" = "null" ] || [ "$ISSUE_TITLE" = "이슈 정보 없음" ]; then
329
- echo "⚠️ 이슈 #$ISSUE_NUMBER를 찾을 수 없습니다."
377
+ if [ "$ISSUE_TITLE" = "null" ] || [ "$ISSUE_TITLE" = "$NO_ISSUE_INFO" ]; then
378
+ m ios_test_tf.issue_not_found number="$ISSUE_NUMBER"
330
379
  echo "issue_title=" >> $GITHUB_OUTPUT
331
380
  else
332
- echo "📋 이슈 제목: $ISSUE_TITLE"
333
- echo "📌 이슈 상태: $ISSUE_STATE"
381
+ m ios_test_tf.issue_title_log title="$ISSUE_TITLE"
382
+ m ios_test_tf.issue_state_log state="$ISSUE_STATE"
334
383
  echo "issue_title=$ISSUE_TITLE" >> $GITHUB_OUTPUT
335
384
  fi
336
385
  fi
337
386
 
338
- - name: 릴리즈 노트 생성
387
+ - name: Generate release notes
339
388
  id: release_notes
340
- working-directory: ${{ github.workspace }} # final_release_notes.txt(upload 짝)는 레포 루트 기준
389
+ working-directory: ${{ github.workspace }} # final_release_notes.txt (paired with the upload step) is relative to the repo root
341
390
  run: |
391
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
342
392
  BUILD_NUMBER="${{ steps.test_version.outputs.build_number }}"
343
393
  BRANCH_NAME="${{ steps.issue_info.outputs.branch_name }}"
344
394
  COMMIT_SHA="${{ github.sha }}"
@@ -347,30 +397,30 @@ jobs:
347
397
 
348
398
  echo "commit_hash=$COMMIT_SHORT" >> $GITHUB_OUTPUT
349
399
 
350
- cat > final_release_notes.txt << EOF
351
- 테스트 빌드 #$BUILD_NUMBER
352
-
353
- 브랜치: $BRANCH_NAME
354
- 커밋: $COMMIT_SHORT
355
- 날짜: $BUILD_DATE
356
-
357
- EOF
358
-
359
- # 이슈 정보가 있으면 추가
400
+ {
401
+ m ios_test_tf.notes_build build="$BUILD_NUMBER"
402
+ echo
403
+ m ios_test_tf.notes_branch branch="$BRANCH_NAME"
404
+ m ios_test_tf.notes_commit commit="$COMMIT_SHORT"
405
+ m ios_test_tf.notes_date date="$BUILD_DATE"
406
+ echo
407
+ } > final_release_notes.txt
408
+
409
+ # Add the issue info when available
360
410
  if [ -n "${{ steps.issue_info.outputs.issue_number }}" ]; then
361
411
  ISSUE_NUMBER="${{ steps.issue_info.outputs.issue_number }}"
362
412
  ISSUE_TITLE="${{ steps.issue_info.outputs.issue_title }}"
363
413
  ISSUE_URL="${{ steps.issue_info.outputs.issue_url }}"
364
414
 
365
- cat >> final_release_notes.txt << EOF
366
- 관련 이슈:
367
- - #$ISSUE_NUMBER: $ISSUE_TITLE
368
- - URL: $ISSUE_URL
369
-
370
- EOF
415
+ {
416
+ m ios_test_tf.notes_issue_header
417
+ echo "- #$ISSUE_NUMBER: $ISSUE_TITLE"
418
+ echo "- URL: $ISSUE_URL"
419
+ echo
420
+ } >> final_release_notes.txt
371
421
  fi
372
422
 
373
- echo "📋 생성된 릴리즈 노트:"
423
+ m ios_test_tf.notes_generated
374
424
  echo "----------------------------------------"
375
425
  cat final_release_notes.txt
376
426
  echo "----------------------------------------"
@@ -393,12 +443,15 @@ jobs:
393
443
  ${{ env.FLUTTER_PROJECT_DIR }}/assets/
394
444
  retention-days: 1
395
445
 
396
- - name: 진행 상황 업데이트 - 준비 실패
446
+ - name: Progress update - prepare failed
397
447
  if: failure() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != ''
398
448
  uses: actions/github-script@v9
399
449
  with:
400
450
  github-token: ${{ secrets.GITHUB_TOKEN }}
401
451
  script: |
452
+ const M = JSON.parse(process.env.PAW_MSG);
453
+ const msg = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
454
+ const row = (name, status, elapsed = '-') => `| ${msg('ios_test_tf.step_' + name)} | ${msg('ios_test_tf.st_' + status)} | ${elapsed} |`;
402
455
  const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}');
403
456
  const startTime = parseInt('${{ needs.notify-start.outputs.start_time }}');
404
457
  const branchName = '${{ github.event.client_payload.branch_name }}';
@@ -410,25 +463,25 @@ jobs:
410
463
  const elapsed = now - startTime;
411
464
  const minutes = Math.floor(elapsed / 60000);
412
465
  const seconds = Math.floor((elapsed % 60000) / 1000);
413
- const duration = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
466
+ const duration = minutes > 0 ? msg('ios_test_tf.elapsed_min_sec', { m: minutes, s: seconds }) : msg('ios_test_tf.elapsed_sec', { s: seconds });
414
467
 
415
468
  const body = [
416
- '## 🍎 ❌ iOS TestFlight 빌드 실패',
469
+ msg('ios_test_tf.title_prepare_failed'),
417
470
  '',
418
- '| 단계 | 상태 | 소요 시간 |',
471
+ msg('ios_test_tf.table_header'),
419
472
  '|------|------|----------|',
420
- `| 🔧 준비 | ❌ 실패 | ${duration} |`,
421
- '| 🔨 IPA 빌드 | ⏸️ 취소됨 | - |',
422
- '| 📤 TestFlight 배포 | ⏸️ 취소됨 | - |',
473
+ row('prepare', 'failed', duration),
474
+ row('build', 'canceled'),
475
+ row('deploy', 'canceled'),
423
476
  '',
424
- '| 항목 | 값 |',
477
+ msg('ios_test_tf.kv_head'),
425
478
  '|------|-----|',
426
- `| **빌드 번호** | \`${buildNumber}\` |`,
427
- `| **브랜치** | \`${branchName}\` |`,
479
+ msg('ios_test_tf.row_build_number', { build: buildNumber }),
480
+ msg('ios_test_tf.row_branch', { branch: branchName }),
428
481
  '',
429
- '❌ **환경 준비 중 오류가 발생했습니다.**',
482
+ msg('ios_test_tf.prepare_error'),
430
483
  '',
431
- `🔗 [워크플로우 실행 로그 확인](${runUrl})`
484
+ msg('ios_test_tf.run_log_check_link', { url: runUrl })
432
485
  ].join('\n');
433
486
 
434
487
  await github.rest.issues.updateComment({
@@ -438,11 +491,11 @@ jobs:
438
491
  body: body
439
492
  });
440
493
 
441
- console.log(`❌ 진행 상황 업데이트 완료: 준비 실패`);
494
+ console.log(msg('ios_test_tf.log_prepare_failed'));
442
495
 
443
- # iOS 빌드 (xcodebuild 직접 사용)
496
+ # iOS build (uses xcodebuild directly)
444
497
  build-ios-test:
445
- name: iOS 테스트 빌드
498
+ name: iOS test build
446
499
  runs-on: macos-26
447
500
  defaults:
448
501
  run:
@@ -459,14 +512,37 @@ jobs:
459
512
  step2_end_time: ${{ steps.progress_step2.outputs.end_time }}
460
513
 
461
514
  steps:
462
- # 진행 상황 업데이트 - 준비 완료 (build job 시작 시점에 업데이트해야 준비 시간이 정확히 표시됨)
463
- - name: 진행 상황 업데이트 - 준비 완료
515
+ # Fetch the message catalog from the triggering ref first — the feature branch being built may predate it
516
+ - name: Checkout messages
517
+ uses: actions/checkout@v7
518
+ with:
519
+ path: .paw-msg
520
+ sparse-checkout: |
521
+ .github/scripts/messages.py
522
+ version.yml
523
+ sparse-checkout-cone-mode: false
524
+
525
+ - name: Load messages
526
+ working-directory: ${{ github.workspace }}
527
+ run: |
528
+ cp .paw-msg/.github/scripts/messages.py "$RUNNER_TEMP/paw_messages.py"
529
+ LANG_CODE=$(cd .paw-msg && python3 "$RUNNER_TEMP/paw_messages.py" lang)
530
+ rm -rf .paw-msg
531
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE" >> "$GITHUB_ENV"
532
+ echo "PAW_MESSAGES_PY=$RUNNER_TEMP/paw_messages.py" >> "$GITHUB_ENV"
533
+ echo "PAW_MSG=$(PROJECT_AUTO_WIZARD_LANG=$LANG_CODE python3 "$RUNNER_TEMP/paw_messages.py" dump ios_test_tf.)" >> "$GITHUB_ENV"
534
+
535
+ # Progress update - prepare done (must be updated when the build job starts so the prepare time is shown accurately)
536
+ - name: Progress update - prepare done
464
537
  id: progress_step1
465
538
  if: github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != ''
466
539
  uses: actions/github-script@v9
467
540
  with:
468
541
  github-token: ${{ secrets.GITHUB_TOKEN }}
469
542
  script: |
543
+ const M = JSON.parse(process.env.PAW_MSG);
544
+ const msg = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
545
+ const row = (name, status, elapsed = '-') => `| ${msg('ios_test_tf.step_' + name)} | ${msg('ios_test_tf.st_' + status)} | ${elapsed} |`;
470
546
  const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}');
471
547
  const startTime = parseInt('${{ needs.notify-start.outputs.start_time }}');
472
548
  const branchName = '${{ github.event.client_payload.branch_name }}';
@@ -479,26 +555,26 @@ jobs:
479
555
  const elapsed = Math.round((now - startTime) / 1000);
480
556
  const minutes = Math.floor(elapsed / 60);
481
557
  const seconds = elapsed % 60;
482
- const elapsedStr = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
558
+ const elapsedStr = minutes > 0 ? msg('ios_test_tf.elapsed_min_sec', { m: minutes, s: seconds }) : msg('ios_test_tf.elapsed_sec', { s: seconds });
483
559
 
484
560
  const body = [
485
- '## 🍎 iOS TestFlight 빌드 진행 중...',
561
+ msg('ios_test_tf.title_running'),
486
562
  '',
487
- '| 단계 | 상태 | 소요 시간 |',
563
+ msg('ios_test_tf.table_header'),
488
564
  '|------|------|----------|',
489
- `| 🔧 준비 | ✅ 완료 | ${elapsedStr} |`,
490
- '| 🔨 IPA 빌드 | ⏳ 진행 중... | - |',
491
- '| 📤 TestFlight 배포 | ⏸️ 대기 | - |',
565
+ row('prepare', 'done', elapsedStr),
566
+ row('build', 'running'),
567
+ row('deploy', 'waiting'),
492
568
  '',
493
- '| 항목 | 값 |',
569
+ msg('ios_test_tf.kv_head'),
494
570
  '|------|-----|',
495
- `| **앱 버전** | \`${version}(${buildNumber})\` |`,
496
- `| **브랜치** | \`${branchName}\` |`,
571
+ msg('ios_test_tf.row_app_version', { version, build: buildNumber }),
572
+ msg('ios_test_tf.row_branch', { branch: branchName }),
497
573
  '',
498
- `**[📋 실시간 로그 보기](${runUrl})**`,
574
+ msg('ios_test_tf.live_log_link', { url: runUrl }),
499
575
  '',
500
576
  '---',
501
- '*🤖 이 댓글은 자동으로 업데이트됩니다.*'
577
+ msg('ios_test_tf.auto_update_note')
502
578
  ].join('\n');
503
579
 
504
580
  await github.rest.issues.updateComment({
@@ -508,7 +584,7 @@ jobs:
508
584
  body: body
509
585
  });
510
586
 
511
- console.log(`✅ 진행 상황 업데이트 완료: 준비 완료 (${elapsedStr})`);
587
+ console.log(msg('ios_test_tf.log_prepare_done', { elapsed: elapsedStr }));
512
588
  core.setOutput('elapsed', elapsedStr);
513
589
  core.setOutput('end_time', now.toString());
514
590
 
@@ -526,7 +602,7 @@ jobs:
526
602
 
527
603
  - name: Install iOS device platform
528
604
  env:
529
- # xcode-select만으로는 서브프로세스에 전파되지 않아 명시적으로 지정
605
+ # xcode-select alone does not propagate to subprocesses, so set it explicitly
530
606
  DEVELOPER_DIR: /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer
531
607
  run: |
532
608
  echo "=== iPhoneOS.platform check ==="
@@ -590,12 +666,13 @@ jobs:
590
666
 
591
667
  - name: Install CocoaPods
592
668
  run: |
593
- # 플러그인이 없는 기본 프로젝트는 Podfile이 없다 — 플러그인이 생기면 flutter build가 Podfile을 만든다
669
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
670
+ # A default project without plugins has no Podfile — flutter build creates one once a plugin is added
594
671
  if [ -f ios/Podfile ]; then
595
672
  gem install cocoapods
596
673
  (cd ios && pod install)
597
674
  else
598
- echo "ℹ️ ios/Podfile이 없어 pod install을 건너뜁니다 (CocoaPods 플러그인 없음)"
675
+ m ios_test_tf.podfile_skip
599
676
  fi
600
677
 
601
678
  - name: Import Code-Signing Certificates
@@ -616,17 +693,18 @@ jobs:
616
693
 
617
694
  - name: Verify ExportOptions.plist
618
695
  run: |
696
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
619
697
  cd ios
620
698
  if [ ! -f "ExportOptions.plist" ]; then
621
- echo "❌ ios/ExportOptions.plist가 없습니다!"
622
- echo "ios/ExportOptions.plist를 저장소에 추가(커밋)하세요. 필요한 Secrets는 워크플로우 상단 주석을 참고하세요."
699
+ m ios_test_tf.plist_missing
700
+ m ios_test_tf.plist_missing_hint
623
701
  exit 1
624
702
  fi
625
- echo "✅ ExportOptions.plist 확인됨"
703
+ m ios_test_tf.plist_ok
626
704
 
627
705
  - name: Flutter build (no codesign)
628
706
  env:
629
- # xcode-select만으로는 서브프로세스에 전파되지 않아 명시적으로 지정
707
+ # xcode-select alone does not propagate to subprocesses, so set it explicitly
630
708
  DEVELOPER_DIR: /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer
631
709
  run: |
632
710
  flutter build ios --release --no-codesign \
@@ -637,7 +715,7 @@ jobs:
637
715
  - name: Create Archive
638
716
  env:
639
717
  IOS_PROVISIONING_PROFILE_NAME: ${{ secrets.IOS_PROVISIONING_PROFILE_NAME }}
640
- # xcode-select만으로는 서브프로세스에 전파되지 않아 명시적으로 지정
718
+ # xcode-select alone does not propagate to subprocesses, so set it explicitly
641
719
  DEVELOPER_DIR: /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer
642
720
  run: |
643
721
  cd ios
@@ -679,13 +757,16 @@ jobs:
679
757
  ${{ env.FLUTTER_PROJECT_DIR }}/build-metadata.json
680
758
  retention-days: 1
681
759
 
682
- - name: 진행 상황 업데이트 - IPA 빌드 완료
760
+ - name: Progress update - IPA build done
683
761
  id: progress_step2
684
762
  if: success() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != ''
685
763
  uses: actions/github-script@v9
686
764
  with:
687
765
  github-token: ${{ secrets.GITHUB_TOKEN }}
688
766
  script: |
767
+ const M = JSON.parse(process.env.PAW_MSG);
768
+ const msg = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
769
+ const row = (name, status, elapsed = '-') => `| ${msg('ios_test_tf.step_' + name)} | ${msg('ios_test_tf.st_' + status)} | ${elapsed} |`;
689
770
  const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}');
690
771
  const step1EndTime = parseInt('${{ steps.progress_step1.outputs.end_time }}');
691
772
  const step1Elapsed = '${{ steps.progress_step1.outputs.elapsed }}';
@@ -697,30 +778,30 @@ jobs:
697
778
 
698
779
  const now = Date.now();
699
780
 
700
- // 빌드 단계 소요 시간 (step1 종료 ~ 현재)
781
+ // Build step duration (step1 end to now)
701
782
  const elapsed = Math.round((now - step1EndTime) / 1000);
702
783
  const buildMin = Math.floor(elapsed / 60);
703
784
  const buildSec = elapsed % 60;
704
- const elapsedStr = buildMin > 0 ? `${buildMin}분 ${buildSec}초` : `${buildSec}초`;
785
+ const elapsedStr = buildMin > 0 ? msg('ios_test_tf.elapsed_min_sec', { m: buildMin, s: buildSec }) : msg('ios_test_tf.elapsed_sec', { s: buildSec });
705
786
 
706
787
  const body = [
707
- '## 🍎 ✅ iOS 빌드 완료',
788
+ msg('ios_test_tf.title_ipa_done'),
708
789
  '',
709
- '| 단계 | 상태 | 소요 시간 |',
790
+ msg('ios_test_tf.table_header'),
710
791
  '|------|------|----------|',
711
- `| 🔧 준비 | ✅ 완료 | ${step1Elapsed || '-'} |`,
712
- `| 🔨 IPA 빌드 | ✅ 완료 | ${elapsedStr} |`,
713
- '| 📤 TestFlight 배포 | ⏳ 진행 중... | - |',
792
+ row('prepare', 'done', step1Elapsed || '-'),
793
+ row('build', 'done', elapsedStr),
794
+ row('deploy', 'running'),
714
795
  '',
715
- '| 항목 | 값 |',
796
+ msg('ios_test_tf.kv_head'),
716
797
  '|------|-----|',
717
- `| **앱 버전** | \`${version}(${buildNumber})\` |`,
718
- `| **브랜치** | \`${branchName}\` |`,
798
+ msg('ios_test_tf.row_app_version', { version, build: buildNumber }),
799
+ msg('ios_test_tf.row_branch', { branch: branchName }),
719
800
  '',
720
- `**[📋 실시간 로그 보기](${runUrl})**`,
801
+ msg('ios_test_tf.live_log_link', { url: runUrl }),
721
802
  '',
722
803
  '---',
723
- '*🤖 이 댓글은 자동으로 업데이트됩니다.*'
804
+ msg('ios_test_tf.auto_update_note')
724
805
  ].join('\n');
725
806
 
726
807
  await github.rest.issues.updateComment({
@@ -730,16 +811,19 @@ jobs:
730
811
  body: body
731
812
  });
732
813
 
733
- console.log(`✅ 진행 상황 업데이트 완료: IPA 빌드 완료 (${elapsedStr})`);
814
+ console.log(msg('ios_test_tf.log_ipa_done', { elapsed: elapsedStr }));
734
815
  core.setOutput('elapsed', elapsedStr);
735
816
  core.setOutput('end_time', now.toString());
736
817
 
737
- - name: 빌드 실패 시 progress 댓글 업데이트
818
+ - name: Update the progress comment on build failure
738
819
  if: failure() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != ''
739
820
  uses: actions/github-script@v9
740
821
  with:
741
822
  github-token: ${{ secrets.GITHUB_TOKEN }}
742
823
  script: |
824
+ const M = JSON.parse(process.env.PAW_MSG);
825
+ const msg = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
826
+ const row = (name, status, elapsed = '-') => `| ${msg('ios_test_tf.step_' + name)} | ${msg('ios_test_tf.st_' + status)} | ${elapsed} |`;
743
827
  const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}');
744
828
  const startTime = parseInt('${{ needs.notify-start.outputs.start_time }}');
745
829
  const step1EndTime = parseInt('${{ steps.progress_step1.outputs.end_time }}') || 0;
@@ -753,33 +837,33 @@ jobs:
753
837
 
754
838
  const now = Date.now();
755
839
 
756
- // 빌드 단계 소요 시간 (step1 종료 ~ 현재)
840
+ // Build step duration (step1 end to now)
757
841
  let step2ElapsedStr = '-';
758
842
  if (step1EndTime > 0) {
759
843
  const step2_s = Math.round((now - step1EndTime) / 1000);
760
844
  const s2Min = Math.floor(step2_s / 60);
761
845
  const s2Sec = step2_s % 60;
762
- step2ElapsedStr = s2Min > 0 ? `${s2Min}분 ${s2Sec}초` : `${s2Sec}초`;
846
+ step2ElapsedStr = s2Min > 0 ? msg('ios_test_tf.elapsed_min_sec', { m: s2Min, s: s2Sec }) : msg('ios_test_tf.elapsed_sec', { s: s2Sec });
763
847
  }
764
848
 
765
849
  const body = [
766
- '## 🍎 ❌ iOS TestFlight 빌드 실패',
850
+ msg('ios_test_tf.title_ipa_failed'),
767
851
  '',
768
- '| 단계 | 상태 | 소요 시간 |',
852
+ msg('ios_test_tf.table_header'),
769
853
  '|------|------|----------|',
770
- `| 🔧 준비 | ✅ 완료 | ${step1Elapsed || '-'} |`,
771
- `| 🔨 IPA 빌드 | ❌ 실패 | ${step2ElapsedStr} |`,
772
- '| 📤 TestFlight 배포 | ⏸️ 취소됨 | - |',
854
+ row('prepare', 'done', step1Elapsed || '-'),
855
+ row('build', 'failed', step2ElapsedStr),
856
+ row('deploy', 'canceled'),
773
857
  '',
774
- '| 항목 | 값 |',
858
+ msg('ios_test_tf.kv_head'),
775
859
  '|------|-----|',
776
- `| **앱 버전** | \`${version}(${buildNumber})\` |`,
777
- `| **브랜치** | \`${branchName}\` |`,
778
- `| **커밋** | \`${commitHash}\` |`,
860
+ msg('ios_test_tf.row_app_version', { version, build: buildNumber }),
861
+ msg('ios_test_tf.row_branch', { branch: branchName }),
862
+ msg('ios_test_tf.row_commit', { commit: commitHash }),
779
863
  '',
780
- '❌ **IPA 빌드 중 오류가 발생했습니다.**',
864
+ msg('ios_test_tf.ipa_error'),
781
865
  '',
782
- `🔗 [워크플로우 실행 로그 확인](${runUrl})`
866
+ msg('ios_test_tf.run_log_check_link', { url: runUrl })
783
867
  ].join('\n');
784
868
 
785
869
  await github.rest.issues.updateComment({
@@ -789,11 +873,11 @@ jobs:
789
873
  body: body
790
874
  });
791
875
 
792
- console.log(`❌ 진행 상황 업데이트 완료: IPA 빌드 실패`);
876
+ console.log(msg('ios_test_tf.log_ipa_failed'));
793
877
 
794
- # TestFlight 배포 (ios/fastlane/Fastfile 사용)
878
+ # TestFlight deploy (uses ios/fastlane/Fastfile)
795
879
  deploy-testflight-test:
796
- name: TestFlight 테스트 배포
880
+ name: TestFlight test deploy
797
881
  runs-on: macos-26
798
882
  defaults:
799
883
  run:
@@ -804,6 +888,26 @@ jobs:
804
888
  needs.build-ios-test.result == 'success'
805
889
 
806
890
  steps:
891
+ # Fetch the message catalog from the triggering ref first — the feature branch being built may predate it
892
+ - name: Checkout messages
893
+ uses: actions/checkout@v7
894
+ with:
895
+ path: .paw-msg
896
+ sparse-checkout: |
897
+ .github/scripts/messages.py
898
+ version.yml
899
+ sparse-checkout-cone-mode: false
900
+
901
+ - name: Load messages
902
+ working-directory: ${{ github.workspace }}
903
+ run: |
904
+ cp .paw-msg/.github/scripts/messages.py "$RUNNER_TEMP/paw_messages.py"
905
+ LANG_CODE=$(cd .paw-msg && python3 "$RUNNER_TEMP/paw_messages.py" lang)
906
+ rm -rf .paw-msg
907
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE" >> "$GITHUB_ENV"
908
+ echo "PAW_MESSAGES_PY=$RUNNER_TEMP/paw_messages.py" >> "$GITHUB_ENV"
909
+ echo "PAW_MSG=$(PROJECT_AUTO_WIZARD_LANG=$LANG_CODE python3 "$RUNNER_TEMP/paw_messages.py" dump ios_test_tf.)" >> "$GITHUB_ENV"
910
+
807
911
  - name: Checkout repository
808
912
  uses: actions/checkout@v7
809
913
  with:
@@ -832,21 +936,23 @@ jobs:
832
936
 
833
937
  - name: Verify Fastfile exists
834
938
  run: |
939
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
835
940
  if [ ! -f "ios/fastlane/Fastfile" ]; then
836
- echo "❌ ios/fastlane/Fastfile이 없습니다!"
837
- echo "ios/fastlane/Fastfile을 저장소에 추가(커밋)하세요. 필요한 Secrets는 워크플로우 상단 주석을 참고하세요."
941
+ m ios_test_tf.fastfile_missing
942
+ m ios_test_tf.fastfile_missing_hint
838
943
  exit 1
839
944
  fi
840
- echo "✅ Fastfile 확인됨: ios/fastlane/Fastfile"
945
+ m ios_test_tf.fastfile_ok
841
946
 
842
947
  - name: Install Fastlane
843
948
  working-directory: ${{ env.FLUTTER_PROJECT_DIR }}/ios
844
949
  run: |
845
- # 저장소의 Gemfile에 fastlane이 있으면 그대로 쓴다 (Gemfile.lock 커밋 권장). 없으면 생성한다.
950
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
951
+ # Use the repository's Gemfile as-is if it lists fastlane (committing Gemfile.lock is recommended). Otherwise generate one.
846
952
  if [ -f Gemfile ] && grep -Eq "['\"]fastlane['\"]" Gemfile; then
847
- echo "ℹ️ 저장소의 Gemfile을 사용합니다"
953
+ m ios_test_tf.gemfile_repo
848
954
  else
849
- # multi_json: google-apis transitive 의존성이 선언 누락한 upstream 버그 회피 (gemspec 미선언 → Gem::LoadError)
955
+ # multi_json: works around an upstream bug where a google-apis transitive dependency omits it from its gemspec (Gem::LoadError)
850
956
  printf 'source "https://rubygems.org"\ngem "fastlane"\ngem "multi_json"\n' > Gemfile
851
957
  fi
852
958
  bundle install
@@ -857,27 +963,28 @@ jobs:
857
963
  APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
858
964
  APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
859
965
  run: |
860
- # IPA 파일 찾기
966
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
967
+ # Find the IPA file
861
968
  IPA_PATH=$(find "$GITHUB_WORKSPACE/${FLUTTER_PROJECT_DIR}/ios/build/ipa" -name "*.ipa" | head -1)
862
969
  echo "📦 Found IPA at: $IPA_PATH"
863
970
 
864
971
  if [ ! -f "$IPA_PATH" ]; then
865
- echo "❌ IPA 파일을 찾을 수 없습니다!"
972
+ m ios_test_tf.ipa_missing
866
973
  ls -la ios/build/ipa/
867
974
  exit 1
868
975
  fi
869
976
 
870
- # Release notes 준비 (final_release_notes.txt는 레포 루트 기준 — working-directory가 FLUTTER_PROJECT_DIR이므로 절대경로 사용)
977
+ # Prepare release notes (final_release_notes.txt is relative to the repo root — use an absolute path since working-directory is FLUTTER_PROJECT_DIR)
871
978
  if [ -f "$GITHUB_WORKSPACE/final_release_notes.txt" ]; then
872
979
  RELEASE_NOTES=$(cat "$GITHUB_WORKSPACE/final_release_notes.txt")
873
980
  echo "📝 Release Notes:"
874
981
  echo "$RELEASE_NOTES"
875
982
  else
876
- RELEASE_NOTES="테스트 빌드 #${{ needs.prepare-test-build.outputs.build_number }}"
877
- echo "📝 기본 Release Notes: $RELEASE_NOTES"
983
+ RELEASE_NOTES="$(m ios_test_tf.notes_build build="${{ needs.prepare-test-build.outputs.build_number }}")"
984
+ m ios_test_tf.default_notes_echo notes="$RELEASE_NOTES"
878
985
  fi
879
986
 
880
- # 환경변수 설정 (Fastfile에서 사용)
987
+ # Set the environment variables (used by the Fastfile)
881
988
  export API_KEY_PATH="$HOME/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8"
882
989
  export IPA_PATH="$IPA_PATH"
883
990
  export RELEASE_NOTES="$RELEASE_NOTES"
@@ -886,37 +993,42 @@ jobs:
886
993
  echo "🔧 IPA_PATH: $IPA_PATH"
887
994
  echo "🔧 API_KEY_PATH: $API_KEY_PATH"
888
995
 
889
- # Fastlane 실행 (ios/fastlane/Fastfile 사용)
996
+ # Run Fastlane (uses ios/fastlane/Fastfile)
890
997
  cd ios
891
998
  bundle exec fastlane upload_testflight
892
999
 
893
1000
  - name: Notify TestFlight Upload Success
894
1001
  if: success()
895
1002
  run: |
896
- echo "✅ TestFlight 테스트 빌드 업로드 성공!"
897
- echo "버전: ${{ needs.prepare-test-build.outputs.version }}"
898
- echo "빌드 번호: ${{ needs.prepare-test-build.outputs.build_number }}"
1003
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
1004
+ m ios_test_tf.upload_ok
1005
+ m ios_test_tf.upload_version version="${{ needs.prepare-test-build.outputs.version }}"
1006
+ m ios_test_tf.upload_build build="${{ needs.prepare-test-build.outputs.build_number }}"
899
1007
  if [ -n "${{ needs.prepare-test-build.outputs.pr_number }}" ]; then
900
- echo "PR 번호: ${{ needs.prepare-test-build.outputs.pr_number }}"
1008
+ m ios_test_tf.upload_pr pr="${{ needs.prepare-test-build.outputs.pr_number }}"
901
1009
  fi
902
- echo "브랜치: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}"
903
- echo "커밋: ${{ github.sha }}"
1010
+ m ios_test_tf.upload_branch branch="${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}"
1011
+ m ios_test_tf.upload_commit sha="${{ github.sha }}"
904
1012
  if [ -n "${{ needs.prepare-test-build.outputs.issue_url }}" ]; then
905
- echo "관련 이슈: ${{ needs.prepare-test-build.outputs.issue_url }}"
1013
+ m ios_test_tf.upload_issue url="${{ needs.prepare-test-build.outputs.issue_url }}"
906
1014
  fi
907
1015
 
908
1016
  - name: Notify on Failure
909
1017
  if: failure()
910
1018
  run: |
911
- echo "❌ TestFlight 테스트 빌드 업로드 실패!"
912
- echo "로그를 확인해주세요."
1019
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
1020
+ m ios_test_tf.upload_failed
1021
+ m ios_test_tf.check_logs
913
1022
 
914
- - name: 진행 상황 최종 업데이트 - 성공
1023
+ - name: Final progress update - success
915
1024
  if: success() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != ''
916
1025
  uses: actions/github-script@v9
917
1026
  with:
918
1027
  github-token: ${{ secrets.GITHUB_TOKEN }}
919
1028
  script: |
1029
+ const M = JSON.parse(process.env.PAW_MSG);
1030
+ const msg = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1031
+ const row = (name, status, elapsed = '-') => `| ${msg('ios_test_tf.step_' + name)} | ${msg('ios_test_tf.st_' + status)} | ${elapsed} |`;
920
1032
  const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}');
921
1033
  const startTime = parseInt('${{ needs.notify-start.outputs.start_time }}');
922
1034
  const step2EndTime = parseInt('${{ needs.build-ios-test.outputs.step2_end_time }}');
@@ -931,37 +1043,37 @@ jobs:
931
1043
 
932
1044
  const now = Date.now();
933
1045
 
934
- // 배포 단계 소요 시간 (step2 종료 ~ 현재)
1046
+ // Deploy step duration (step2 end to now)
935
1047
  const step3_s = Math.round((now - step2EndTime) / 1000);
936
1048
  const s3Min = Math.floor(step3_s / 60);
937
1049
  const s3Sec = step3_s % 60;
938
- const step3Elapsed = s3Min > 0 ? `${s3Min}분 ${s3Sec}초` : `${s3Sec}초`;
1050
+ const step3Elapsed = s3Min > 0 ? msg('ios_test_tf.elapsed_min_sec', { m: s3Min, s: s3Sec }) : msg('ios_test_tf.elapsed_sec', { s: s3Sec });
939
1051
 
940
- // 전체 소요 시간
1052
+ // Total duration
941
1053
  const total_s = Math.round((now - startTime) / 1000);
942
1054
  const tMin = Math.floor(total_s / 60);
943
1055
  const tSec = total_s % 60;
944
- const totalElapsed = tMin > 0 ? `${tMin}분 ${tSec}초` : `${tSec}초`;
1056
+ const totalElapsed = tMin > 0 ? msg('ios_test_tf.elapsed_min_sec', { m: tMin, s: tSec }) : msg('ios_test_tf.elapsed_sec', { s: tSec });
945
1057
 
946
1058
  const body = [
947
- '## 🍎 ✅ iOS TestFlight 배포 완료',
1059
+ msg('ios_test_tf.title_deploy_done'),
948
1060
  '',
949
- '| 단계 | 상태 | 소요 시간 |',
1061
+ msg('ios_test_tf.table_header'),
950
1062
  '|------|------|----------|',
951
- `| 🔧 준비 | ✅ 완료 | ${step1Elapsed || '-'} |`,
952
- `| 🔨 IPA 빌드 | ✅ 완료 | ${step2Elapsed || '-'} |`,
953
- `| 📤 TestFlight 배포 | ✅ 완료 | ${step3Elapsed} |`,
1063
+ row('prepare', 'done', step1Elapsed || '-'),
1064
+ row('build', 'done', step2Elapsed || '-'),
1065
+ row('deploy', 'done', step3Elapsed),
954
1066
  '',
955
- '| 항목 | 값 |',
1067
+ msg('ios_test_tf.kv_head'),
956
1068
  '|------|-----|',
957
- `| **앱 버전** | \`${version}(${buildNumber})\` |`,
958
- `| **브랜치** | \`${branchName}\` |`,
959
- `| **커밋** | \`${commitHash}\` |`,
960
- `| **총 소요 시간** | ${totalElapsed} |`,
1069
+ msg('ios_test_tf.row_app_version', { version, build: buildNumber }),
1070
+ msg('ios_test_tf.row_branch', { branch: branchName }),
1071
+ msg('ios_test_tf.row_commit', { commit: commitHash }),
1072
+ msg('ios_test_tf.row_total', { elapsed: totalElapsed }),
961
1073
  '',
962
- '📱 **TestFlight에서 테스트 앱을 다운로드할 수 있습니다.**',
1074
+ msg('ios_test_tf.download_hint'),
963
1075
  '',
964
- `🔗 [워크플로우 실행 로그](${runUrl})`
1076
+ msg('ios_test_tf.run_log_link', { url: runUrl })
965
1077
  ].join('\n');
966
1078
 
967
1079
  await github.rest.issues.updateComment({
@@ -971,14 +1083,17 @@ jobs:
971
1083
  body: body
972
1084
  });
973
1085
 
974
- console.log(`✅ 진행 상황 최종 업데이트 완료: 전체 성공 (${totalElapsed})`);
1086
+ console.log(msg('ios_test_tf.log_all_done', { elapsed: totalElapsed }));
975
1087
 
976
- - name: 진행 상황 최종 업데이트 - 실패
1088
+ - name: Final progress update - failure
977
1089
  if: failure() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.notify-start.outputs.comment_id != ''
978
1090
  uses: actions/github-script@v9
979
1091
  with:
980
1092
  github-token: ${{ secrets.GITHUB_TOKEN }}
981
1093
  script: |
1094
+ const M = JSON.parse(process.env.PAW_MSG);
1095
+ const msg = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
1096
+ const row = (name, status, elapsed = '-') => `| ${msg('ios_test_tf.step_' + name)} | ${msg('ios_test_tf.st_' + status)} | ${elapsed} |`;
982
1097
  const commentId = parseInt('${{ needs.notify-start.outputs.comment_id }}');
983
1098
  const startTime = parseInt('${{ needs.notify-start.outputs.start_time }}');
984
1099
  const step2EndTime = parseInt('${{ needs.build-ios-test.outputs.step2_end_time }}') || 0;
@@ -993,33 +1108,33 @@ jobs:
993
1108
 
994
1109
  const now = Date.now();
995
1110
 
996
- // 배포 단계 소요 시간 (step2 종료 ~ 현재)
1111
+ // Deploy step duration (step2 end to now)
997
1112
  let step3Elapsed = '-';
998
1113
  if (step2EndTime > 0) {
999
1114
  const step3_s = Math.round((now - step2EndTime) / 1000);
1000
1115
  const s3Min = Math.floor(step3_s / 60);
1001
1116
  const s3Sec = step3_s % 60;
1002
- step3Elapsed = s3Min > 0 ? `${s3Min}분 ${s3Sec}초` : `${s3Sec}초`;
1117
+ step3Elapsed = s3Min > 0 ? msg('ios_test_tf.elapsed_min_sec', { m: s3Min, s: s3Sec }) : msg('ios_test_tf.elapsed_sec', { s: s3Sec });
1003
1118
  }
1004
1119
 
1005
1120
  const body = [
1006
- '## 🍎 ❌ iOS TestFlight 배포 실패',
1121
+ msg('ios_test_tf.title_deploy_failed'),
1007
1122
  '',
1008
- '| 단계 | 상태 | 소요 시간 |',
1123
+ msg('ios_test_tf.table_header'),
1009
1124
  '|------|------|----------|',
1010
- `| 🔧 준비 | ✅ 완료 | ${step1Elapsed || '-'} |`,
1011
- `| 🔨 IPA 빌드 | ✅ 완료 | ${step2Elapsed || '-'} |`,
1012
- `| 📤 TestFlight 배포 | ❌ 실패 | ${step3Elapsed} |`,
1125
+ row('prepare', 'done', step1Elapsed || '-'),
1126
+ row('build', 'done', step2Elapsed || '-'),
1127
+ row('deploy', 'failed', step3Elapsed),
1013
1128
  '',
1014
- '| 항목 | 값 |',
1129
+ msg('ios_test_tf.kv_head'),
1015
1130
  '|------|-----|',
1016
- `| **앱 버전** | \`${version}(${buildNumber})\` |`,
1017
- `| **브랜치** | \`${branchName}\` |`,
1018
- `| **커밋** | \`${commitHash}\` |`,
1131
+ msg('ios_test_tf.row_app_version', { version, build: buildNumber }),
1132
+ msg('ios_test_tf.row_branch', { branch: branchName }),
1133
+ msg('ios_test_tf.row_commit', { commit: commitHash }),
1019
1134
  '',
1020
- '❌ **TestFlight 배포 중 오류가 발생했습니다.**',
1135
+ msg('ios_test_tf.deploy_error'),
1021
1136
  '',
1022
- `🔗 [워크플로우 실행 로그 확인](${runUrl})`
1137
+ msg('ios_test_tf.run_log_check_link', { url: runUrl })
1023
1138
  ].join('\n');
1024
1139
 
1025
1140
  await github.rest.issues.updateComment({
@@ -1029,4 +1144,4 @@ jobs:
1029
1144
  body: body
1030
1145
  });
1031
1146
 
1032
- console.log(`❌ 진행 상황 최종 업데이트 완료: TestFlight 배포 실패`);
1147
+ console.log(msg('ios_test_tf.log_deploy_failed'));