用 Python 自动化 App Store Connect 多语言发版说明上传
每次发版只需改一个 YAML 文件、跑一条命令,9 种语言的「What's New」自动翻译并同步到 App Store Connect。
目录
背景与痛点
iOS 应用上架或更新时,需要在 App Store Connect 为每一种语言单独填写「此版本的新增内容」(What's New)。对于支持多语言的应用,这通常意味着:
-
在网页上逐个语言切换、复制粘贴
-
手动翻译或借助 ChatGPT 逐条处理
-
容易漏语言、版本号对不上、格式不统一
本工具将这一流程压缩为:
编辑 release_notes.yaml → 运行 python3 scripts/upload_notes.py → 完成
脚本会自动完成:读取源语言文案 → 翻译其余语言 → 生成 JWT → 调用 App Store Connect API 批量上传。
方案概览
| 能力 | 说明 |
|------|------|
| 源语言可配置 | 默认 zh-Hans,也可改为 en-US 等任意已配置语言 |
| 自动翻译 | 优先使用 Google 翻译(免费、无需 API Key) |
| 翻译降级 | Google 失败时自动回退 OpenAI GPT-4o-mini(可选) |
| 手动覆盖 | YAML 中某语言已填写内容时,跳过自动翻译 |
| 智能版本匹配 | 自动找到 App Store Connect 中最新可编辑的版本 |
| 本地化增改 | 已有语言记录则 PATCH 更新,否则 POST 新建 |
| 结果预览 | 生成 release_notes_preview_x_x_x.txt 供人工复核 |
**支持的语言(与 App Store Connect locale 一致): **
| 语言代码 | 说明 |
|---------|------|
| en-US | 英语(美国) |
| zh-Hans | 简体中文 |
| zh-Hant | 繁体中文 |
| de-DE | 德语 |
| fr-FR | 法语 |
| es-ES | 西班牙语 |
| pt-BR | 葡萄牙语(巴西) |
| it | 意大利语 |
| tr | 土耳其语 |
技术架构
flowchart LR
A[release_notes.yaml] --> B[upload_notes.py]
B --> C{需要翻译?}
C -->|是| D[Google 翻译]
C -->|否| E[使用原文/手动文案]
D -->|失败| F[OpenAI 备用]
D --> E
F --> E
E --> G[生成 ASC JWT]
G --> H[App Store Connect API]
H --> I[创建/更新 Version Localization]
E --> J[release_notes_preview.txt]
**技术栈: **
-
Python 3 — 脚本运行时
-
PyJWT + ES256 — App Store Connect API 鉴权
-
httpx — HTTP 客户端
-
PyYAML — 读取发版说明配置
-
python-dotenv — 环境变量管理
-
deep-translator — Google 翻译封装
项目结构
scripts/
├── upload_notes.py # 主脚本
├── release_notes.yaml # 发版说明配置(每次发版只改这个)
├── requirements.txt # Python 依赖
├── .env.example # 环境变量模板
├── .env # 实际配置(勿提交 git)
├── AuthKey_XXXXXXXXXX.p8 # ASC API 私钥(勿提交 git)
├── release_notes_preview_*.txt # 运行后生成的翻译预览
└── README.md # 快速使用说明
环境准备与首次配置
1. 安装 Python 依赖
在项目根目录执行(macOS 请使用 python3 / pip3):
pip3 install -r scripts/requirements.txt
requirements.txt 内容:
PyJWT>=2.8.0
httpx>=0.27.0
pyyaml>=6.0.1
python-dotenv>=1.0.0
cryptography>=41.0.0
deep-translator>=1.11.4
2. 获取 App Store Connect API 密钥
-
打开 App Store Connect → 用户与访问 → 集成 → App Store Connect API
-
点击 ** + ** 生成新密钥,权限选择 App 管理(App Manager)
-
下载
.p8私钥文件(只能下载一次,请妥善保存) -
将
.p8文件放到scripts/目录 -
记录页面上的 Key ID 和 Issuer ID
3. 获取应用的 Apple ID
在 App Store Connect → 选择你的 App → App 信息 → 找到 Apple ID(纯数字,例如 1234567890)。
4. 配置环境变量
cp scripts/.env.example scripts/.env
编辑 scripts/.env,填入真实值:
# App Store Connect API(必填)
ASC_KEY_ID=ABCD1234EF
ASC_ISSUER_ID=57246542-96fe-1a63-e053-0824d011072a
ASC_PRIVATE_KEY=scripts/AuthKey_XXXXXXXXXX.p8
ASC_APP_ID=1234567890
# OpenAI API Key(可选,Google 翻译失败时的备用)
# OPENAI_API_KEY=sk-xxxxxxxx
注意:脚本从
scripts/.env加载环境变量(与upload_notes.py同目录),请确保.env放在scripts/下。
5. 在 App Store Connect 创建新版本
脚本只能向已存在且可编辑的版本写入发版说明。请先在 ASC 网页端:
-
进入 App → App Store 标签
-
点击 ** + ** 创建新版本,填写版本号(需与
release_notes.yaml中的version一致或接近) -
确保版本状态为 Prepare for Submission(准备提交)或类似可编辑状态
日常使用流程
第一步:编辑发版说明
打开 scripts/release_notes.yaml,修改 version 和源语言内容。其余语言留空 "" 即可自动翻译。
**示例 A:以简体中文为源语言(默认) **
version: "2.1.0"
source_language: "zh-Hans"
notes:
zh-Hans: |
• 新增每日互动功能
• 优化 AI 响应速度
• 修复若干已知问题
en-US: ""
zh-Hant: ""
de-DE: ""
fr-FR: ""
es-ES: ""
pt-BR: ""
it: ""
tr: ""
示例 B:以英文为源语言
version: "1.10.41"
source_language: "en-US"
notes:
en-US: |
What's New:
Added Tai Chi courses.
Added landscape video playback support.
Fixed bugs and improved user experience.
zh-Hans: ""
zh-Hant: ""
de-DE: ""
fr-FR: ""
es-ES: ""
pt-BR: ""
it: ""
tr: ""
如需某语言使用人工精修文案,直接填写该语言字段,脚本会跳过翻译。
第二步:运行脚本
在项目根目录执行:
python3 scripts/upload_notes.py
第三步:检查输出
成功时终端会显示类似:
=======================================================
App Store Connect 多语言发版说明上传工具
=======================================================
① 读取配置
✓ 版本:1.10.41
✓ 源语言:en-US
✓ 目标语言数:9
② 自动翻译(Google 翻译)
✓ en-US (原文,跳过翻译)
→ zh-Hans 翻译中...
✓ zh-Hans 翻译完成
...
③ 连接 App Store Connect
✓ JWT Token 生成成功
✓ 找到版本:1.10.41 状态:PREPARE_FOR_SUBMISSION
④ 上传更新说明
✓ en-US 已更新
✓ zh-Hans 已更新
...
⑤ 翻译结果预览
✓ 预览文件已保存:scripts/release_notes_preview_1_10_41.txt
=======================================================
全部完成!成功上传 9 个语言
=======================================================
打开 scripts/release_notes_preview_1_10_41.txt 复核各语言翻译质量,必要时在 YAML 中手动覆盖后重新运行。
配置文件详解
release_notes.yaml 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| version | string | 版本号字符串,用于预览文件命名,建议与 ASC 版本一致 |
| source_language | string | 源语言 locale,翻译从此语言出发 |
| notes | object | 各语言发版说明,key 为 locale,value 为多行文本 |
****** **notes** 中每种语言的三种状态: **
-
源语言 — 必须填写,作为翻译来源
-
留空
**""** — 脚本自动翻译 -
已填写 — 视为人工定稿,跳过翻译
环境变量说明
| 变量 | 必填 | 说明 |
|------|------|------|
| ASC_KEY_ID | 是 | API 密钥 Key ID |
| ASC_ISSUER_ID | 是 | Issuer ID |
| ASC_PRIVATE_KEY | 是 | .p8 私钥文件路径 |
| ASC_APP_ID | 是 | 应用 Apple ID(数字) |
| OPENAI_API_KEY | 否 | OpenAI 密钥,翻译降级用 |
核心实现解析
1. App Store Connect JWT 鉴权
Apple API 使用 ES256 签名的 JWT,有效期 20 分钟:
payload = {
"iss": issuer_id,
"iat": int(time.time()),
"exp": int(time.time()) + 1200,
"aud": "appstoreconnect-v1",
}
token = jwt.encode(payload, private_key, algorithm="ES256", headers={"kid": key_id})
2. 版本查找策略
调用 GET /v1/apps/{app_id}/appStoreVersions 获取最近 10 个版本,客户端按版本号降序排序,选取第一个处于可编辑状态的版本:
-
PREPARE_FOR_SUBMISSION -
DEVELOPER_REJECTED/REJECTED/METADATA_REJECTED -
WAITING_FOR_REVIEW/READY_FOR_REVIEW
若最新版本已上架(如 READY_FOR_SALE),脚本会报错提示先创建新版本。
3. 本地化记录的创建与更新
GET /v1/appStoreVersions/{id}/appStoreVersionLocalizations → 获取已有记录
PATCH /v1/appStoreVersionLocalizations/{id} → 更新 whatsNew
POST /v1/appStoreVersionLocalizations → 新建记录
4. 翻译引擎选择
def translate(text, target_locale, source_locale="zh-Hans"):
try:
return translate_google(text, target_locale, source_locale)
except Exception:
return translate_openai(text, target_locale, source_locale)
Google 翻译通过 deep-translator 库调用,无需 API Key;OpenAI 使用 gpt-4o-mini,system prompt 要求保留 bullet 格式、输出简洁自然的 App Store 文案。
5. Locale 映射
YAML 中的 key 与 ASC API locale 通过 LOCALE_MAP 映射;Google 翻译使用独立的 GOOGLE_LANG_MAP(如 zh-Hans → zh-CN)。
完整源码
scripts/upload_notes.py
#!/usr/bin/env python3
"""
App Store Connect 多语言发版说明自动上传工具
=========================================
功能:
1. 读取 release_notes.yaml 中的中文更新说明
2. 自动翻译为所有目标语言(Google 翻译,免费无需 API Key)
3. 调用 App Store Connect API 批量上传
使用方法:
python scripts/upload_notes.py
依赖安装:
pip install -r scripts/requirements.txt
环境变量(在 .env 文件中配置):
ASC_KEY_ID - App Store Connect API Key ID
ASC_ISSUER_ID - App Store Connect Issuer ID
ASC_PRIVATE_KEY - .p8 私钥文件路径(如 scripts/asc_key.p8)
ASC_APP_ID - 应用的 Apple ID(数字)
OPENAI_API_KEY - OpenAI API Key(可选,Google 翻译失败时的备用引擎)
"""
import os
import sys
import time
import yaml
import jwt
import httpx
from datetime import datetime
from pathlib import Path
from dotenv import load_dotenv
from deep_translator import GoogleTranslator
# ── 加载 .env ──────────────────────────────────────────────────────────────────
SCRIPT_DIR = Path(__file__).parent
load_dotenv(SCRIPT_DIR / ".env")
# ── App Store Connect 语言代码映射 ─────────────────────────────────────────────
# 左侧:release_notes.yaml 中的 key;右侧:ASC API 实际接受的 locale
LOCALE_MAP = {
"en-US": "en-US",
"zh-Hans": "zh-Hans",
"zh-Hant": "zh-Hant",
"de-DE": "de-DE",
"fr-FR": "fr-FR",
"es-ES": "es-ES",
"pt-BR": "pt-BR",
"it": "it",
"tr": "tr",
}
# Google 翻译语言代码映射
GOOGLE_LANG_MAP = {
"en-US": "en",
"zh-Hans": "zh-CN",
"zh-Hant": "zh-TW",
"de-DE": "de",
"fr-FR": "fr",
"es-ES": "es",
"pt-BR": "pt",
"it": "it",
"tr": "tr",
}
# OpenAI 语言名称映射(备用)
OPENAI_LANG_MAP = {
"en-US": "American English",
"zh-Hans": "Simplified Chinese",
"zh-Hant": "Traditional Chinese",
"de-DE": "German",
"fr-FR": "French",
"es-ES": "Spanish",
"pt-BR": "Brazilian Portuguese",
"it": "Italian",
"tr": "Turkish",
}
# ── 颜色输出 ───────────────────────────────────────────────────────────────────
class C:
RESET = "\033[0m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
CYAN = "\033[96m"
BOLD = "\033[1m"
def info(msg): print(f" {C.CYAN}→{C.RESET} {msg}")
def ok(msg): print(f" {C.GREEN}✓{C.RESET} {msg}")
def warn(msg): print(f" {C.YELLOW}⚠{C.RESET} {msg}")
def err(msg): print(f" {C.RED}✗{C.RESET} {msg}")
def section(title): print(f"\n{C.BOLD}{title}{C.RESET}")
# ── App Store Connect JWT ──────────────────────────────────────────────────────
def generate_asc_token() -> str:
key_id = os.environ["ASC_KEY_ID"]
issuer_id = os.environ["ASC_ISSUER_ID"]
key_path = Path(os.environ["ASC_PRIVATE_KEY"]).expanduser()
if not key_path.exists():
raise FileNotFoundError(f"找不到 .p8 私钥文件:{key_path}")
private_key = key_path.read_text()
payload = {
"iss": issuer_id,
"iat": int(time.time()),
"exp": int(time.time()) + 1200, # 20 分钟有效期
"aud": "appstoreconnect-v1",
}
token = jwt.encode(
payload,
private_key,
algorithm="ES256",
headers={"kid": key_id},
)
return token
# ── 翻译:Google 翻译(免费,无需 API Key)──────────────────────────────────────
def translate_google(text: str, target_locale: str, source_locale: str) -> str:
source_lang = GOOGLE_LANG_MAP.get(source_locale, "zh-CN")
target_lang = GOOGLE_LANG_MAP.get(target_locale)
if not target_lang:
raise ValueError(f"不支持的语言:{target_locale}")
translator = GoogleTranslator(source=source_lang, target=target_lang)
return translator.translate(text)
# ── 翻译:OpenAI(可选备用)──────────────────────────────────────────────────────
def translate_openai(text: str, target_locale: str, source_locale: str = "zh-Hans") -> str:
api_key = os.environ.get("OPENAI_API_KEY", "")
if not api_key:
raise EnvironmentError("OPENAI_API_KEY 未设置")
target_lang = OPENAI_LANG_MAP.get(target_locale)
if not target_lang:
raise ValueError(f"不支持的语言:{target_locale}")
resp = httpx.post(
"https://api.openai.com/v1/chat/completions",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json={
"model": "gpt-4o-mini",
"messages": [
{
"role": "system",
"content": (
f"You are a professional app store localization translator. "
f"Translate the following App Store release notes to {target_lang}. "
f"Keep the bullet point format (•). "
f"Keep it concise and natural for app store audiences. "
f"Output only the translated text, nothing else."
),
},
{"role": "user", "content": text},
],
"temperature": 0.3,
},
timeout=30,
)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"].strip()
def translate(text: str, target_locale: str, source_locale: str = "zh-Hans") -> str:
"""优先使用免费 Google 翻译,失败时回退到 OpenAI"""
try:
return translate_google(text, target_locale, source_locale)
except Exception as e:
warn(f"Google 翻译失败,尝试 OpenAI:{e}")
return translate_openai(text, target_locale, source_locale)
# ── App Store Connect API ──────────────────────────────────────────────────────
ASC_BASE = "https://api.appstoreconnect.apple.com/v1"
def get_latest_version(app_id: str, token: str) -> dict:
"""获取最新的版本(不过滤状态,客户端筛选)"""
headers = {"Authorization": f"Bearer {token}"}
resp = httpx.get(
f"{ASC_BASE}/apps/{app_id}/appStoreVersions",
headers=headers,
params={
"limit": 10,
},
timeout=30,
)
resp.raise_for_status()
data = resp.json()["data"]
# API 不认 sort,客户端自己排序(按版本号降序)
data.sort(
key=lambda v: tuple(int(x) for x in v["attributes"]["versionString"].split(".")),
reverse=True,
)
if not data:
raise RuntimeError(
"未找到任何版本。请先在 App Store Connect 创建新版本。"
)
# 找到第一个可编辑的版本(pre-release 状态)
for version in data:
state = version["attributes"]["appStoreState"]
if state in (
"PREPARE_FOR_SUBMISSION",
"DEVELOPER_REJECTED",
"REJECTED",
"METADATA_REJECTED",
"WAITING_FOR_REVIEW",
"READY_FOR_REVIEW",
):
return version
# 如果没有可编辑版本,返回最新版本(允许尝试)
latest = data[0]
latest_state = latest["attributes"]["appStoreState"]
raise RuntimeError(
f"最新版本状态为 {latest_state},不可编辑。"
"请先在 App Store Connect 创建新版本(状态应为 Prepare for Submission)。"
)
def get_version_localizations(version_id: str, token: str) -> list:
"""获取版本的所有语言本地化记录"""
resp = httpx.get(
f"{ASC_BASE}/appStoreVersions/{version_id}/appStoreVersionLocalizations",
headers={"Authorization": f"Bearer {token}"},
timeout=30,
)
resp.raise_for_status()
return resp.json()["data"]
def create_localization(version_id: str, locale: str, notes: str, token: str) -> dict:
"""新建一个语言本地化记录"""
resp = httpx.post(
f"{ASC_BASE}/appStoreVersionLocalizations",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={
"data": {
"type": "appStoreVersionLocalizations",
"attributes": {
"locale": locale,
"whatsNew": notes,
},
"relationships": {
"appStoreVersion": {
"data": {"type": "appStoreVersions", "id": version_id}
}
},
}
},
timeout=30,
)
resp.raise_for_status()
return resp.json()["data"]
def update_localization(loc_id: str, notes: str, token: str) -> dict:
"""更新已有的语言本地化记录"""
resp = httpx.patch(
f"{ASC_BASE}/appStoreVersionLocalizations/{loc_id}",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={
"data": {
"type": "appStoreVersionLocalizations",
"id": loc_id,
"attributes": {"whatsNew": notes},
}
},
timeout=30,
)
resp.raise_for_status()
return resp.json()["data"]
# ── 主流程 ─────────────────────────────────────────────────────────────────────
def main():
print(f"\n{C.BOLD}{'=' * 55}{C.RESET}")
print(f"{C.BOLD} App Store Connect 多语言发版说明上传工具{C.RESET}")
print(f"{C.BOLD}{'=' * 55}{C.RESET}")
# 1. 读取配置
section("① 读取配置")
notes_file = SCRIPT_DIR / "release_notes.yaml"
if not notes_file.exists():
err(f"找不到配置文件:{notes_file}")
sys.exit(1)
with open(notes_file, encoding="utf-8") as f:
config = yaml.safe_load(f)
version_str = config.get("version", "Unknown")
source_locale = config.get("source_language", "zh-Hans")
notes_map = config.get("notes", {})
source_text = notes_map.get(source_locale, "").strip()
if not source_text:
err(f"源语言 ({source_locale}) 内容为空,请先填写更新说明")
sys.exit(1)
ok(f"版本:{version_str}")
ok(f"源语言:{source_locale}")
ok(f"目标语言数:{len(notes_map)}")
# 2. 自动翻译空白语言
section("② 自动翻译(Google 翻译)")
if not os.environ.get("OPENAI_API_KEY"):
info("未配置 OPENAI_API_KEY,仅使用 Google 翻译(免费)")
translated_notes = {}
for locale, text in notes_map.items():
if locale == source_locale:
translated_notes[locale] = source_text
ok(f"{locale} (原文,跳过翻译)")
continue
if text.strip():
translated_notes[locale] = text.strip()
ok(f"{locale} (已手动填写,跳过翻译)")
continue
info(f"{locale} 翻译中...")
try:
translated = translate(source_text, locale, source_locale)
translated_notes[locale] = translated
ok(f"{locale} 翻译完成")
except Exception as e:
warn(f"{locale} 翻译失败:{e},跳过此语言")
# 3. 生成 ASC Token
section("③ 连接 App Store Connect")
try:
token = generate_asc_token()
ok("JWT Token 生成成功")
except KeyError as e:
err(f"缺少环境变量:{e},请检查 .env 文件")
sys.exit(1)
except Exception as e:
err(f"Token 生成失败:{e}")
sys.exit(1)
# 4. 查找版本
app_id = os.environ.get("ASC_APP_ID", "")
if not app_id:
err("缺少环境变量:ASC_APP_ID")
sys.exit(1)
try:
version_obj = get_latest_version(app_id, token)
version_id = version_obj["id"]
version_num = version_obj["attributes"]["versionString"]
state = version_obj["attributes"]["appStoreState"]
ok(f"找到版本:{version_num} 状态:{state}")
except Exception as e:
err(f"获取版本失败:{e}")
sys.exit(1)
# 5. 获取已有本地化记录
try:
existing_locs = get_version_localizations(version_id, token)
existing_map = {loc["attributes"]["locale"]: loc["id"] for loc in existing_locs}
ok(f"已有本地化记录:{len(existing_map)} 个")
except Exception as e:
err(f"获取本地化记录失败:{e}")
sys.exit(1)
# 6. 批量上传
section("④ 上传更新说明")
success_count = 0
fail_count = 0
for locale, notes in translated_notes.items():
asc_locale = LOCALE_MAP.get(locale, locale)
try:
if asc_locale in existing_map:
update_localization(existing_map[asc_locale], notes, token)
ok(f"{asc_locale} 已更新")
else:
create_localization(version_id, asc_locale, notes, token)
ok(f"{asc_locale} 已创建")
success_count += 1
except Exception as e:
warn(f"{asc_locale} 上传失败:{e}")
fail_count += 1
# 7. 输出翻译结果预览(供人工复核)
section("⑤ 翻译结果预览")
preview_file = SCRIPT_DIR / f"release_notes_preview_{version_str.replace('.', '_')}.txt"
try:
with open(preview_file, "w", encoding="utf-8") as f:
f.write(f"版本 {version_str} 发版说明预览\n")
f.write(f"生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write("=" * 55 + "\n\n")
for locale, notes in translated_notes.items():
f.write(f"[{locale}]\n{notes}\n\n")
ok(f"预览文件已保存:scripts/{preview_file.name}")
except (OSError, PermissionError):
warn(f"无法写入预览文件,跳过")
# 直接在终端打印翻译结果
print()
for locale, notes in translated_notes.items():
print(f" [{locale}]\n{notes}\n")
# 8. 最终汇报
print(f"\n{C.BOLD}{'=' * 55}{C.RESET}")
if fail_count == 0:
print(f"{C.GREEN}{C.BOLD} 全部完成!成功上传 {success_count} 个语言{C.RESET}")
else:
print(f"{C.YELLOW}{C.BOLD} 完成(成功 {success_count},失败 {fail_count}){C.RESET}")
print(f"{C.BOLD}{'=' * 55}{C.RESET}\n")
if __name__ == "__main__":
main()
scripts/requirements.txt
PyJWT>=2.8.0
httpx>=0.27.0
pyyaml>=6.0.1
python-dotenv>=1.0.0
cryptography>=41.0.0
deep-translator>=1.11.4
scripts/.env.example
# =====================================================
# App Store Connect 多语言上传工具 - 环境变量配置
# =====================================================
# 将此文件复制为 scripts/.env,填写真实值后保存
# .env 文件已在 .gitignore 中,不会上传到 git
# =====================================================
# ── App Store Connect API(必填)─────────────────────
# 来源:App Store Connect → 用户与访问 → 密钥 → App Store Connect API
# Key ID(示例:ABCD1234EF)
ASC_KEY_ID=your_key_id_here
# Issuer ID(示例:57246542-96fe-1a63-e053-0824d011072a)
ASC_ISSUER_ID=your_issuer_id_here
# .p8 私钥文件路径(下载后放到 scripts/ 目录)
ASC_PRIVATE_KEY=scripts/AuthKey_XXXXXXXXXX.p8
# 应用的 Apple ID(在 App Store Connect → 应用信息 → Apple ID 下方找到,纯数字)
ASC_APP_ID=1234567890
# ── 翻译引擎 ─────────────────────────────────────────
# 默认使用 Google 翻译(免费,无需 API Key,无需信用卡)
# 如果 Google 翻译失败,脚本会自动回退到 OpenAI
# OpenAI API Key(可选备用,不填也能用)
# 注册:https://platform.openai.com/api-keys
# OPENAI_API_KEY=your_openai_api_key_here
scripts/release_notes.yaml 模板
# App Store 发版说明配置
# 使用方法:
# 1. 修改 version 和源语言下的内容
# 2. 其他语言留空(""),脚本会自动翻译
# 3. 也可以手动填写某个语言,填写后脚本跳过该语言的翻译
# 4. 运行:python3 scripts/upload_notes.py
version: "2.1.0"
# 源语言(翻译的来源)
source_language: "zh-Hans"
# 各语言更新说明
# 留空 ("") 表示由脚本自动翻译填充
# 手动填写则跳过翻译直接使用
notes:
zh-Hans: |
• 新增某某功能
• 优化性能与体验
• 修复若干已知问题
en-US: ""
zh-Hant: ""
de-DE: ""
fr-FR: ""
es-ES: ""
pt-BR: ""
it: ""
tr: ""
常见问题与排查
「未找到可编辑的版本」或「最新版本状态为 READY_FOR_SALE,不可编辑」
原因:当前 App 没有处于草稿/待提交状态的版本。
解决:在 App Store Connect 网页端手动创建新版本,状态变为 Prepare for Submission 后再运行脚本。
「找不到 .p8 私钥文件」
原因:ASC_PRIVATE_KEY 路径不正确,或文件未放到指定位置。
解决:确认 .p8 文件存在,路径相对于项目根目录(如 scripts/AuthKey_ABC123.p8)。
「缺少环境变量」
原因:.env 未创建或变量名拼写错误。
解决:确认 scripts/.env 存在且四个 ASC 变量均已填写。
Google 翻译失败
原因:网络问题或 Google 服务暂时不可用。
解决:
-
检查网络(国内可能需要代理)
-
配置
OPENAI_API_KEY作为备用 -
或在 YAML 中手动填写翻译内容
某语言上传失败
原因:该 locale 未在 App Store Connect 的 App 信息中启用。
解决:在 ASC → App 信息 → 本地化语言中添加对应语言,或从 release_notes.yaml 中移除该语言。
pip: command not found / python: command not found
macOS 请使用:
brew install python3
pip3 install -r scripts/requirements.txt
python3 scripts/upload_notes.py
JWT 401 Unauthorized
原因:Key ID、Issuer ID 与 .p8 文件不匹配,或密钥权限不足。
解决:核对三项配置是否来自同一把密钥,权限需包含 App 管理。
安全建议
-
永远不要将
.p8私钥、scripts/.env提交到 Git -
建议在
.gitignore中加入:
```gitignore
scripts/.env
scripts/*.p8
scripts/release_notes_preview_*.txt
```
-
API 密钥权限遵循最小原则,App 管理即可,无需 Admin
-
若密钥泄露,立即在 App Store Connect 中吊销并重新生成
-
OpenAI API Key 同样不要硬编码在脚本中
扩展与定制
增加新语言
-
在
release_notes.yaml的notes下添加新 locale -
在
upload_notes.py的LOCALE_MAP、GOOGLE_LANG_MAP、OPENAI_LANG_MAP中补充映射 -
在 App Store Connect 中启用该语言
接入 CI/CD
可在 Fastlane 或 GitHub Actions 中于发版前自动执行:
# .github/workflows/upload-release-notes.yml 示例
- name: Upload App Store release notes
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_PRIVATE_KEY: scripts/AuthKey.p8
ASC_APP_ID: ${{ secrets.ASC_APP_ID }}
run: |
pip3 install -r scripts/requirements.txt
python3 scripts/upload_notes.py
将 .p8 内容存入 GitHub Secrets,在 workflow 中写入临时文件即可。
替换翻译引擎
修改 translate() 函数,可接入 DeepL、Azure Translator、自建 LLM 等,保持输入输出接口不变即可。
仅预览不上传
将 main() 中第 ⑥ 步「批量上传」注释掉,或增加 --dry-run 参数(自行扩展),即可只生成预览文件、不调用 API。
总结
这套工具把 iOS 发版中最繁琐的「多语言 What's New 填写」变成了配置驱动的一次性操作:
-
开发侧:只维护一份源语言文案
-
翻译侧:Google 免费翻译 + OpenAI 可选降级
-
发布侧:App Store Connect API 自动同步
整个脚本约 430 行 Python,无框架依赖,易于复制到其他 iOS 项目。