locale-sync-cli 1.0.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.
package/README.md ADDED
@@ -0,0 +1,800 @@
1
+ # 🌐 locale-sync-cli
2
+
3
+ > 企业级国际化 (i18n) 同步命令行工具 — 本地语言包 ↔ Google Sheets 双向同步
4
+
5
+ [![Node Version](https://img.shields.io/badge/node-%3E%3D16.0.0-brightgreen)](https://nodejs.org)
6
+ [![License](https://img.shields.io/badge/license-ISC-blue.svg)](./package.json)
7
+
8
+ ---
9
+
10
+ ## 📋 目录
11
+
12
+ - [核心功能](#-核心功能)
13
+ - [架构概览](#-架构概览)
14
+ - [安装与初始化](#-安装与初始化)
15
+ - [命令详解](#-命令详解)
16
+ - [init — 项目初始化](#init--项目初始化)
17
+ - [push — 本地 → Google Sheets](#push--本地--google-sheets)
18
+ - [pull — Google Sheets → 本地](#pull--google-sheets--本地)
19
+ - [set-credentials — 凭证管理](#set-credentials--凭证管理)
20
+ - [ignore — 忽略规则管理](#ignore--忽略规则管理)
21
+ - [config — 查看当前配置](#config--查看当前配置)
22
+ - [Google Sheets 双工作表架构](#-google-sheets-双工作表架构)
23
+ - [颜色语义系统](#-颜色语义系统)
24
+ - [智能同步策略](#-智能同步策略)
25
+ - [Push 详细流程](#push-详细流程)
26
+ - [Pull 详细流程](#pull-详细流程)
27
+ - [冲突检测与解决](#-冲突检测与解决)
28
+ - [文件扫描与解析](#-文件扫描与解析)
29
+ - [多 Locale 文件格式支持](#-多-locale-文件格式支持)
30
+ - [占位符一致性校验](#-占位符一致性校验)
31
+ - [安全防护](#-安全防护)
32
+ - [配置参考](#-配置参考)
33
+ - [凭证管理](#-凭证管理)
34
+ - [CI/CD 集成](#-cicd-集成)
35
+ - [本地语言包目录结构](#-本地语言包目录结构)
36
+ - [技术栈](#-技术栈)
37
+ - [常见问题](#-常见问题)
38
+
39
+ ---
40
+
41
+ ## 🎯 核心功能
42
+
43
+ | 功能 | 描述 |
44
+ |------|------|
45
+ | **双向同步** | 本地语言包 ⇄ Google Sheets,支持 Push(上传)和 Pull(下载) |
46
+ | **智能匹配** | 基于英文锚点 (English Anchor) 的精准 key 匹配,兼容多 key 同值场景 |
47
+ | **冲突检测** | 多人协作时自动检测翻译冲突,以黄色标记提示人工确认 |
48
+ | **颜色语义** | Google Sheets 中通过单元格背景色区分翻译来源与状态 |
49
+ | **双表架构** | Common 表 + 项目表,实现共享翻译与项目级覆盖的灵活管理 |
50
+ | **多格式支持** | 支持 `.js` (AST)、`.json` (Comment JSON)、`.yaml/.yml` 三种文件格式 |
51
+ | **Multi-Locale** | 自动识别单文件多 locale 格式(如 `{ "en-US": {...}, "ar-SA": {...} }`) |
52
+ | **AST 保真** | JS 文件通过 AST 解析 + recast 回写,保留原始代码风格、注释、格式 |
53
+ | **占位符检查** | Pull 时自动校验 `${...}`、`{...}`、`%s` 等占位符一致性,防止翻译遗漏 |
54
+ | **安全拦截** | 自动拦截 Google Sheets 公式注入(`=` 开头)及非法控制字符 |
55
+ | **防环路机制** | 可配置的 `antiLoopWindow` 防止 Push/Pull 在短时间内形成循环更新 |
56
+ | **Dry-Run** | 默认预览模式,显示变更计划后确认执行,安全可控 |
57
+ | **CI/CD** | 支持 `--yes` 参数跳过交互确认,适配自动化流水线 |
58
+ | **凭证安全** | Base64 编码存入 Shell Profile,无需在项目中暴露敏感文件 |
59
+
60
+ ---
61
+
62
+ ## 🏗 架构概览
63
+
64
+ ```
65
+ ┌──────────────────────────────────────────────────────────────┐
66
+ │ CLI Layer (bin/) │
67
+ │ commander + inquirer 交互 │
68
+ ├──────────────────────────────────────────────────────────────┤
69
+ │ Commands (lib/cli/) │
70
+ │ init / push / pull / set-credentials / ignore │
71
+ ├──────────────────────┬───────────────────────────────────────┤
72
+ │ Push Engine │ Pull Engine │
73
+ │ lib/core/engines/ │ lib/core/engines/ │
74
+ │ pushEngine.js │ pullEngine.js │
75
+ ├──────────────────────┴───────────────────────────────────────┤
76
+ │ Core Utils │
77
+ │ stringNormalizer.js │ placeholderChecker.js │
78
+ ├──────────────────────┬───────────────────────────────────────┤
79
+ │ IO: Google Sheets │ IO: Local FS │
80
+ │ googleSheets.js │ scanner.js / astUpdater.js │
81
+ ├──────────────────────┴───────────────────────────────────────┤
82
+ │ Config Manager │
83
+ │ lib/config/manager.js │
84
+ └──────────────────────────────────────────────────────────────┘
85
+ ```
86
+
87
+ ### 模块说明
88
+
89
+ | 模块 | 文件 | 职责 |
90
+ |------|------|------|
91
+ | **CLI 入口** | `bin/locale-sync.js` | Commander 命令行定义、Web API polyfill |
92
+ | **项目初始化** | `lib/cli/commands/init.js` | 交互式创建 `.locale-sync.json` 配置,配置凭证 |
93
+ | **Push 命令** | `lib/cli/commands/push.js` | 扫描本地 → 读取 Sheets → 生成推送计划 → 执行 |
94
+ | **Pull 命令** | `lib/cli/commands/pull.js` | 读取 Sheets → 扫描本地 → 生成拉取计划 → AST 回写 |
95
+ | **凭证管理** | `lib/cli/commands/set-credentials.js` | 独立更新 Google 凭证 |
96
+ | **忽略规则** | `lib/cli/commands/ignore.js` | 管理文件/文件夹忽略规则 |
97
+ | **Push 引擎** | `lib/core/engines/pushEngine.js` | 核心推送逻辑:增量更新、冲突检测、新增行 |
98
+ | **Pull 引擎** | `lib/core/engines/pullEngine.js` | 核心拉取逻辑:优先级链解析、变化检测 |
99
+ | **Google Sheets IO** | `lib/io/googleSheets.js` | Google Sheets API v4 封装:认证、读写、格式化、批量操作 |
100
+ | **文件扫描器** | `lib/io/localFs/scanner.js` | 递归扫描 locale 目录,解析 JS/JSON/YAML 文件提取 key-value |
101
+ | **AST 更新器** | `lib/io/localFs/astUpdater.js` | AST 解析回写,支持 ObjectProperty、数组、TemplateLiteral |
102
+ | **字符串规范化** | `lib/core/utils/stringNormalizer.js` | 英文锚点规范化、locale 码格式化 |
103
+ | **占位符检查** | `lib/core/utils/placeholderChecker.js` | 正则提取 + 一致性比较(`{...}`, `${...}`, `%s`, XML 标签等) |
104
+ | **配置管理** | `lib/config/manager.js` | 加载/合并配置文件,CI 环境检测 |
105
+ | **日志工具** | `lib/cli/utils/logger.js` | 统一风格的彩色日志输出 (info/success/warn/error) |
106
+ | **默认配置** | `config/default.json` | 默认配置值、targetLocales、formats 策略 |
107
+
108
+ ---
109
+
110
+ ## 📦 安装与初始化
111
+
112
+ ### 环境要求
113
+
114
+ - **Node.js** >= 16.0.0
115
+ - **Google Cloud 项目** + 启用了 Google Sheets API 的 Service Account
116
+ - 目标 Google Sheet 已共享给 Service Account 的邮箱(Editor 权限)
117
+
118
+ ### 安装
119
+
120
+ ```bash
121
+ npm install
122
+ ```
123
+
124
+ ### 初始化
125
+
126
+ ```bash
127
+ npx locale-sync init
128
+ ```
129
+
130
+ 交互式引导:
131
+
132
+ | 提示 | 说明 | 示例 |
133
+ |------|------|------|
134
+ | `Google Sheet ID` | 从 Sheets URL 中提取 | `1NbUr7mtbRMZXtrOmyvcjiaMeQzzgWmYhNUke7uUSe54` |
135
+ | `Locales Directory` | 本地语言包根目录 | `locales/lang` |
136
+ | `Project Name` | 项目名称(用作项目表名) | `my-app` |
137
+ | `Ignore patterns` | 跳过扫描的文件/文件夹(逗号分隔) | `node_modules,*.d.ts` |
138
+ | `Credentials source` | Service Account JSON 路径或 URL | `./service-account.json` |
139
+
140
+ 初始化后生成 `.locale-sync.json` 配置文件,并将凭证 Base64 编码写入 Shell Profile(`~/.zshrc` / `~/.bash_profile`)。
141
+
142
+ ---
143
+
144
+ ## 📟 命令详解
145
+
146
+ ### init — 项目初始化
147
+
148
+ ```bash
149
+ npx locale-sync init
150
+ ```
151
+
152
+ 创建 `.locale-sync.json` 配置,设置 Google 凭证。重复运行可重新配置。
153
+
154
+ ---
155
+
156
+ ### push — 本地 → Google Sheets
157
+
158
+ ```bash
159
+ npx locale-sync push [options]
160
+ ```
161
+
162
+ | 选项 | 说明 |
163
+ |------|------|
164
+ | `-y, --yes` | 跳过确认提示,直接执行同步 |
165
+ | `-v, --verbose` | 输出详细错误堆栈 |
166
+
167
+ **执行流程:**
168
+
169
+ 1. ✅ 加载配置 + 认证 Google Sheets API
170
+ 2. ✅ 扫描本地 locale 目录(.js / .json / .yaml)
171
+ 3. ✅ 读取 Google Sheets 的 Common 表和项目表
172
+ 4. ✅ 输出 **Scan Summary**(每个 locale 的 key 数量、空值数量)
173
+ 5. ✅ 生成推送计划并展示 **Push Preview**(新增行数、更新行数)
174
+ 6. ✅ 用户确认后执行批量写入
175
+
176
+ ---
177
+
178
+ ### pull — Google Sheets → 本地
179
+
180
+ ```bash
181
+ npx locale-sync pull [options]
182
+ ```
183
+
184
+ | 选项 | 说明 |
185
+ |------|------|
186
+ | `-y, --yes` | 跳过确认提示 |
187
+ | `-v, --verbose` | 输出详细错误堆栈 |
188
+
189
+ **执行流程:**
190
+
191
+ 1. ✅ 读取 Google Sheets 的 Common 表和项目表
192
+ 2. ✅ 扫描本地 locale 目录
193
+ 3. ✅ 按优先级链生成 Pull Plan
194
+ 4. ✅ 展示每个变更(locale/key: old → new)+ 总计 **key 变更数**
195
+ 5. ✅ 用户确认后通过 AST 回写到本地文件,结果报告实际更新的 key 数量
196
+
197
+ ---
198
+
199
+ ### set-credentials — 凭证管理
200
+
201
+ ```bash
202
+ npx locale-sync set-credentials
203
+ ```
204
+
205
+ 独立更新 Google Service Account 凭证,支持本地文件路径或远程 URL。
206
+
207
+ ---
208
+
209
+ ### ignore — 忽略规则管理
210
+
211
+ ```bash
212
+ # 添加忽略规则
213
+ npx locale-sync ignore node_modules
214
+ npx locale-sync ignore *.d.ts index.js
215
+
216
+ # 查看当前规则
217
+ npx locale-sync ignore --list
218
+
219
+ # 移除规则
220
+ npx locale-sync ignore --remove node_modules
221
+ ```
222
+
223
+ 忽略规则会自动转换为 glob 模式:
224
+ - `node_modules` → `**/node_modules/**`, `**/node_modules`
225
+ - `*.d.ts` → `**/*.d.ts`
226
+ - `index.js` → `**/index.js`
227
+ - 含 `/` 的路径(如 `locales/i18n.js`)视为**项目根目录**的相对路径,自动转换为相对于扫描目录(`locales`)的路径后再应用 glob 规则
228
+
229
+ ---
230
+
231
+ ### config — 查看当前配置
232
+
233
+ ```bash
234
+ npx locale-sync
235
+ # 或直接运行无参数命令
236
+ ```
237
+
238
+ 展示合并后的完整配置(Default + Local + Overrides)。
239
+
240
+ ---
241
+
242
+ ## 📊 Google Sheets 双工作表架构
243
+
244
+ ```
245
+ ┌─────────────────────────────────────────────────────────┐
246
+ │ Google Sheet │
247
+ ├─────────────────────┬───────────────────────────────────┤
248
+ │ Common 表 (共享) │ Project 表 (项目专用) │
249
+ ├─────────────────────┼───────────────────────────────────┤
250
+ │ en-US │ ar-SA │ ... │ key │ filePath │ en-US │ ar-SA │...│
251
+ ├─────────────────────┼───────────────────────────────────┤
252
+ │ Hello │ مرحبا │ ... │ a.b │ path/to/ │ │ مرحبا │ │
253
+ │ World │ 世界 │ ... │ x.y │ path/to/ │ Hello │ │ │
254
+ └─────────────────────┴───────────────────────────────────┘
255
+ ```
256
+
257
+ ### Common 表(共享翻译层)
258
+
259
+ - **列结构**: `en-US | ar-SA | pt-BR | ... | _lastSyncBy | _lastSyncAt`
260
+ - **锚点**: 以英文(en-US / en / enus)列作为匹配锚点
261
+ - **职责**: 存放跨项目共享的通用翻译,Push 时优先写入此处
262
+ - **特点**: 无 `key`、`filePath` 列,纯翻译内容驱动
263
+
264
+ ### Project 表(项目专用层)
265
+
266
+ - **列结构**: `key | filePath | en-US | ar-SA | pt-BR | ... | _lastSyncBy | _lastSyncAt`
267
+ - **主键**: `key + filePath` 组合唯一
268
+ - **职责**:
269
+ - 项目级翻译覆盖(优先级高于 Common)
270
+ - 存放冲突翻译(黄色行)
271
+ - 存放项目独有的 key
272
+ - **灰色行**: 整行标记为已废弃/忽略
273
+
274
+ ### 元数据列
275
+
276
+ | 列名 | 说明 |
277
+ |------|------|
278
+ | `_lastSyncBy` | 最后同步方向:`push` 或 `pull` |
279
+ | `_lastSyncAt` | 最后同步时间戳 (ISO 8601) |
280
+
281
+ ---
282
+
283
+ ## 🎨 颜色语义系统
284
+
285
+ | 颜色 | 含义 | 触发条件 |
286
+ |------|------|----------|
287
+ | 🟢 **绿色** | 自动同步内容 | Push 写入的新值或填充的空格 |
288
+ | 🟡 **黄色** | 冲突待确认 | 同一 locale 存在多个不同翻译来源 |
289
+ | ⬜ **无色/白色** | 人工编辑锁定 | 译者在 Sheets 中手动输入的值,Push 不会覆盖 |
290
+ | 🔴 **红色** | 错误标记 | (预留) |
291
+ | 🩶 **灰色** | 整行废弃 | 整行标记为忽略,Push/Pull 均跳过 |
292
+ | 🔵 **青色(Header)** | 表头标识 | 首行自动格式化为青色背景 + 粗体 |
293
+
294
+ ### Push 写入规则
295
+
296
+ ```
297
+ 单元格状态 │ Push 行为
298
+ ──────────────────┼────────────
299
+ 空 + 无色 │ ✅ 写入(绿色)
300
+ 有值 + 绿色 │ ✅ 更新(绿色)
301
+ 有值 + 无色 │ ❌ 跳过(视为人工编辑锁定)
302
+ 有值 + 黄色 │ ❌ 跳过(保留冲突状态)
303
+ 空 + 绿色/黄色 │ ❌ 跳过(视为主动清空)
304
+ ```
305
+
306
+ ---
307
+
308
+ ## 🧠 智能同步策略
309
+
310
+ ### Push 详细流程
311
+
312
+ ```
313
+ 本地文件扫描
314
+
315
+
316
+ ┌─────────────────────────────────────┐
317
+ │ 1. 构建英文锚点 (normalizeForAnchor) │
318
+ │ "Hello World" → "Hello World" │
319
+ └─────────────────────────────────────┘
320
+
321
+
322
+ ┌─────────────────────────────────────┐
323
+ │ 2. 在 Common 表按锚点匹配 │
324
+ │ 找到?→ 更新 Common 行 │
325
+ │ 未找到?→ 新增 Common 行 │
326
+ └─────────────────────────────────────┘
327
+
328
+
329
+ ┌─────────────────────────────────────┐
330
+ │ 3. 在 Project 表按 key+filePath 匹配 │
331
+ │ 找到?→ 更新 Project 行 │
332
+ │ 冲突?→ 追加黄色冲突行 │
333
+ └─────────────────────────────────────┘
334
+
335
+
336
+ ┌─────────────────────────────────────┐
337
+ │ 4. 同一批次内冲突检测 │
338
+ │ 同锚点多次出现 → 首个写入 Common │
339
+ │ 后续差异 → 追加 Project 冲突行 │
340
+ └─────────────────────────────────────┘
341
+
342
+
343
+ ┌─────────────────────────────────────┐
344
+ │ 5. 批量写入 Google Sheets │
345
+ │ • appendCells (新增行) │
346
+ │ • updateCells (更新单元格) │
347
+ │ 每批最多 400 请求 + 3 次重试 │
348
+ └─────────────────────────────────────┘
349
+ ```
350
+
351
+ ### Pull 详细流程
352
+
353
+ ```
354
+ Google Sheets 数据
355
+
356
+
357
+ ┌───────────────────────────────────────┐
358
+ │ 优先级链(按列独立判定) │
359
+ │ │
360
+ │ 1. 项目表 有值 (不论颜色) ← 最高 │
361
+ │ 2. 项目表 清空 (空+有色) │
362
+ │ 3. Common 有值 (无色/绿色) │
363
+ │ 4. Common 有值 (黄色) ← 警告 │
364
+ │ 5. Common 清空 (空+有色) │
365
+ │ 6. 保留本地值 ← 最低 │
366
+ └───────────────────────────────────────┘
367
+
368
+
369
+ ┌───────────────────────────────────────┐
370
+ │ 第一轮:遍历 Project 表行 │
371
+ │ • 跳过灰色行 │
372
+ │ • 跳过 antiLoopWindow 内的行 │
373
+ │ • en 锚点 → 备用锚点(__noen__/__key__)│
374
+ │ • 按优先级链取最终值 │
375
+ │ • 若与本地不同 → 加入 Pull Plan │
376
+ └───────────────────────────────────────┘
377
+
378
+
379
+ ┌───────────────────────────────────────┐
380
+ │ 第二轮:遍历 Common 表未覆盖的行 │
381
+ │ • 跳过已在 Project 表处理过的锚点 │
382
+ │ • 同样支持备用锚点 │
383
+ │ • 相同优先级逻辑处理 │
384
+ └───────────────────────────────────────┘
385
+
386
+
387
+ ┌───────────────────────────────────────┐
388
+ │ AST 回写到本地文件 │
389
+ │ .js → recast AST 保真写入 │
390
+ │ .json → comment-json 保留注释 │
391
+ │ .yaml → js-yaml dump │
392
+ └───────────────────────────────────────┘
393
+ ```
394
+
395
+ ### 英文锚点策略
396
+
397
+ Push 和 Pull 都使用 **英文值 (en)** 作为跨表匹配的锚点:
398
+
399
+ ```js
400
+ // 规范化流程
401
+ " Hello World " → normalizeForAnchor → "Hello World"
402
+ ```
403
+
404
+ - 合并连续空白为单空格
405
+ - 移除零宽字符(`\u200B-\u200D`)和不间断空格(`\u00A0`)
406
+ - 保留大小写(区分大小写匹配)
407
+
408
+ **备用锚点**(en 为空时,Push/Pull 均支持):
409
+ 1. `__noen__` 前缀 + 按表头 locale 列顺序取第一个非空翻译值
410
+ 2. `__key__` 前缀 + `key|filePath` 组合兜底
411
+
412
+ ### Locale 码多级回退匹配
413
+
414
+ Pull 时从 Sheet 行读取值与颜色,采用三级回退兼容本地短码与 Sheet 长格式:
415
+
416
+ ```
417
+ localeKey = "ar"(来自本地目录结构)
418
+
419
+ ├─ 1. 精确匹配: row["ar"] → ar-SA 表头归一化为 arsa,不命中
420
+ ├─ 2. 基础码匹配: row["ar"] → 若 ar 存在于 Sheet 行
421
+ └─ 3. 前缀扫描: row["arsa"] → 找到以 "ar" 开头且更长的 key
422
+ ```
423
+
424
+ 这确保以下场景无缝工作:
425
+
426
+ | 本地目录 | Sheet 表头 | 匹配结果 |
427
+ |---------|-----------|---------|
428
+ | `locales/lang/ar/` | `ar-SA` | ✅ `arsa` |
429
+ | `locales/lang/en/` | `en-US` | ✅ `enus` |
430
+ | `locales/lang/pt/` | `pt-BR` | ✅ `ptbr` |
431
+ | `locales/lang/arae/` | `ar-SA` | ✅ `arsa` |
432
+
433
+ ---
434
+
435
+ ## ⚔️ 冲突检测与解决
436
+
437
+ ### 冲突场景
438
+
439
+ ```
440
+ 场景 1: 同一英文锚点,不同 locale 值
441
+ File A: en="Hello" → ar="مرحبا"
442
+ File B: en="Hello" → ar="أهلا"
443
+ → Common 写入 "مرحبا",Project 追加黄色行 "أهلا"
444
+
445
+ 场景 2: 同一批次 Push 内重复锚点
446
+ 第 1 个 en="Welcome" → Common 写入(绿色)
447
+ 第 2 个 en="Welcome" → 差异 locale 追加冲突行(黄色)
448
+
449
+ 场景 3: Pull 时 Common 与 Project 不一致
450
+ Project ar = "مرحبا" (绿色)
451
+ Common ar = "أهلا" (黄色)
452
+ → 优先取 Project 值,若为黄色则附加警告
453
+ ```
454
+
455
+ ### 黄色冲突行的处理
456
+
457
+ - **Push 阶段**: 黄色冲突行保留不改,供人工审核
458
+ - **Pull 阶段**: 黄色值会被拉取,但附带 `[待人工确认]` 警告
459
+ - **人工解决**: 译者在 Sheets 中确认正确翻译后,可清除颜色标记锁定
460
+
461
+ ---
462
+
463
+ ## 📝 文件扫描与解析
464
+
465
+ ### 扫描器 (scanner.js)
466
+
467
+ ```
468
+ 扫描目录
469
+
470
+ ├─ .js 文件 → @babel/parser + recast (AST)
471
+ │ ├─ ObjectProperty: StringLiteral → 提取 key-value
472
+ │ ├─ ObjectProperty: TemplateLiteral → 序列化 ${...}
473
+ │ ├─ Array StringLiteral → 提取
474
+ │ └─ Array TemplateLiteral → 序列化
475
+
476
+ ├─ .json 文件 → comment-json 解析(保留注释)
477
+ │ └─ 递归 flatten 嵌套对象
478
+
479
+ └─ .yaml/.yml 文件 → js-yaml 解析
480
+ └─ 递归 flatten 嵌套对象
481
+ ```
482
+
483
+ ### AST 更新器 (astUpdater.js)
484
+
485
+ ```
486
+ Pull 回写策略
487
+
488
+ .js 文件:
489
+ ├─ StringLiteral → 直接替换 value
490
+ ├─ TemplateLiteral → 按 ${expr} 分割重组 quasis
491
+ └─ recast.print() 保真输出(保留代码风格)
492
+
493
+ .json 文件:
494
+ └─ comment-json 深度赋值 + stringify(保留注释)
495
+
496
+ .yaml 文件:
497
+ └─ js-yaml dump(保持结构)
498
+ ```
499
+
500
+ ### Locale 检测策略
501
+
502
+ ```
503
+ 文件路径解析优先级:
504
+ 1. 文件名是合法 locale 代码 → 使用文件名
505
+ 例: ar-SA.js → locale='ar', en-US.js → locale='en'
506
+
507
+ 2. 任意目录层级是合法 locale 代码 → 使用该目录名
508
+ 例: lang/en-US/common.js → locale='en'
509
+
510
+ 3. 都不满足 → 使用文件名(后续 multi-locale 检测覆盖)
511
+ ```
512
+
513
+ ---
514
+
515
+ ## 🌍 多 Locale 文件格式支持
516
+
517
+ ### 格式 1: 单文件单 Locale(推荐)
518
+
519
+ ```javascript
520
+ // locales/lang/en-US/common.js
521
+ export default {
522
+ hello: 'Hello',
523
+ welcome: 'Welcome'
524
+ };
525
+
526
+ // locales/lang/ar-SA/common.js
527
+ export default {
528
+ hello: 'مرحبا',
529
+ welcome: 'أهلا وسهلا'
530
+ };
531
+ ```
532
+
533
+ ### 格式 2: 单文件多 Locale(自动识别)
534
+
535
+ ```javascript
536
+ // locales/oom/en-US.js
537
+ export default {
538
+ enUS: { hello: 'Hello', welcome: 'Welcome' },
539
+ arSA: { hello: 'مرحبا', welcome: 'أهلا وسهلا' },
540
+ ptPT: { hello: 'Olá', welcome: 'Bem-vindo' }
541
+ };
542
+ ```
543
+
544
+ **自动识别条件**: 顶层所有 key 均为合法 locale 代码 (`/^[a-z]{2,3}([A-Z]{2,3}|[-_][a-zA-Z]{2,3})?$/`)
545
+
546
+ ### 格式 3: JSON / YAML 多 Locale
547
+
548
+ ```json
549
+ {
550
+ "en-US": { "hello": "Hello" },
551
+ "ar-SA": { "hello": "مرحبا" }
552
+ }
553
+ ```
554
+
555
+ 同样自动拆分合并。
556
+
557
+ ---
558
+
559
+ ## 🔍 占位符一致性校验
560
+
561
+ Pull 回写时自动检测翻译中的占位符是否与英文源一致。
562
+
563
+ ### 支持的占位符类型
564
+
565
+ | 类型 | 正则 | 示例 |
566
+ |------|------|------|
567
+ | 花括号 | `{name}` | `{count}` `{username}` |
568
+ | 美元花括号 | `${expr}` | `${process.env.API}` |
569
+ | printf 风格 | `%s` `%d` | `%s` `%d` |
570
+ | 命名 printf | `%{name}` | `%{value}` |
571
+ | XML 标签 | `<tag/>` `<br/>` | `<br/>` |
572
+
573
+ ### 校验输出
574
+
575
+ ```
576
+ ⚠️ Pull 警告:
577
+ [arSA] 缺失占位符: {username}, %s
578
+ [ptPT] 多余占位符: {email}
579
+ [arSA] 来源为黄色单元格(待人工确认)
580
+ ```
581
+
582
+ ---
583
+
584
+ ## 🔒 安全防护
585
+
586
+ ### 公式注入拦截
587
+
588
+ Google Sheets 中 `=` 开头的值会被解释为公式,存在安全风险。Push 时自动检测并拒绝:
589
+
590
+ ```js
591
+ // ❌ 拒绝写入
592
+ "=IMPORTXML(\"https://evil.com\")" → Error: [Formula Injection]
593
+
594
+ // ✅ 正常写入
595
+ "Hello World" → OK
596
+ ```
597
+
598
+ ### 控制字符检测
599
+
600
+ 拒绝包含 ASCII 控制字符(`\x00-\x08`, `\x0B-\x0C`, `\x0E-\x1F`, `\x7F`)的翻译值。
601
+
602
+ ### 凭证安全
603
+
604
+ - Service Account JSON **不保留在项目文件中**
605
+ - Base64 编码后存入 Shell Profile 环境变量
606
+ - `init` 完成后提示删除原始文件:`rm ./service-account.json`
607
+
608
+ ---
609
+
610
+ ## ⚙️ 配置参考
611
+
612
+ ### .locale-sync.json
613
+
614
+ ```json
615
+ {
616
+ "sheetId": "1NbUr7mtbRMZXtrOmyvcjiaMeQzzgWmYhNUke7uUSe54",
617
+ "locales": ["locales/lang"],
618
+ "project": "my-project",
619
+ "ignore": ["node_modules", "*.d.ts"],
620
+ "sync": {
621
+ "dryRun": false,
622
+ "pruneDeadKeys": false,
623
+ "antiLoopWindow": 0
624
+ }
625
+ }
626
+ ```
627
+
628
+ ### 配置项说明
629
+
630
+ | 字段 | 类型 | 默认值 | 说明 |
631
+ |------|------|--------|------|
632
+ | `sheetId` | string | - | Google Sheets 表格 ID |
633
+ | `locales` | string[] | `["locales/lang"]` | 本地语言包目录列表 |
634
+ | `project` | string | `package.json.name` | 项目名(Project 表名) |
635
+ | `ignore` | string[] | `[]` | 扫描时要忽略的文件/目录 |
636
+ | `sync.dryRun` | boolean | `false` | 预览模式,不真正写入 |
637
+ | `sync.pruneDeadKeys` | boolean | `false` | 是否删除过期 key(待实现) |
638
+ | `sync.antiLoopWindow` | number | `0` | 防环路窗口(秒),0 表示关闭 |
639
+ | `sync.ciAntiLoopWindow` | number | `300` | CI 环境下的防环路窗口 |
640
+ | `keyFile` | string | `./service-account.json` | 凭证文件路径(向后兼容) |
641
+
642
+ ### 环境变量检测
643
+
644
+ - `CI=true` 或 `--ci` 参数:自动切换为 `ciAntiLoopWindow`
645
+ - `LOCALE_SYNC_CREDENTIALS`:Base64 编码的凭证(优先于 keyFile)
646
+
647
+ ---
648
+
649
+ ## 🔐 凭证管理
650
+
651
+ ### 凭证获取
652
+
653
+ 1. 前往 [Google Cloud Console](https://console.cloud.google.com/)
654
+ 2. 创建/选择项目,启用 **Google Sheets API**
655
+ 3. 创建 Service Account → 生成 JSON 密钥
656
+ 4. 将生成的文件保存为 `service-account.json`
657
+ 5. 将该文件拖入项目根目录
658
+
659
+ ### 凭证存储
660
+
661
+ ```
662
+ 方式一:环境变量(推荐,CI 友好)
663
+ LOCALE_SYNC_CREDENTIALS = "base64-encoded-json"
664
+
665
+ 方式二:Shell Profile
666
+ 运行 locale-sync init → 自动写入 ~/.zshrc 或 ~/.bash_profile
667
+
668
+ 方式三:本地文件(向后兼容)
669
+ 项目根目录/service-account.json
670
+ ```
671
+
672
+ ### Google Sheets 权限
673
+
674
+ 将 Service Account 的邮箱(`client_email`,形如 `xxx@xxx.iam.gserviceaccount.com`)添加为 Google Sheet 的 **Editor**。
675
+
676
+ ---
677
+
678
+ ## 🤖 CI/CD 集成
679
+
680
+ ### GitHub Actions 示例
681
+
682
+ ```yaml
683
+ - name: Sync Locales to Google Sheets
684
+ env:
685
+ LOCALE_SYNC_CREDENTIALS: ${{ secrets.LOCALE_SYNC_CREDENTIALS }}
686
+ run: |
687
+ npx locale-sync push --yes
688
+ ```
689
+
690
+ ### 注意事项
691
+
692
+ - CI 环境自动启用 `ciAntiLoopWindow`(默认 300 秒)
693
+ - 使用 `--yes` 跳过交互确认
694
+ - 凭证通过 CI Secrets 注入 `LOCALE_SYNC_CREDENTIALS` 环境变量
695
+
696
+ ---
697
+
698
+ ## 📁 本地语言包目录结构
699
+
700
+ ### 示例项目结构
701
+
702
+ ```
703
+ project/
704
+ ├── .locale-sync.json # 项目配置
705
+ ├── locales/
706
+ │ └── lang/
707
+ │ ├── en-US/ # 以 locale 命名的目录
708
+ │ │ ├── index.js
709
+ │ │ ├── common.js
710
+ │ │ ├── home.js
711
+ │ │ └── account.js
712
+ │ ├── ar-SA/
713
+ │ │ ├── index.js
714
+ │ │ ├── common.js
715
+ │ │ └── home.js
716
+ │ ├── pt-BR/
717
+ │ │ └── ...
718
+ │ └── es-MX/
719
+ │ └── ...
720
+
721
+ ├── locales_foota/ # 另一个 locale 目录(可配置多个)
722
+ │ └── lang/en-US/...
723
+
724
+ └── locales_gulf/ # 第三个 locale 目录
725
+ └── lang/en-US/...
726
+ ```
727
+
728
+ ### Locale 代码规范
729
+
730
+ 支持多种命名风格,内部自动规范化:
731
+
732
+ | 输入格式 | 规范化 | Google Sheets 表头 |
733
+ |----------|--------|-------------------|
734
+ | `en-US` | `enus` | `en-US` |
735
+ | `ar-SA` | `arsa` | `ar-SA` |
736
+ | `pt-BR` | `ptbr` | `pt-BR` |
737
+ | `zh_CN` | `zhcn` | `zh-CN` |
738
+ | `en` | `en` | `en` |
739
+ | `arb` | `arb` | `arb` |
740
+
741
+ ---
742
+
743
+ ## 🛠 技术栈
744
+
745
+ | 类别 | 技术 | 用途 |
746
+ |------|------|------|
747
+ | **运行时** | Node.js >= 16 | JavaScript 运行环境 |
748
+ | **CLI 框架** | Commander.js + Inquirer.js | 命令行交互 |
749
+ | **AST 解析** | @babel/parser + recast | JS 文件解析与保真回写 |
750
+ | **AST 遍历** | @babel/traverse + @babel/types | AST 节点遍历/类型判断 |
751
+ | **Google API** | googleapis v173+ | Google Sheets API v4 |
752
+ | **JSON 解析** | comment-json | 保留 JSON 注释的解析/序列化 |
753
+ | **YAML 解析** | js-yaml | YAML 格式支持 |
754
+ | **重试机制** | p-retry | API 请求自动重试 |
755
+ | **并发控制** | p-queue | 批量操作队列管理 |
756
+ | **日志美化** | chalk + ora | 彩色日志 + 加载动画 |
757
+ | **文件匹配** | fast-glob | 递归文件扫描 |
758
+ | **HTTP Polyfill** | undici + node-fetch | 低版本 Node.js 兼容 |
759
+
760
+ ---
761
+
762
+ ## ❓ 常见问题
763
+
764
+ ### Q: Push 后某些翻译没有出现在 Sheets 中?
765
+
766
+ 检查是否被 **忽略规则** 排除(`locale-sync ignore --list`)。含 `/` 的路径(如 `locales/i18n.js`)以项目根目录为基准解析,确认文件确实在扫描目录内。
767
+
768
+ ### Q: 为什么某些单元格没有被 Push 更新?
769
+
770
+ 该单元格可能处于以下状态之一:
771
+ - 无色 + 有值 → **人工编辑锁定**,Push 不会覆盖
772
+ - 黄色 → **冲突状态**,由人工确认后清除颜色
773
+ - 灰色行 → **已废弃**,整行跳过
774
+
775
+ ### Q: 如何处理 Push 后出现的黄色冲突行?
776
+
777
+ 1. 打开 Google Sheet
778
+ 2. 查看黄色行中的冲突 locale
779
+ 3. 确认正确翻译,删除错误的
780
+ 4. 清除黄色背景 → 下次 Push 将遵循新值
781
+
782
+ ### Q: Pull 后本地文件格式变了?
783
+
784
+ JS 文件的 AST 回写使用 recast 保真输出,应保留原始代码风格。如遇格式变化,请检查源文件是否有语法错误导致 AST 解析失败。
785
+
786
+ ### Q: 如何在多项目间共享翻译?
787
+
788
+ Common 表中的翻译对所有使用同一 Google Sheet 的项目可见。Pull 时以 Project 表优先,Common 表作为 fallback。
789
+
790
+ ### Q: antiLoopWindow 是什么?
791
+
792
+ 防止 Push 和 Pull 在短时间内形成循环更新。例如 Pull 写入某行后,antiLoopWindow 秒内来自 Pull 的行不会被 Push 回写。CI 环境下默认 300 秒。
793
+
794
+ ---
795
+
796
+ ## 📄 License
797
+
798
+ ISC
799
+
800
+ ---