project-auto-wizard 0.13.3 → 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,42 +1,42 @@
1
1
  # project-auto-wizard:managed-workflow
2
- # Flutter Android 테스트용 APK 빌드 워크플로우
2
+ # Flutter Android test APK build workflow
3
3
  #
4
- # 이 워크플로우는 기능 브랜치에서 수동으로 실행하여
5
- # 테스트용 Android APK를 빌드합니다.
4
+ # Run this workflow manually from a feature branch
5
+ # to build a test Android APK.
6
6
  #
7
- # 주요 특징:
8
- # - workflow_dispatch로 수동 실행 또는 repository_dispatch로 트리거
9
- # - 버전은 version.yml에서 동적으로 읽어 사용 (운영 버전과 동일, 빌드 번호로 구분)
10
- # - 브랜치명에서 이슈 번호 자동 추출 (YYYYMMDD_#이슈번호_내용 형식)
11
- # - GitHub API로 이슈 정보 가져와서 빌드 정보에 포함
12
- # - APK 파일만 생성 (Play Store 배포 제거)
13
- # - 아티팩트로 업로드하여 다운로드 가능 (최대 500MB/아티팩트)
14
- # - 아티팩트 이름에 브랜치명 포함하여 구분 용이
15
- # - Firebase App Distribution 연동 (FIREBASE_SERVICE_ACCOUNT_JSON_BASE64 시크릿 있을 때)
7
+ # Highlights:
8
+ # - Runs manually via workflow_dispatch or is triggered by repository_dispatch
9
+ # - The version is read dynamically from version.yml (same as production; builds are told apart by build number)
10
+ # - Extracts the issue number from the branch name automatically (YYYYMMDD_#issue-number_title format)
11
+ # - Fetches the issue through the GitHub API and includes it in the build info
12
+ # - Only produces the APK file (no Play Store deploy)
13
+ # - Uploaded as an artifact you can download (up to 500MB per artifact)
14
+ # - The artifact name includes the branch name so builds are easy to tell apart
15
+ # - Firebase App Distribution integration (when the FIREBASE_SERVICE_ACCOUNT_JSON_BASE64 secret exists)
16
16
  #
17
- # 사용 방법:
18
- # 1. 기능 브랜치로 체크아웃
19
- # 2. GitHub Actions에서 이 워크플로우 선택
20
- # 3. "Run workflow" 버튼 클릭
21
- # 4. 빌드 완료 후 아티팩트에서 APK 다운로드
17
+ # Usage:
18
+ # 1. Check out a feature branch
19
+ # 2. Select this workflow in GitHub Actions
20
+ # 3. Click the "Run workflow" button
21
+ # 4. When the build finishes, download the APK from the artifacts
22
22
  #
23
- # GitHub Secrets (모두 선택 — 하나도 없어도 테스트 APK는 빌드됩니다)
23
+ # GitHub Secrets (all optional — the test APK builds even with none of them)
24
24
  #
25
- # Release Keystore (Play Store 배포와 동일, 없으면 경고 후 debug 키로 서명):
26
- # RELEASE_KEYSTORE_BASE64 (선택): 릴리즈 키스토어 파일 (base64)
27
- # RELEASE_KEYSTORE_PASSWORD (선택): 키스토어 비밀번호
28
- # RELEASE_KEY_ALIAS (선택): 키 별칭
29
- # RELEASE_KEY_PASSWORD (선택): 키 비밀번호
25
+ # Release Keystore (same as the Play Store deploy; if missing, a warning is shown and the debug key is used):
26
+ # RELEASE_KEYSTORE_BASE64 (optional): release keystore file (base64)
27
+ # RELEASE_KEYSTORE_PASSWORD (optional): keystore password
28
+ # RELEASE_KEY_ALIAS (optional): key alias
29
+ # RELEASE_KEY_PASSWORD (optional): key password
30
30
  #
31
- # 환경 설정 (선택):
32
- # ENV_FILE (선택): .env 파일 내용
33
- # GOOGLE_SERVICES_JSON (선택): google-services.json 내용 (Firebase 사용 시)
31
+ # Environment settings (optional):
32
+ # ENV_FILE (optional): contents of the .env file
33
+ # GOOGLE_SERVICES_JSON (optional): contents of google-services.json (when using Firebase)
34
34
  #
35
- # Firebase App Distribution (선택):
36
- # FIREBASE_SERVICE_ACCOUNT_JSON_BASE64 (선택): Firebase 서비스 계정 JSON (base64)
35
+ # Firebase App Distribution (optional):
36
+ # FIREBASE_SERVICE_ACCOUNT_JSON_BASE64 (optional): Firebase service account JSON (base64)
37
37
  #
38
- # ※ 위 Secrets는 저장소 Settings → Secrets and variables → Actions에 등록하세요.
39
- # Firebase 업로드를 쓰려면 아래 env의 FIREBASE_APP_ID·FIREBASE_TESTER_GROUP도 수정합니다.
38
+ # Note: register the Secrets above under the repository Settings → Secrets and variables → Actions.
39
+ # To use the Firebase upload, also edit FIREBASE_APP_ID and FIREBASE_TESTER_GROUP in the env below.
40
40
 
41
41
  name: PROJECT-Flutter-Android-Test-APK
42
42
 
@@ -49,30 +49,30 @@ on:
49
49
  repository_dispatch:
50
50
  types: [build-android-app]
51
51
 
52
- # 레포 기본 권한이 read여도 빌드 진행 댓글을 달 수 있게 필요한 권한을 직접 선언한다
52
+ # Declare the needed permissions explicitly so progress comments work even when the repository default is read-only
53
53
  permissions:
54
54
  contents: read
55
55
  issues: write
56
56
  pull-requests: write
57
57
 
58
- # 프로젝트별 설정 (아래 값들을 수정하세요)
58
+ # Project settings (edit the values below)
59
59
  env:
60
- # Flutter SDK 버전 — 비워 두면 stable 채널 최신을 쓴다(최신 flutter create 프로젝트의 SDK 제약과 맞음).
61
- # 특정 버전에 고정하려면 "3.38.9"처럼 적는다. pubspec.yaml의 environment.sdk 범위를 만족해야 한다.
60
+ # Flutter SDK version — leave empty to use the latest stable channel (matches the SDK constraint of a fresh flutter create project).
61
+ # To pin a version, write it like "3.38.9". It must satisfy environment.sdk in pubspec.yaml.
62
62
  FLUTTER_VERSION: ""
63
63
  JAVA_VERSION: "__JAVA_VERSION__" # @wizard ask:17
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 .env in dotenv mode (relative to the Flutter root)
69
69
  ENV_FILE_PATH: ".env"
70
70
  FIREBASE_APP_ID: "{FIREBASE_APP_ID}"
71
71
  FIREBASE_TESTER_GROUP: "{TESTER_GROUP}"
72
72
 
73
73
  jobs:
74
74
  prepare-test-build:
75
- name: 테스트 빌드 준비
75
+ name: Prepare test build
76
76
  runs-on: ubuntu-latest
77
77
 
78
78
  outputs:
@@ -89,19 +89,41 @@ jobs:
89
89
  firebase_available: ${{ steps.firebase_check.outputs.available }}
90
90
 
91
91
  steps:
92
- - name: 브랜치 존재 여부 사전 확인
92
+ # Fetch the message catalog from the triggering ref first — the feature branch being built may predate it
93
+ - name: Checkout messages
94
+ uses: actions/checkout@v7
95
+ with:
96
+ path: .paw-msg
97
+ sparse-checkout: |
98
+ .github/scripts/messages.py
99
+ version.yml
100
+ sparse-checkout-cone-mode: false
101
+
102
+ - name: Load messages
103
+ working-directory: ${{ github.workspace }}
104
+ run: |
105
+ cp .paw-msg/.github/scripts/messages.py "$RUNNER_TEMP/paw_messages.py"
106
+ LANG_CODE=$(cd .paw-msg && python3 "$RUNNER_TEMP/paw_messages.py" lang)
107
+ rm -rf .paw-msg
108
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE" >> "$GITHUB_ENV"
109
+ echo "PAW_MESSAGES_PY=$RUNNER_TEMP/paw_messages.py" >> "$GITHUB_ENV"
110
+ echo "PAW_MSG=$(PROJECT_AUTO_WIZARD_LANG=$LANG_CODE python3 "$RUNNER_TEMP/paw_messages.py" dump apk_test.)" >> "$GITHUB_ENV"
111
+
112
+ - name: Verify the branch exists up front
93
113
  id: verify_branch
94
114
  if: github.event_name == 'repository_dispatch'
95
115
  uses: actions/github-script@v9
96
116
  with:
97
117
  github-token: ${{ secrets.GITHUB_TOKEN }}
98
118
  script: |
119
+ const M = JSON.parse(process.env.PAW_MSG);
120
+ const msg = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
99
121
  const branchName = '${{ github.event.client_payload.branch_name }}';
100
122
  const prNumber = '${{ github.event.client_payload.pr_number }}';
101
123
  const runId = '${{ github.run_id }}';
102
124
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
103
125
 
104
- console.log(`🔍 브랜치 존재 여부 확인: ${branchName}`);
126
+ console.log(msg('apk_test.branch_check_log', { branch: branchName }));
105
127
 
106
128
  try {
107
129
  await github.rest.repos.getBranch({
@@ -109,26 +131,26 @@ jobs:
109
131
  repo: context.repo.repo,
110
132
  branch: branchName
111
133
  });
112
- console.log(`✅ 브랜치 존재 확인됨: ${branchName}`);
134
+ console.log(msg('apk_test.branch_exists_log', { branch: branchName }));
113
135
  } catch (error) {
114
136
  if (error.status === 404) {
115
- console.log(`❌ 브랜치를 찾을 수 없음: ${branchName}`);
137
+ console.log(msg('apk_test.branch_not_found_log', { branch: branchName }));
116
138
 
117
- // 에러 댓글 작성
139
+ // Post an error comment
118
140
  const body = [
119
- '🤖 ❌ **Android 테스트 APK 빌드 실패 - 브랜치를 찾을 수 없습니다**',
141
+ msg('apk_test.branch_missing_title'),
120
142
  '',
121
- '| 항목 | 값 |',
143
+ msg('apk_test.kv_head'),
122
144
  '|------|-----|',
123
- `| **요청된 브랜치** | \`${branchName}\` |`,
124
- `| **이슈/PR** | #${prNumber} |`,
145
+ msg('apk_test.row_requested_branch', { branch: branchName }),
146
+ msg('apk_test.row_issue_pr', { number: prNumber }),
125
147
  '',
126
- '### 💡 확인 사항',
127
- '1. 브랜치가 원격 저장소에 push되었는지 확인하세요',
128
- '2. "Issue Helper" 댓글의 브랜치명이 올바른지 확인하세요',
129
- '3. 브랜치명에 오타가 없는지 확인하세요',
148
+ msg('apk_test.hint_title'),
149
+ msg('apk_test.hint_pushed'),
150
+ msg('apk_test.hint_helper_branch'),
151
+ msg('apk_test.hint_typo'),
130
152
  '',
131
- `🔗 [워크플로우 로그](${runUrl})`
153
+ msg('apk_test.workflow_log_link', { url: runUrl })
132
154
  ].join('\n');
133
155
 
134
156
  await github.rest.issues.createComment({
@@ -138,9 +160,9 @@ jobs:
138
160
  body: body
139
161
  });
140
162
 
141
- core.setFailed(`브랜치를 찾을 수 없습니다: ${branchName}`);
163
+ core.setFailed(msg('apk_test.branch_not_found_fail', { branch: branchName }));
142
164
  } else {
143
- core.setFailed(`브랜치 확인 중 오류: ${error.message}`);
165
+ core.setFailed(msg('apk_test.branch_check_error_fail', { error: error.message }));
144
166
  }
145
167
  }
146
168
 
@@ -149,21 +171,25 @@ jobs:
149
171
  env:
150
172
  FIREBASE_SECRET: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON_BASE64 }}
151
173
  run: |
174
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
152
175
  if [ -n "$FIREBASE_SECRET" ]; then
153
176
  echo "available=true" >> $GITHUB_OUTPUT
154
- echo "✅ Firebase 서비스 계정 감지 - App Distribution 업로드 예정"
177
+ m apk_test.firebase_detected
155
178
  else
156
179
  echo "available=false" >> $GITHUB_OUTPUT
157
- echo "⚠️ Firebase 서비스 계정 없음 - App Distribution 업로드 건너뜀"
180
+ m apk_test.firebase_missing
158
181
  fi
159
182
 
160
- - name: 진행 상황 댓글 생성
183
+ - name: Create the progress comment
161
184
  id: progress_comment
162
185
  if: github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != ''
163
186
  uses: actions/github-script@v9
164
187
  with:
165
188
  github-token: ${{ secrets.GITHUB_TOKEN }}
166
189
  script: |
190
+ const M = JSON.parse(process.env.PAW_MSG);
191
+ const msg = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
192
+ const row = (name, status, elapsed = '-') => `| ${msg('apk_test.step_' + name)} | ${msg('apk_test.st_' + status)} | ${elapsed} |`;
167
193
  const prNumber = parseInt('${{ github.event.client_payload.pr_number }}');
168
194
  const runId = '${{ github.run_id }}';
169
195
  const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
@@ -171,22 +197,22 @@ jobs:
171
197
  const firebaseAvailable = '${{ steps.firebase_check.outputs.available }}' === 'true';
172
198
 
173
199
  const rows = [
174
- '| 🔧 준비 | ⏳ 진행 중... | - |',
175
- '| 🔨 APK 빌드 | ⏸️ 대기 | - |',
176
- '| 📤 GitHub 아티팩트 업로드 | ⏸️ 대기 | - |',
200
+ row('prepare', 'running'),
201
+ row('build', 'waiting'),
202
+ row('upload', 'waiting'),
177
203
  ];
178
204
  if (firebaseAvailable) {
179
- rows.push('| 🔥 Firebase App Distribution | ⏸️ 대기 | - |');
205
+ rows.push(row('firebase', 'waiting'));
180
206
  }
181
207
 
182
208
  const body = [
183
- '## 🤖 Android APK 빌드 중...',
209
+ msg('apk_test.title_running'),
184
210
  '',
185
- '| 단계 | 상태 | 소요 시간 |',
211
+ msg('apk_test.table_header'),
186
212
  '|------|------|----------|',
187
213
  ...rows,
188
214
  '',
189
- `📋 **[실시간 로그 보기](${runUrl})**`
215
+ msg('apk_test.live_log', { url: runUrl })
190
216
  ].join('\n');
191
217
 
192
218
  const { data: comment } = await github.rest.issues.createComment({
@@ -196,7 +222,7 @@ jobs:
196
222
  body: body
197
223
  });
198
224
 
199
- console.log(`✅ 진행상황 댓글 생성 완료 (ID: ${comment.id})`);
225
+ console.log(msg('apk_test.log_progress_created', { id: comment.id }));
200
226
  core.setOutput('comment_id', comment.id.toString());
201
227
  core.setOutput('start_time', startTime.toString());
202
228
 
@@ -208,59 +234,63 @@ jobs:
208
234
 
209
235
  - name: Pull latest changes
210
236
  run: |
237
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
211
238
  BRANCH_NAME="${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}"
212
- echo "🌿 브랜치: $BRANCH_NAME"
213
- git pull origin "$BRANCH_NAME" || echo "⚠️ Pull 실패 (이미 최신 상태일 수 있음)"
239
+ m apk_test.pull_branch branch="$BRANCH_NAME"
240
+ git pull origin "$BRANCH_NAME" || m apk_test.pull_failed
214
241
 
215
- - name: 브랜치명에서 이슈 정보 추출
242
+ - name: Extract issue info from the branch name
216
243
  id: issue_info
217
244
  run: |
218
- # repository_dispatch인 경우 client_payload에서 브랜치명 가져오기
245
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
246
+ # For repository_dispatch, take the branch name from client_payload
219
247
  if [ "${{ github.event_name }}" == "repository_dispatch" ]; then
220
248
  BRANCH_NAME="${{ github.event.client_payload.branch_name }}"
221
249
  ISSUE_NUMBER="${{ github.event.client_payload.issue_number }}"
222
250
  else
223
251
  BRANCH_NAME="${{ github.ref_name }}"
224
- # 브랜치명에서 이슈 번호 추출 (#387 형식)
252
+ # Extract the issue number from the branch name (`<date>_#<number>_...` format)
225
253
  ISSUE_NUMBER=$(echo "$BRANCH_NAME" | sed -n 's/.*#\([0-9]*\).*/\1/p')
226
254
  fi
227
255
 
228
- echo "🌿 브랜치명: $BRANCH_NAME"
256
+ m apk_test.branch_name_log branch="$BRANCH_NAME"
229
257
  echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
230
258
 
231
259
  if [ -z "$ISSUE_NUMBER" ]; then
232
- echo "⚠️ 브랜치명에서 이슈 번호를 찾을 수 없습니다."
260
+ m apk_test.issue_number_missing
233
261
  echo "issue_url=" >> $GITHUB_OUTPUT
234
262
  echo "issue_title=" >> $GITHUB_OUTPUT
235
263
  echo "issue_number=" >> $GITHUB_OUTPUT
236
264
  else
237
- echo "✅ 추출된 이슈 번호: #$ISSUE_NUMBER"
265
+ m apk_test.issue_number_extracted number="$ISSUE_NUMBER"
238
266
  ISSUE_URL="https://github.com/${{ github.repository }}/issues/$ISSUE_NUMBER"
239
267
  echo "issue_url=$ISSUE_URL" >> $GITHUB_OUTPUT
240
268
  echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
241
269
 
242
- # GitHub API로 이슈 정보 가져오기
243
- echo "🔍 GitHub API로 이슈 정보 조회 중..."
270
+ # Fetch the issue info through the GitHub API
271
+ m apk_test.issue_fetching
244
272
  ISSUE_RESPONSE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
245
273
  "https://api.github.com/repos/${{ github.repository }}/issues/$ISSUE_NUMBER")
246
274
 
247
- ISSUE_TITLE=$(echo "$ISSUE_RESPONSE" | jq -r '.title // "이슈 정보 없음"')
275
+ NO_ISSUE_INFO="$(m apk_test.issue_title_default)"
276
+ ISSUE_TITLE=$(echo "$ISSUE_RESPONSE" | jq -r --arg d "$NO_ISSUE_INFO" '.title // $d')
248
277
  ISSUE_STATE=$(echo "$ISSUE_RESPONSE" | jq -r '.state // "unknown"')
249
278
 
250
- if [ "$ISSUE_TITLE" = "null" ] || [ "$ISSUE_TITLE" = "이슈 정보 없음" ]; then
251
- echo "⚠️ 이슈 #$ISSUE_NUMBER를 찾을 수 없습니다."
279
+ if [ "$ISSUE_TITLE" = "null" ] || [ "$ISSUE_TITLE" = "$NO_ISSUE_INFO" ]; then
280
+ m apk_test.issue_not_found number="$ISSUE_NUMBER"
252
281
  echo "issue_title=" >> $GITHUB_OUTPUT
253
282
  else
254
- echo "📋 이슈 제목: $ISSUE_TITLE"
255
- echo "📌 이슈 상태: $ISSUE_STATE"
283
+ m apk_test.issue_title_log title="$ISSUE_TITLE"
284
+ m apk_test.issue_state_log state="$ISSUE_STATE"
256
285
  echo "issue_title=$ISSUE_TITLE" >> $GITHUB_OUTPUT
257
286
  fi
258
287
  fi
259
288
 
260
- - name: 빌드 정보 생성
289
+ - name: Generate build info
261
290
  id: build_info
262
291
  run: |
263
- # version_manager.py로 버전 가져오기 (프로덕션 워크플로우와 동일한 방식)
292
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
293
+ # Get the version through version_manager.py (same approach as the production workflow)
264
294
  VERSION=$(python3 .github/scripts/version_manager.py get | tail -n 1)
265
295
  echo "version=$VERSION" >> $GITHUB_OUTPUT
266
296
 
@@ -268,18 +298,18 @@ jobs:
268
298
  COMMIT_SHORT=$(echo "$COMMIT_SHA" | cut -c1-7)
269
299
  BUILD_DATE=$(date '+%Y-%m-%d %H:%M:%S')
270
300
 
271
- # repository_dispatch 이벤트인 경우 PR 번호를 빌드 번호로 사용
301
+ # For repository_dispatch events, use the PR number as the build number
272
302
  if [ "${{ github.event_name }}" == "repository_dispatch" ]; then
273
303
  BUILD_NUMBER="${{ github.event.client_payload.build_number }}"
274
304
  PR_NUMBER="${{ github.event.client_payload.pr_number }}"
275
- echo "📋 repository_dispatch 이벤트 감지"
276
- echo " PR 번호: #$PR_NUMBER"
277
- echo " 빌드 번호: $BUILD_NUMBER (PR 번호 사용)"
305
+ m apk_test.event_dispatch
306
+ m apk_test.event_pr_number pr="$PR_NUMBER"
307
+ m apk_test.event_build_number_pr build="$BUILD_NUMBER"
278
308
  else
279
309
  BUILD_NUMBER="${{ github.run_number }}"
280
310
  PR_NUMBER=""
281
- echo "📋 workflow_dispatch 이벤트 감지"
282
- echo " 빌드 번호: $BUILD_NUMBER (기본값)"
311
+ m apk_test.event_workflow_dispatch
312
+ m apk_test.event_build_number_default build="$BUILD_NUMBER"
283
313
  fi
284
314
 
285
315
  echo "commit_hash=$COMMIT_SHORT" >> $GITHUB_OUTPUT
@@ -287,16 +317,16 @@ jobs:
287
317
  echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
288
318
  echo "build_date=$BUILD_DATE" >> $GITHUB_OUTPUT
289
319
 
290
- echo "📋 빌드 정보:"
291
- echo " 빌드 번호: #$BUILD_NUMBER"
320
+ m apk_test.build_info_header
321
+ m apk_test.build_info_build build="$BUILD_NUMBER"
292
322
  if [ -n "$PR_NUMBER" ]; then
293
- echo " PR 번호: #$PR_NUMBER"
323
+ m apk_test.build_info_pr pr="$PR_NUMBER"
294
324
  fi
295
- echo " 커밋 해시: $COMMIT_SHORT"
296
- echo " 빌드 날짜: $BUILD_DATE"
325
+ m apk_test.build_info_commit commit="$COMMIT_SHORT"
326
+ m apk_test.build_info_date date="$BUILD_DATE"
297
327
 
298
328
  build-android-test:
299
- name: Android 테스트 APK 빌드
329
+ name: Build Android test APK
300
330
  runs-on: ubuntu-latest
301
331
  needs: prepare-test-build
302
332
  defaults:
@@ -304,6 +334,26 @@ jobs:
304
334
  working-directory: ${{ env.FLUTTER_PROJECT_DIR }}
305
335
 
306
336
  steps:
337
+ # Fetch the message catalog from the triggering ref first — the feature branch being built may predate it
338
+ - name: Checkout messages
339
+ uses: actions/checkout@v7
340
+ with:
341
+ path: .paw-msg
342
+ sparse-checkout: |
343
+ .github/scripts/messages.py
344
+ version.yml
345
+ sparse-checkout-cone-mode: false
346
+
347
+ - name: Load messages
348
+ working-directory: ${{ github.workspace }}
349
+ run: |
350
+ cp .paw-msg/.github/scripts/messages.py "$RUNNER_TEMP/paw_messages.py"
351
+ LANG_CODE=$(cd .paw-msg && python3 "$RUNNER_TEMP/paw_messages.py" lang)
352
+ rm -rf .paw-msg
353
+ echo "PROJECT_AUTO_WIZARD_LANG=$LANG_CODE" >> "$GITHUB_ENV"
354
+ echo "PAW_MESSAGES_PY=$RUNNER_TEMP/paw_messages.py" >> "$GITHUB_ENV"
355
+ echo "PAW_MSG=$(PROJECT_AUTO_WIZARD_LANG=$LANG_CODE python3 "$RUNNER_TEMP/paw_messages.py" dump apk_test.)" >> "$GITHUB_ENV"
356
+
307
357
  - name: Checkout repository
308
358
  uses: actions/checkout@v7
309
359
  with:
@@ -313,7 +363,7 @@ jobs:
313
363
  - name: Pull latest changes
314
364
  run: git pull origin ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}
315
365
 
316
- # 환경변수 준비 — ENV_MODE=dotenv면 .env 파일, dart-define이면 프로젝트 밖 임시 파일(--dart-define-from-file)
366
+ # Prepare env vars — a .env file when ENV_MODE=dotenv, otherwise a temp file outside the project (--dart-define-from-file)
317
367
  - name: Prepare env file
318
368
  env:
319
369
  ENV_CONTENT: ${{ secrets.ENV_FILE || secrets.ENV }}
@@ -336,28 +386,29 @@ jobs:
336
386
  RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }}
337
387
  RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }}
338
388
  run: |
339
- # 테스트 빌드는 설정 없이도 돌아야 한다 — Secret이 비면 keystore·key.properties를 만들지 않고
340
- # 프로젝트 기본 서명(flutter create 기본값은 release도 debug 키)으로 빌드한다. 어떤 키인지는 로그에 남긴다.
389
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
390
+ # A test build must work without any setup — when a Secret is empty, keystore and key.properties are not created
391
+ # and the build uses the project's default signing (flutter create defaults to the debug key even for release). The key in use is logged.
341
392
  MISSING=""
342
393
  for name in RELEASE_KEYSTORE_BASE64 RELEASE_KEYSTORE_PASSWORD RELEASE_KEY_ALIAS RELEASE_KEY_PASSWORD; do
343
394
  [ -n "${!name}" ] || MISSING="$MISSING $name"
344
395
  done
345
396
  if [ -n "$MISSING" ]; then
346
- echo "::warning::서명 Secret이 비어 있어 debug 키로 서명합니다:$MISSING (스토어 업로드용 키가 아닙니다)"
397
+ echo "::warning::$(m apk_test.sign_debug_warning missing="$MISSING")"
347
398
  echo "mode=debug" >> "$GITHUB_OUTPUT"
348
399
  exit 0
349
400
  fi
350
- # 값이 있는데 깨진 keystore는 조용히 넘기지 않고 여기서 실패한다
401
+ # A broken keystore with a value present is not skipped silently — fail here
351
402
  mkdir -p android/keystore
352
403
  printf '%s' "$RELEASE_KEYSTORE_BASE64" | base64 -d > android/keystore/key.jks
353
404
  if [ ! -s android/keystore/key.jks ]; then
354
- echo "::error::RELEASE_KEYSTORE_BASE64를 디코딩한 keystore가 비어 있습니다"
405
+ echo "::error::$(m apk_test.sign_keystore_empty)"
355
406
  exit 1
356
407
  fi
357
- # 비밀번호·alias가 keystore와 맞지 않으면 여기서 멈춘다 (gradle 서명 단계의 긴 오류 대신)
408
+ # Stop here if the password/alias does not match the keystore (instead of a long error in the gradle signing step)
358
409
  keytool -list -keystore android/keystore/key.jks -storepass "$RELEASE_KEYSTORE_PASSWORD" -alias "$RELEASE_KEY_ALIAS" > /dev/null
359
410
  echo "mode=release" >> "$GITHUB_OUTPUT"
360
- echo "✅ Release Keystore 생성 완료 (release 키로 서명)"
411
+ m apk_test.sign_release_ok
361
412
  ls -la android/keystore/
362
413
 
363
414
  - name: Create key.properties
@@ -367,15 +418,16 @@ jobs:
367
418
  KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }}
368
419
  KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }}
369
420
  run: |
421
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
370
422
  cat > android/key.properties << EOF
371
423
  storeFile=keystore/key.jks
372
424
  storePassword=$STORE_PASSWORD
373
425
  keyAlias=$KEY_ALIAS
374
426
  keyPassword=$KEY_PASSWORD
375
427
  EOF
376
- echo "✅ key.properties 생성 완료"
428
+ m apk_test.key_properties_ok
377
429
 
378
- # google-services.json 생성 (선택적)
430
+ # Create google-services.json (optional)
379
431
  - name: Create google-services.json
380
432
  shell: bash
381
433
  env:
@@ -429,12 +481,13 @@ jobs:
429
481
  - name: Setup Gradle
430
482
  working-directory: ${{ env.FLUTTER_PROJECT_DIR }}/android
431
483
  run: |
432
- # flutter create 기본 .gitignore는 gradlew를 커밋하지 않는다 — 없으면 flutter build가 만든다
484
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
485
+ # The default flutter create .gitignore does not commit gradlew — flutter build creates it when missing
433
486
  if [ -f gradlew ]; then
434
487
  chmod +x gradlew
435
488
  echo "✅ Gradle wrapper permissions set"
436
489
  else
437
- echo "ℹ️ gradlew가 저장소에 없습니다 — flutter build가 생성합니다"
490
+ m apk_test.gradlew_missing
438
491
  fi
439
492
 
440
493
  - name: Set up Java
@@ -448,13 +501,16 @@ jobs:
448
501
  echo "✅ Java setup completed"
449
502
  java -version
450
503
 
451
- - name: 진행상황 - 준비 완료
504
+ - name: Progress - prepare done
452
505
  id: progress_step1
453
506
  if: github.event_name == 'repository_dispatch' && needs.prepare-test-build.outputs.progress_comment_id != ''
454
507
  uses: actions/github-script@v9
455
508
  with:
456
509
  github-token: ${{ secrets.GITHUB_TOKEN }}
457
510
  script: |
511
+ const M = JSON.parse(process.env.PAW_MSG);
512
+ const msg = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
513
+ const row = (name, status, elapsed = '-') => `| ${msg('apk_test.step_' + name)} | ${msg('apk_test.st_' + status)} | ${elapsed} |`;
458
514
  const commentId = parseInt('${{ needs.prepare-test-build.outputs.progress_comment_id }}');
459
515
  const startTime = parseInt('${{ needs.prepare-test-build.outputs.progress_start_time }}');
460
516
  const runId = '${{ github.run_id }}';
@@ -464,26 +520,26 @@ jobs:
464
520
  const elapsed = Math.round((now - startTime) / 1000);
465
521
  const minutes = Math.floor(elapsed / 60);
466
522
  const seconds = elapsed % 60;
467
- const elapsedStr = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
523
+ const elapsedStr = minutes > 0 ? msg('apk_test.elapsed_min_sec', { m: minutes, s: seconds }) : msg('apk_test.elapsed_sec', { s: seconds });
468
524
  const firebaseAvailable = '${{ needs.prepare-test-build.outputs.firebase_available }}' === 'true';
469
525
 
470
526
  const rows = [
471
- `| 🔧 준비 | ✅ 완료 | ${elapsedStr} |`,
472
- '| 🔨 APK 빌드 | ⏳ 진행 중... | - |',
473
- '| 📤 GitHub 아티팩트 업로드 | ⏸️ 대기 | - |',
527
+ row('prepare', 'done', elapsedStr),
528
+ row('build', 'running'),
529
+ row('upload', 'waiting'),
474
530
  ];
475
531
  if (firebaseAvailable) {
476
- rows.push('| 🔥 Firebase App Distribution | ⏸️ 대기 | - |');
532
+ rows.push(row('firebase', 'waiting'));
477
533
  }
478
534
 
479
535
  const body = [
480
- '## 🤖 Android APK 빌드 중...',
536
+ msg('apk_test.title_running'),
481
537
  '',
482
- '| 단계 | 상태 | 소요 시간 |',
538
+ msg('apk_test.table_header'),
483
539
  '|------|------|----------|',
484
540
  ...rows,
485
541
  '',
486
- `📋 **[실시간 로그 보기](${runUrl})**`
542
+ msg('apk_test.live_log', { url: runUrl })
487
543
  ].join('\n');
488
544
 
489
545
  await github.rest.issues.updateComment({
@@ -493,42 +549,43 @@ jobs:
493
549
  body: body
494
550
  });
495
551
 
496
- console.log(`✅ 진행상황 업데이트: 준비 완료 (${elapsedStr})`);
552
+ console.log(msg('apk_test.log_prepare_done', { elapsed: elapsedStr }));
497
553
  core.setOutput('elapsed', elapsedStr);
498
554
  core.setOutput('end_time', now.toString());
499
555
 
500
556
  - name: Generate APK filename
501
557
  id: apk_filename
502
558
  run: |
559
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
503
560
  REPO_NAME="${{ github.event.repository.name }}"
504
561
  COMMIT_SHORT="${{ needs.prepare-test-build.outputs.commit_hash }}"
505
562
  BUILD_NUMBER="${{ needs.prepare-test-build.outputs.build_number }}"
506
563
  ISSUE_NUMBER="${{ needs.prepare-test-build.outputs.issue_number }}"
507
564
  ISSUE_TITLE="${{ needs.prepare-test-build.outputs.issue_title }}"
508
565
 
509
- # 이슈 제목 정제
566
+ # Clean up the issue title
510
567
  CLEAN_TITLE=""
511
568
  if [ -n "$ISSUE_TITLE" ]; then
512
569
  CLEAN_TITLE="$ISSUE_TITLE"
513
- # 1. 이모지 제거 (유니코드 이모지 범위)
570
+ # 1. Remove emoji (Unicode emoji ranges)
514
571
  CLEAN_TITLE=$(echo "$CLEAN_TITLE" | sed 's/[\xF0\x9F][\x80-\xBF][\x80-\xBF][\x80-\xBF]//g; s/[\xE2\x9C\xE2\x9A\xE2\xAD\xE2\x9D\xE2\x99\xE2\x98\xE2\x9E\xE2\xAC\xE2\xAB\xE2\xAC][\x80-\xBF][\x80-\xBF]//g; s/[\xE2\x80\x8D]//g; s/\xEF\xB8\x8F//g; s/\xE2\x83\xA3//g')
515
- # 2. 대괄호 태그 제거 [xxx]
572
+ # 2. Remove bracket tags [xxx]
516
573
  CLEAN_TITLE=$(echo "$CLEAN_TITLE" | sed 's/\[[^]]*\]//g')
517
- # 3. 앞뒤 공백 trim
574
+ # 3. Trim leading/trailing whitespace
518
575
  CLEAN_TITLE=$(echo "$CLEAN_TITLE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
519
- # 4. 공백 → _
576
+ # 4. Whitespace -> _
520
577
  CLEAN_TITLE=$(echo "$CLEAN_TITLE" | sed 's/[[:space:]]/_/g')
521
- # 5. 파일시스템 비호환 문자 제거
578
+ # 5. Remove characters that are invalid in file names
522
579
  CLEAN_TITLE=$(echo "$CLEAN_TITLE" | sed 's/[#\/\\:*?"<>|]//g')
523
- # 6. 연속 밑줄 정리
580
+ # 6. Collapse repeated underscores
524
581
  CLEAN_TITLE=$(echo "$CLEAN_TITLE" | sed 's/__*/_/g; s/^_//; s/_$//')
525
- # 7. 30자 제한
582
+ # 7. Limit to 30 characters
526
583
  CLEAN_TITLE=$(echo "$CLEAN_TITLE" | cut -c1-30)
527
- # 8. 끝 밑줄 제거
584
+ # 8. Remove a trailing underscore
528
585
  CLEAN_TITLE=$(echo "$CLEAN_TITLE" | sed 's/_$//')
529
586
  fi
530
587
 
531
- # 파일명 조립
588
+ # Assemble the file name
532
589
  if [ -n "$ISSUE_NUMBER" ] && [ -n "$CLEAN_TITLE" ]; then
533
590
  APK_NAME="${REPO_NAME}-${ISSUE_NUMBER}-${BUILD_NUMBER}-${CLEAN_TITLE}-${COMMIT_SHORT}.apk"
534
591
  elif [ -n "$ISSUE_NUMBER" ]; then
@@ -537,56 +594,62 @@ jobs:
537
594
  APK_NAME="${REPO_NAME}-${BUILD_NUMBER}-${COMMIT_SHORT}.apk"
538
595
  fi
539
596
 
540
- # 아티팩트 이름 (확장자 제거)
597
+ # Artifact name (without the extension)
541
598
  ARTIFACT_NAME="${APK_NAME%.apk}"
542
599
  echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT
543
600
  echo "apk_name=$APK_NAME" >> $GITHUB_OUTPUT
544
- echo "📦 APK 파일명: $APK_NAME"
601
+ m apk_test.apk_filename_log name="$APK_NAME"
545
602
 
546
603
  - name: Set test build version
547
604
  run: |
605
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
548
606
  VERSION="${{ needs.prepare-test-build.outputs.version }}"
549
607
  BUILD_NUMBER="${{ needs.prepare-test-build.outputs.build_number }}"
550
- echo "📋 테스트 빌드 버전 설정: ${VERSION}+${BUILD_NUMBER}"
608
+ m apk_test.version_set_log version="${VERSION}+${BUILD_NUMBER}"
551
609
  sed -i "s/^version: .*/version: ${VERSION}+${BUILD_NUMBER}/" pubspec.yaml
552
- echo "✅ pubspec.yaml 버전 변경 완료:"
610
+ m apk_test.pubspec_updated
553
611
  grep "^version:" pubspec.yaml
554
612
 
555
- # APK 빌드 (flutter로 직접 빌드)
613
+ # Build the APK (directly with flutter)
556
614
  - name: Build APK
557
615
  run: |
558
- echo "📦 Flutter 직접 빌드..."
616
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
617
+ m apk_test.flutter_build_log
559
618
  flutter build apk --release ${DART_DEFINE_FILE:+--dart-define-from-file="$DART_DEFINE_FILE"}
560
619
  ls -la ./build/app/outputs/flutter-apk/ || true
561
620
  echo "✅ APK built"
562
621
 
563
622
  - name: Rename and Prepare APK
564
623
  run: |
624
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
565
625
  mkdir -p ./android/app/build/outputs/apk/release/
566
626
  APK_NAME="${{ steps.apk_filename.outputs.apk_name }}"
567
627
  mv ./build/app/outputs/flutter-apk/app-release.apk ./android/app/build/outputs/apk/release/${APK_NAME}
568
628
  echo "✅ APK renamed to ${APK_NAME}"
569
629
  ls -la ./android/app/build/outputs/apk/release/
570
630
 
571
- # APK 파일 크기 확인
631
+ # Check the APK file size
572
632
  APK_SIZE=$(stat -c%s "./android/app/build/outputs/apk/release/${APK_NAME}" 2>/dev/null || stat -f%z "./android/app/build/outputs/apk/release/${APK_NAME}" 2>/dev/null || echo "0")
573
633
  APK_SIZE_MB=$(echo "scale=2; $APK_SIZE / 1024 / 1024" | bc)
574
- echo "📦 APK 파일 크기: ${APK_SIZE_MB}MB"
634
+ m apk_test.apk_size_log size="$APK_SIZE_MB"
575
635
 
576
- # 아티팩트 용량 제한 확인 (500MB)
636
+ # Check the artifact size limit (500MB)
577
637
  if [ "$APK_SIZE" -gt 524288000 ]; then
578
- echo "⚠️ 경고: APK 파일 크기가 500MB를 초과합니다 (${APK_SIZE_MB}MB)"
638
+ m apk_test.apk_size_over size="$APK_SIZE_MB"
579
639
  else
580
- echo "✅ APK 파일 크기가 아티팩트 제한 내입니다 (${APK_SIZE_MB}MB / 500MB)"
640
+ m apk_test.apk_size_ok size="$APK_SIZE_MB"
581
641
  fi
582
642
 
583
- - name: 진행상황 - APK 빌드 완료
643
+ - name: Progress - APK build done
584
644
  id: progress_step2
585
645
  if: github.event_name == 'repository_dispatch' && needs.prepare-test-build.outputs.progress_comment_id != ''
586
646
  uses: actions/github-script@v9
587
647
  with:
588
648
  github-token: ${{ secrets.GITHUB_TOKEN }}
589
649
  script: |
650
+ const M = JSON.parse(process.env.PAW_MSG);
651
+ const msg = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
652
+ const row = (name, status, elapsed = '-') => `| ${msg('apk_test.step_' + name)} | ${msg('apk_test.st_' + status)} | ${elapsed} |`;
590
653
  const commentId = parseInt('${{ needs.prepare-test-build.outputs.progress_comment_id }}');
591
654
  const step1EndTime = parseInt('${{ steps.progress_step1.outputs.end_time }}');
592
655
  const step1Elapsed = '${{ steps.progress_step1.outputs.elapsed }}';
@@ -597,26 +660,26 @@ jobs:
597
660
  const elapsed = Math.round((now - step1EndTime) / 1000);
598
661
  const minutes = Math.floor(elapsed / 60);
599
662
  const seconds = elapsed % 60;
600
- const elapsedStr = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
663
+ const elapsedStr = minutes > 0 ? msg('apk_test.elapsed_min_sec', { m: minutes, s: seconds }) : msg('apk_test.elapsed_sec', { s: seconds });
601
664
  const firebaseAvailable = '${{ needs.prepare-test-build.outputs.firebase_available }}' === 'true';
602
665
 
603
666
  const rows = [
604
- `| 🔧 준비 | ✅ 완료 | ${step1Elapsed || '-'} |`,
605
- `| 🔨 APK 빌드 | ✅ 완료 | ${elapsedStr} |`,
606
- '| 📤 GitHub 아티팩트 업로드 | ⏳ 진행 중... | - |',
667
+ row('prepare', 'done', step1Elapsed || '-'),
668
+ row('build', 'done', elapsedStr),
669
+ row('upload', 'running'),
607
670
  ];
608
671
  if (firebaseAvailable) {
609
- rows.push('| 🔥 Firebase App Distribution | ⏸️ 대기 | - |');
672
+ rows.push(row('firebase', 'waiting'));
610
673
  }
611
674
 
612
675
  const body = [
613
- '## 🤖 Android APK 빌드 중...',
676
+ msg('apk_test.title_running'),
614
677
  '',
615
- '| 단계 | 상태 | 소요 시간 |',
678
+ msg('apk_test.table_header'),
616
679
  '|------|------|----------|',
617
680
  ...rows,
618
681
  '',
619
- `📋 **[실시간 로그 보기](${runUrl})**`
682
+ msg('apk_test.live_log', { url: runUrl })
620
683
  ].join('\n');
621
684
 
622
685
  await github.rest.issues.updateComment({
@@ -626,12 +689,13 @@ jobs:
626
689
  body: body
627
690
  });
628
691
 
629
- console.log(`✅ 진행상황 업데이트: APK 빌드 완료 (${elapsedStr})`);
692
+ console.log(msg('apk_test.log_build_done', { elapsed: elapsedStr }));
630
693
  core.setOutput('elapsed', elapsedStr);
631
694
  core.setOutput('end_time', now.toString());
632
695
 
633
696
  - name: Create build info file
634
697
  run: |
698
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
635
699
  BUILD_NUMBER="${{ needs.prepare-test-build.outputs.build_number }}"
636
700
  PR_NUMBER="${{ needs.prepare-test-build.outputs.pr_number }}"
637
701
  BRANCH_NAME="${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}"
@@ -639,44 +703,42 @@ jobs:
639
703
  COMMIT_SHORT="${{ needs.prepare-test-build.outputs.commit_hash }}"
640
704
  BUILD_DATE=$(date '+%Y-%m-%d %H:%M:%S')
641
705
 
642
- cat > build-info.txt << EOF
643
- 테스트 APK 빌드 정보
644
-
645
- 빌드 번호: #$BUILD_NUMBER
646
- EOF
706
+ {
707
+ m apk_test.info_title
708
+ echo
709
+ m apk_test.info_build_number build="$BUILD_NUMBER"
710
+ } > build-info.txt
647
711
 
648
712
  if [ -n "$PR_NUMBER" ]; then
649
- cat >> build-info.txt << EOF
650
- PR 번호: #$PR_NUMBER
651
- EOF
713
+ m apk_test.info_pr_number pr="$PR_NUMBER" >> build-info.txt
652
714
  fi
653
715
 
654
- cat >> build-info.txt << EOF
655
- 브랜치: $BRANCH_NAME
656
- 커밋: $COMMIT_SHORT
657
- 전체 커밋 해시: $COMMIT_SHA
658
- 빌드 날짜: $BUILD_DATE
659
-
660
- EOF
661
-
662
- # 이슈 정보가 있으면 추가
716
+ {
717
+ m apk_test.info_branch branch="$BRANCH_NAME"
718
+ m apk_test.info_commit commit="$COMMIT_SHORT"
719
+ m apk_test.info_full_commit sha="$COMMIT_SHA"
720
+ m apk_test.info_date date="$BUILD_DATE"
721
+ echo
722
+ } >> build-info.txt
723
+
724
+ # Add the issue info when available
663
725
  if [ -n "${{ needs.prepare-test-build.outputs.issue_number }}" ]; then
664
726
  ISSUE_NUMBER="${{ needs.prepare-test-build.outputs.issue_number }}"
665
727
  ISSUE_TITLE="${{ needs.prepare-test-build.outputs.issue_title }}"
666
728
  ISSUE_URL="${{ needs.prepare-test-build.outputs.issue_url }}"
667
729
 
668
- cat >> build-info.txt << EOF
669
- 관련 이슈:
670
- - #$ISSUE_NUMBER: $ISSUE_TITLE
671
- - URL: $ISSUE_URL
672
-
673
- EOF
730
+ {
731
+ m apk_test.info_issue_header
732
+ echo "- #$ISSUE_NUMBER: $ISSUE_TITLE"
733
+ echo "- URL: $ISSUE_URL"
734
+ echo
735
+ } >> build-info.txt
674
736
  fi
675
737
 
676
- echo "📋 빌드 정보 파일 생성 완료:"
738
+ m apk_test.info_file_created
677
739
  cat build-info.txt
678
740
 
679
- # 빌드 메타데이터 파일 생성 (댓글 작성 워크플로우용)
741
+ # Create the build metadata file (for the comment-posting workflow)
680
742
  - name: Create build metadata file
681
743
  run: |
682
744
  cat > build-metadata.json << EOF
@@ -689,7 +751,7 @@ jobs:
689
751
  EOF
690
752
  cat build-metadata.json
691
753
 
692
- # APK와 빌드 정보를 아티팩트로 업로드 (브랜치명 포함)
754
+ # Upload the APK and build info as an artifact (the name includes the branch name)
693
755
  - name: Upload APK and build info as Artifact
694
756
  id: upload_artifact
695
757
  uses: actions/upload-artifact@v7
@@ -708,23 +770,47 @@ jobs:
708
770
  uses: actions/github-script@v9
709
771
  with:
710
772
  script: |
773
+ const M = JSON.parse(process.env.PAW_MSG);
774
+ const msg = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
711
775
  const step2EndTime = parseInt('${{ steps.progress_step2.outputs.end_time }}');
712
776
  const now = Date.now();
713
777
  const elapsed = Math.round((now - step2EndTime) / 1000);
714
778
  const minutes = Math.floor(elapsed / 60);
715
779
  const seconds = elapsed % 60;
716
- const elapsedStr = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
780
+ const elapsedStr = minutes > 0 ? msg('apk_test.elapsed_min_sec', { m: minutes, s: seconds }) : msg('apk_test.elapsed_sec', { s: seconds });
717
781
  core.setOutput('elapsed', elapsedStr);
718
782
  core.setOutput('end_time', now.toString());
719
783
 
720
- # Firebase App Distribution 업로드 (시크릿 존재 시에만)
784
+ # Firebase App Distribution upload (only when the secret exists)
721
785
  - name: Setup Firebase credentials file
722
786
  if: needs.prepare-test-build.outputs.firebase_available == 'true'
723
787
  env:
724
788
  FIREBASE_SA_BASE64: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON_BASE64 }}
725
789
  run: |
790
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
726
791
  echo "$FIREBASE_SA_BASE64" | base64 --decode > firebase-service-account.json
727
- echo "✅ Firebase 서비스 계정 JSON 파일 생성 완료"
792
+ m apk_test.firebase_creds_ok
793
+
794
+ - name: Prepare Firebase release notes
795
+ id: firebase_notes
796
+ if: needs.prepare-test-build.outputs.firebase_available == 'true'
797
+ env:
798
+ NOTES_BUILD_NUMBER: ${{ needs.prepare-test-build.outputs.build_number }}
799
+ NOTES_BRANCH: ${{ needs.prepare-test-build.outputs.branch_name }}
800
+ NOTES_COMMIT: ${{ needs.prepare-test-build.outputs.commit_hash }}
801
+ NOTES_ISSUE_NUMBER: ${{ needs.prepare-test-build.outputs.issue_number }}
802
+ NOTES_ISSUE_TITLE: ${{ needs.prepare-test-build.outputs.issue_title }}
803
+ run: |
804
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
805
+ # The release notes go through a multi-line step output so the wording follows the configured language
806
+ {
807
+ echo "notes<<PAW_NOTES_EOF"
808
+ m apk_test.fb_notes_build build="$NOTES_BUILD_NUMBER"
809
+ m apk_test.fb_notes_branch branch="$NOTES_BRANCH"
810
+ m apk_test.fb_notes_commit commit="$NOTES_COMMIT"
811
+ m apk_test.fb_notes_issue number="$NOTES_ISSUE_NUMBER" title="$NOTES_ISSUE_TITLE"
812
+ echo "PAW_NOTES_EOF"
813
+ } >> "$GITHUB_OUTPUT"
728
814
 
729
815
  - name: Upload to Firebase App Distribution
730
816
  id: firebase_upload
@@ -736,11 +822,7 @@ jobs:
736
822
  serviceCredentialsFile: ${{ env.FLUTTER_PROJECT_DIR }}/firebase-service-account.json
737
823
  groups: ${{ env.FIREBASE_TESTER_GROUP }}
738
824
  file: ${{ env.FLUTTER_PROJECT_DIR }}/android/app/build/outputs/apk/release/${{ steps.apk_filename.outputs.apk_name }}
739
- releaseNotes: |
740
- 테스트 빌드 #${{ needs.prepare-test-build.outputs.build_number }}
741
- 브랜치: ${{ needs.prepare-test-build.outputs.branch_name }}
742
- 커밋: ${{ needs.prepare-test-build.outputs.commit_hash }}
743
- 이슈: #${{ needs.prepare-test-build.outputs.issue_number }} ${{ needs.prepare-test-build.outputs.issue_title }}
825
+ releaseNotes: ${{ steps.firebase_notes.outputs.notes }}
744
826
 
745
827
  - name: Cleanup Firebase credentials
746
828
  if: always() && needs.prepare-test-build.outputs.firebase_available == 'true'
@@ -750,12 +832,13 @@ jobs:
750
832
  id: firebase_result
751
833
  if: needs.prepare-test-build.outputs.firebase_available == 'true'
752
834
  run: |
835
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
753
836
  if [ "${{ steps.firebase_upload.outcome }}" == "success" ]; then
754
837
  echo "status=success" >> $GITHUB_OUTPUT
755
- echo "✅ Firebase App Distribution 업로드 성공"
838
+ m apk_test.firebase_upload_ok
756
839
  else
757
840
  echo "status=failure" >> $GITHUB_OUTPUT
758
- echo "❌ Firebase App Distribution 업로드 실패 - APK 아티팩트는 정상 업로드됨"
841
+ m apk_test.firebase_upload_failed
759
842
  fi
760
843
 
761
844
  - name: Record Firebase upload time
@@ -764,20 +847,25 @@ jobs:
764
847
  uses: actions/github-script@v9
765
848
  with:
766
849
  script: |
850
+ const M = JSON.parse(process.env.PAW_MSG);
851
+ const msg = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
767
852
  const artifactEndTime = parseInt('${{ steps.artifact_time.outputs.end_time }}');
768
853
  const now = Date.now();
769
854
  const elapsed = Math.round((now - artifactEndTime) / 1000);
770
855
  const minutes = Math.floor(elapsed / 60);
771
856
  const seconds = elapsed % 60;
772
- const elapsedStr = minutes > 0 ? `${minutes}분 ${seconds}초` : `${seconds}초`;
857
+ const elapsedStr = minutes > 0 ? msg('apk_test.elapsed_min_sec', { m: minutes, s: seconds }) : msg('apk_test.elapsed_sec', { s: seconds });
773
858
  core.setOutput('elapsed', elapsedStr);
774
859
 
775
- - name: 진행 상황 최종 업데이트 - 성공
860
+ - name: Final progress update - success
776
861
  if: success() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.prepare-test-build.outputs.progress_comment_id != ''
777
862
  uses: actions/github-script@v9
778
863
  with:
779
864
  github-token: ${{ secrets.GITHUB_TOKEN }}
780
865
  script: |
866
+ const M = JSON.parse(process.env.PAW_MSG);
867
+ const msg = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
868
+ const row = (name, status, elapsed = '-') => `| ${msg('apk_test.step_' + name)} | ${msg('apk_test.st_' + status)} | ${elapsed} |`;
781
869
  const commentId = parseInt('${{ needs.prepare-test-build.outputs.progress_comment_id }}');
782
870
  const startTime = parseInt('${{ needs.prepare-test-build.outputs.progress_start_time }}');
783
871
  const step1Elapsed = '${{ steps.progress_step1.outputs.elapsed }}';
@@ -798,26 +886,26 @@ jobs:
798
886
  const totalElapsed_s = Math.round((now - startTime) / 1000);
799
887
  const totalMin = Math.floor(totalElapsed_s / 60);
800
888
  const totalSec = totalElapsed_s % 60;
801
- const totalElapsed = totalMin > 0 ? `${totalMin}분 ${totalSec}초` : `${totalSec}초`;
889
+ const totalElapsed = totalMin > 0 ? msg('apk_test.elapsed_min_sec', { m: totalMin, s: totalSec }) : msg('apk_test.elapsed_sec', { s: totalSec });
802
890
 
803
891
  const firebaseAvailable = '${{ needs.prepare-test-build.outputs.firebase_available }}' === 'true';
804
892
  const firebaseSuccess = '${{ steps.firebase_result.outputs.status }}' === 'success';
805
893
 
806
894
  const lines = [
807
- '## 🤖 ✅ Android APK 빌드 완료!',
895
+ msg('apk_test.title_done'),
808
896
  '',
809
- '| 단계 | 상태 | 소요 시간 |',
897
+ msg('apk_test.table_header'),
810
898
  '|------|------|----------|',
811
- `| 🔧 준비 | ✅ 완료 | ${step1Elapsed || '-'} |`,
812
- `| 🔨 APK 빌드 | ✅ 완료 | ${step2Elapsed || '-'} |`,
813
- `| 📤 GitHub 아티팩트 업로드 | ✅ 완료 | ${step3Elapsed || '-'} |`,
899
+ row('prepare', 'done', step1Elapsed || '-'),
900
+ row('build', 'done', step2Elapsed || '-'),
901
+ row('upload', 'done', step3Elapsed || '-'),
814
902
  ];
815
903
 
816
904
  if (firebaseAvailable) {
817
905
  if (firebaseSuccess) {
818
- lines.push(`| 🔥 Firebase App Distribution | ✅ 완료 | ${step4Elapsed || '-'} |`);
906
+ lines.push(row('firebase', 'done', step4Elapsed || '-'));
819
907
  } else {
820
- lines.push(`| 🔥 Firebase App Distribution | ❌ 실패 | ${step4Elapsed || '-'} |`);
908
+ lines.push(row('firebase', 'failed', step4Elapsed || '-'));
821
909
  }
822
910
  }
823
911
 
@@ -825,25 +913,25 @@ jobs:
825
913
 
826
914
  lines.push(
827
915
  '',
828
- '### 📦 빌드 결과',
829
- '| 항목 | 값 |',
916
+ msg('apk_test.result_title'),
917
+ msg('apk_test.kv_head'),
830
918
  '|------|-----|',
831
- `| **앱 버전** | \`${version}(${buildNumber})\` |`,
832
- `| **브랜치** | \`${branchName}\` |`,
833
- `| **커밋** | \`${commitHash}\` |`,
834
- `| **총 소요 시간** | ${totalElapsed} |`,
919
+ msg('apk_test.row_app_version', { version, build: buildNumber }),
920
+ msg('apk_test.row_branch', { branch: branchName }),
921
+ msg('apk_test.row_commit', { commit: commitHash }),
922
+ msg('apk_test.row_total', { elapsed: totalElapsed }),
835
923
  '',
836
924
  );
837
925
 
838
926
  if (firebaseAvailable && firebaseSuccess) {
839
- lines.push('🔥 **Firebase App Tester 앱에서 설치 가능합니다**');
927
+ lines.push(msg('apk_test.firebase_installable'));
840
928
  } else if (firebaseAvailable && !firebaseSuccess) {
841
- lines.push('⚠️ **Firebase 업로드 실패 - 아티팩트에서 APK를 다운로드해주세요**');
929
+ lines.push(msg('apk_test.firebase_failed_hint'));
842
930
  }
843
931
 
844
932
  lines.push(
845
- `📦 **[아티팩트 다운로드](${artifactUrl})**`,
846
- `🔗 **[워크플로우 실행 로그](${runUrl})**`
933
+ msg('apk_test.artifact_link', { url: artifactUrl }),
934
+ msg('apk_test.run_log_link', { url: runUrl })
847
935
  );
848
936
 
849
937
  const body = lines.join('\n');
@@ -855,25 +943,25 @@ jobs:
855
943
  body: body
856
944
  });
857
945
 
858
- console.log(`✅ 진행상황 업데이트: 빌드 성공 (총 ${totalElapsed})`);
946
+ console.log(msg('apk_test.log_success', { elapsed: totalElapsed }));
859
947
 
860
- // PR과 연결된 이슈가 있고, PR 번호와 다른 경우 이슈에도 댓글
948
+ // If an issue is linked to the PR and differs from the PR number, comment on the issue too
861
949
  if (issueNumber && issueNumber !== '' && parseInt(issueNumber) !== prNumber) {
862
950
  const issueLines = [
863
- '🤖 ✅ **Android APK 빌드 완료**',
951
+ msg('apk_test.issue_done_title'),
864
952
  '',
865
- `- 앱 버전: \`${version}(${buildNumber})\``,
866
- `- 커밋: \`${commitHash}\``,
953
+ msg('apk_test.issue_line_version', { version, build: buildNumber }),
954
+ msg('apk_test.issue_line_commit', { commit: commitHash }),
867
955
  '',
868
956
  ];
869
957
 
870
958
  if (firebaseAvailable && firebaseSuccess) {
871
- issueLines.push('🔥 **Firebase App Tester 앱에서 설치 가능합니다**');
959
+ issueLines.push(msg('apk_test.firebase_installable'));
872
960
  } else if (firebaseAvailable && !firebaseSuccess) {
873
- issueLines.push('⚠️ **Firebase 업로드 실패 - 아티팩트에서 APK를 다운로드해주세요**');
961
+ issueLines.push(msg('apk_test.firebase_failed_hint'));
874
962
  }
875
963
 
876
- issueLines.push(`📦 **[아티팩트 다운로드](${artifactUrl})**`);
964
+ issueLines.push(msg('apk_test.artifact_link', { url: artifactUrl }));
877
965
 
878
966
  await github.rest.issues.createComment({
879
967
  owner: context.repo.owner,
@@ -881,15 +969,18 @@ jobs:
881
969
  issue_number: parseInt(issueNumber),
882
970
  body: issueLines.join('\n')
883
971
  });
884
- console.log(`✅ 이슈 #${issueNumber}에도 빌드 성공 댓글 작성 완료`);
972
+ console.log(msg('apk_test.log_issue_success', { number: issueNumber }));
885
973
  }
886
974
 
887
- - name: 진행 상황 최종 업데이트 - 실패
975
+ - name: Final progress update - failure
888
976
  if: failure() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && needs.prepare-test-build.outputs.progress_comment_id != ''
889
977
  uses: actions/github-script@v9
890
978
  with:
891
979
  github-token: ${{ secrets.GITHUB_TOKEN }}
892
980
  script: |
981
+ const M = JSON.parse(process.env.PAW_MSG);
982
+ const msg = (k, p = {}) => M[k].replace(/\{(\w+)\}/g, (s, n) => (n in p ? p[n] : s));
983
+ const row = (name, status, elapsed = '-') => `| ${msg('apk_test.step_' + name)} | ${msg('apk_test.st_' + status)} | ${elapsed} |`;
893
984
  const commentId = parseInt('${{ needs.prepare-test-build.outputs.progress_comment_id }}');
894
985
  const startTime = parseInt('${{ needs.prepare-test-build.outputs.progress_start_time }}');
895
986
  const step1EndTime = parseInt('${{ steps.progress_step1.outputs.end_time }}') || 0;
@@ -909,39 +1000,39 @@ jobs:
909
1000
  const step2_s = Math.round((now - step1EndTime) / 1000);
910
1001
  const s2Min = Math.floor(step2_s / 60);
911
1002
  const s2Sec = step2_s % 60;
912
- step2ElapsedStr = s2Min > 0 ? `${s2Min}분 ${s2Sec}초` : `${s2Sec}초`;
1003
+ step2ElapsedStr = s2Min > 0 ? msg('apk_test.elapsed_min_sec', { m: s2Min, s: s2Sec }) : msg('apk_test.elapsed_sec', { s: s2Sec });
913
1004
  }
914
1005
 
915
1006
  const total_s = Math.round((now - startTime) / 1000);
916
1007
  const tMin = Math.floor(total_s / 60);
917
1008
  const tSec = total_s % 60;
918
- const totalElapsed = tMin > 0 ? `${tMin}분 ${tSec}초` : `${tSec}초`;
1009
+ const totalElapsed = tMin > 0 ? msg('apk_test.elapsed_min_sec', { m: tMin, s: tSec }) : msg('apk_test.elapsed_sec', { s: tSec });
919
1010
 
920
1011
  const firebaseAvailable = '${{ needs.prepare-test-build.outputs.firebase_available }}' === 'true';
921
1012
 
922
1013
  const rows = [
923
- `| 🔧 준비 | ✅ 완료 | ${step1Elapsed || '-'} |`,
924
- `| 🔨 APK 빌드 | ❌ 실패 | ${step2ElapsedStr} |`,
925
- '| 📤 GitHub 아티팩트 업로드 | ⏸️ 건너뜀 | - |',
1014
+ row('prepare', 'done', step1Elapsed || '-'),
1015
+ row('build', 'failed', step2ElapsedStr),
1016
+ row('upload', 'skipped'),
926
1017
  ];
927
1018
  if (firebaseAvailable) {
928
- rows.push('| 🔥 Firebase App Distribution | ⏸️ 건너뜀 | - |');
1019
+ rows.push(row('firebase', 'skipped'));
929
1020
  }
930
1021
 
931
1022
  const body = [
932
- '## 🤖 ❌ Android APK 빌드 실패',
1023
+ msg('apk_test.title_failed'),
933
1024
  '',
934
- '| 단계 | 상태 | 소요 시간 |',
1025
+ msg('apk_test.table_header'),
935
1026
  '|------|------|----------|',
936
1027
  ...rows,
937
1028
  '',
938
- `- 앱 버전: \`${version}(${buildNumber})\``,
939
- `- 브랜치: \`${branchName}\``,
940
- `- 총 소요 시간: ${totalElapsed}`,
1029
+ msg('apk_test.issue_line_version', { version, build: buildNumber }),
1030
+ msg('apk_test.fail_line_branch', { branch: branchName }),
1031
+ msg('apk_test.fail_line_total', { elapsed: totalElapsed }),
941
1032
  '',
942
- '❌ 빌드 중 오류가 발생했습니다.',
1033
+ msg('apk_test.build_error'),
943
1034
  '',
944
- `🔗 **[워크플로우 실행 로그 확인](${runUrl})**`
1035
+ msg('apk_test.run_log_check_link', { url: runUrl })
945
1036
  ].join('\n');
946
1037
 
947
1038
  await github.rest.issues.updateComment({
@@ -951,17 +1042,17 @@ jobs:
951
1042
  body: body
952
1043
  });
953
1044
 
954
- console.log(`❌ 진행상황 업데이트: 빌드 실패 (${totalElapsed})`);
1045
+ console.log(msg('apk_test.log_failed', { elapsed: totalElapsed }));
955
1046
 
956
1047
  if (issueNumber && issueNumber !== '' && parseInt(issueNumber) !== prNumber) {
957
1048
  const issueBody = [
958
- '🤖 ❌ **Android APK 빌드 실패**',
1049
+ msg('apk_test.issue_failed_title'),
959
1050
  '',
960
- `- 앱 버전: \`${version}(${buildNumber})\``,
1051
+ msg('apk_test.issue_line_version', { version, build: buildNumber }),
961
1052
  '',
962
- '❌ 빌드 중 오류가 발생했습니다.',
1053
+ msg('apk_test.build_error'),
963
1054
  '',
964
- `🔗 **[워크플로우 실행 로그 확인](${runUrl})**`
1055
+ msg('apk_test.run_log_check_link', { url: runUrl })
965
1056
  ].join('\n');
966
1057
 
967
1058
  await github.rest.issues.createComment({
@@ -970,27 +1061,29 @@ jobs:
970
1061
  issue_number: parseInt(issueNumber),
971
1062
  body: issueBody
972
1063
  });
973
- console.log(`❌ 이슈 #${issueNumber}에도 빌드 실패 댓글 작성 완료`);
1064
+ console.log(msg('apk_test.log_issue_failed', { number: issueNumber }));
974
1065
  }
975
1066
 
976
1067
  - name: Notify Build Success
977
1068
  if: success()
978
1069
  run: |
979
- echo "✅ Android 테스트 APK 빌드 성공!"
980
- echo "빌드 번호: ${{ needs.prepare-test-build.outputs.build_number }}"
1070
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
1071
+ m apk_test.notify_ok
1072
+ m apk_test.notify_build_number build="${{ needs.prepare-test-build.outputs.build_number }}"
981
1073
  if [ -n "${{ needs.prepare-test-build.outputs.pr_number }}" ]; then
982
- echo "PR 번호: ${{ needs.prepare-test-build.outputs.pr_number }}"
1074
+ m apk_test.notify_pr pr="${{ needs.prepare-test-build.outputs.pr_number }}"
983
1075
  fi
984
- echo "브랜치: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}"
985
- echo "커밋: ${{ needs.prepare-test-build.outputs.commit_hash }}"
1076
+ m apk_test.notify_branch branch="${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch_name || github.ref_name }}"
1077
+ m apk_test.notify_commit commit="${{ needs.prepare-test-build.outputs.commit_hash }}"
986
1078
  if [ -n "${{ needs.prepare-test-build.outputs.issue_url }}" ]; then
987
- echo "관련 이슈: ${{ needs.prepare-test-build.outputs.issue_url }}"
1079
+ m apk_test.notify_issue url="${{ needs.prepare-test-build.outputs.issue_url }}"
988
1080
  fi
989
1081
  echo ""
990
- echo "📦 아티팩트에서 APK 파일을 다운로드할 수 있습니다."
1082
+ m apk_test.notify_artifact_hint
991
1083
 
992
1084
  - name: Notify on Failure
993
1085
  if: failure()
994
1086
  run: |
995
- echo "❌ Android 테스트 APK 빌드 실패!"
996
- echo "로그를 확인해주세요."
1087
+ m() { python3 "$PAW_MESSAGES_PY" get "$@"; }
1088
+ m apk_test.notify_fail
1089
+ m apk_test.check_logs